diff --git a/.agents/skills/helm-dev-environment/SKILL.md b/.agents/skills/helm-dev-environment/SKILL.md index e866ec6927..56061385c3 100644 --- a/.agents/skills/helm-dev-environment/SKILL.md +++ b/.agents/skills/helm-dev-environment/SKILL.md @@ -266,8 +266,9 @@ task running while using those endpoints. ### Viewing local traces The gateway exports OTLP/gRPC to -`http://openshell-collector.observability.svc.cluster.local:4317` through the -default Skaffold values. Forward OTLP/gRPC and the trace UI to the host: +`http://openshell-collector.observability.svc.cluster.local:4317` through +`ci/values-local-tracing.yaml`, which only `skaffold.yaml` layers. The Kubernetes +E2E wrapper omits it, so E2E clusters export no traces. Forward OTLP/gRPC and the trace UI to the host: ```bash mise run helm:k3s:forward @@ -278,9 +279,10 @@ Kubernetes compute-driver spans under their distinct service names, along with Agent Sandbox controller reconciliation spans linked through the Sandbox trace-context annotation. The same command exposes OTLP/gRPC on `http://127.0.0.1:4317` and, when deployed, the Kubernetes gateway on -`http://127.0.0.1:8090`. The local `gateway:docker`, `gateway:podman`, and -`gateway:vm` tasks detect the collector listener at startup and enable trace -export, OCSF log export, and the agent trace relay only while it is reachable. +`http://127.0.0.1:8090`. The local `gateway`, `gateway:docker`, +`gateway:podman`, and `gateway:vm` tasks enable trace export, OCSF log export, +and the agent trace relay only when `OPENSHELL_DEV_OTLP_ENDPOINT` is set, for +example `OPENSHELL_DEV_OTLP_ENDPOINT=http://127.0.0.1:4317`. Spans an agent exports from inside a sandbox arrive through the supervisor's relay with `openshell.telemetry.source=agent` and share `openshell.sandbox.id` with that sandbox's supervisor spans. @@ -642,6 +644,7 @@ for dependencies still declared in `Chart.yaml`. | `deploy/helm/openshell/skaffold.yaml` | Skaffold config — images, Helm releases, values overlays | | `deploy/helm/openshell/values.yaml` | Default Helm values | | `deploy/helm/openshell/ci/values-skaffold.yaml` | Dev overrides (image pull policy, TLS disabled for local Skaffold) | +| `deploy/helm/openshell/ci/values-local-tracing.yaml` | OTLP export to the `helm:k3s:create` collector (Skaffold only) | | `deploy/helm/openshell/ci/values-cert-manager.yaml` | cert-manager PKI overlay (opt-in; disables pkiInitJob) | | `deploy/helm/openshell/ci/values-gateway.yaml` | Envoy Gateway GRPCRoute + Gateway overlay | | `deploy/helm/openshell/ci/values-high-availability.yaml` | HA test overlay (`replicaCount: 2` with external PostgreSQL Secret) | diff --git a/.github/actions/setup-e2e-cli/action.yml b/.github/actions/setup-e2e-cli/action.yml index 8b30970d8d..6bc2a78ec2 100644 --- a/.github/actions/setup-e2e-cli/action.yml +++ b/.github/actions/setup-e2e-cli/action.yml @@ -1,6 +1,12 @@ name: Setup E2E CLI description: Download architecture-matched prebuilt OpenShell host CLIs for E2E tests +inputs: + conformance: + description: Also download the prebuilt openshell-conformance program and set OPENSHELL_CONFORMANCE_BIN + required: false + default: "false" + runs: using: composite steps: @@ -17,3 +23,19 @@ runs: chmod +x "$cli" "$cli" --version echo "OPENSHELL_BIN=$cli" >> "$GITHUB_ENV" + + - name: Download prebuilt openshell-conformance + if: inputs.conformance == 'true' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ runner.arch == 'X64' && 'openshell-conformance-x86_64-unknown-linux-musl' || 'openshell-conformance-aarch64-unknown-linux-musl' }} + path: .e2e/prebuilt-conformance + + - name: Configure prebuilt openshell-conformance + if: inputs.conformance == 'true' + shell: bash + run: | # zizmor: ignore[github-env] persists only a fixed path under the trusted workspace + program="$GITHUB_WORKSPACE/.e2e/prebuilt-conformance/openshell-conformance" + chmod +x "$program" + "$program" list + echo "OPENSHELL_CONFORMANCE_BIN=$program" >> "$GITHUB_ENV" diff --git a/.github/workflows/branch-checks.yml b/.github/workflows/branch-checks.yml index 8e21eb7a69..db22a74913 100644 --- a/.github/workflows/branch-checks.yml +++ b/.github/workflows/branch-checks.yml @@ -392,6 +392,7 @@ jobs: cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path tests/tmachine/Cargo.toml cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path tests/runner/Cargo.toml cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path tests/support/Cargo.toml + tasks/scripts/check-conformance-program.sh rust-postgres-test: name: Rust PostgreSQL tests diff --git a/.github/workflows/branch-e2e.yml b/.github/workflows/branch-e2e.yml index cc558fb4e2..0f59ab2a81 100644 --- a/.github/workflows/branch-e2e.yml +++ b/.github/workflows/branch-e2e.yml @@ -144,6 +144,60 @@ jobs: cargo-version: ${{ needs.version.outputs.cargo }} extra-cargo-flags: --no-default-features + # The conformance program, built once per architecture for the host lanes. + # It lives in its own workspace and is not released, so it is not part of + # build-binaries.yml. + build-conformance: + name: openshell-conformance (${{ matrix.triple }}) + needs: [pr_metadata] + if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_any_e2e == 'true' + permissions: + contents: read + strategy: + matrix: + include: + - triple: x86_64-unknown-linux-musl + runner: linux-amd64-cpu8 + - triple: aarch64-unknown-linux-musl + runner: linux-arm64-cpu8 + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ github.sha }} + - uses: ./.github/actions/setup-nix + with: + prepare-shell: "true" + cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }} + - uses: ./.github/actions/setup-rust + with: + cache-key: binaries-v2-openshell-conformance-${{ matrix.triple }} + save-cache: ${{ github.ref == 'refs/heads/main' }} + - name: Build openshell-conformance + shell: nix develop -c bash -euo pipefail {0} + env: + # Build into the root target directory, which setup-rust caches. + CARGO_TARGET_DIR: ${{ github.workspace }}/target + TRIPLE: ${{ matrix.triple }} + run: | + cargo build --release --locked --target "$TRIPLE" \ + --manifest-path tests/suites/conformance/Cargo.toml \ + --bin openshell-conformance + binary="target/$TRIPLE/release/openshell-conformance" + # The program must run on any lane host. + tasks/scripts/verify-static-binary.sh "$binary" + # Fails if the program has no tests linked. + "$binary" list + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: openshell-conformance-${{ matrix.triple }} + path: target/${{ matrix.triple }}/release/openshell-conformance + compression-level: 0 + retention-days: 5 + if-no-files-found: error + build-external-drivers: name: ${{ matrix.binary }} (${{ matrix.triple }}) needs: [pr_metadata, version] @@ -306,7 +360,7 @@ jobs: ] docker-e2e: - needs: [pr_metadata, build-binaries, build-images] + needs: [pr_metadata, build-binaries, build-conformance, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' permissions: actions: read @@ -314,11 +368,12 @@ jobs: packages: read uses: ./.github/workflows/e2e-docker-test.yml with: + prebuilt-conformance: true image-tag: ${{ github.sha }} runner: linux-arm64-cpu8 podman-e2e: - needs: [pr_metadata, build-binaries, build-images] + needs: [pr_metadata, build-binaries, build-conformance, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' permissions: actions: read @@ -326,12 +381,13 @@ jobs: packages: read uses: ./.github/workflows/e2e-podman-test.yml with: + prebuilt-conformance: true image-tag: ${{ github.sha }} suite-matrix: >- [{"suite":"ci","runner":"ubuntu-26.04","podman_major":"5","podman_package_version":"5.7.0+ds2-3build1","conmon_package_version":"2.1.13+ds1-2","cmd":"mise run --no-deps --skip-deps e2e:podman:ci"}] podman-additional-ca-e2e: - needs: [pr_metadata, build-binaries, build-images] + needs: [pr_metadata, build-binaries, build-conformance, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' permissions: actions: read @@ -339,21 +395,24 @@ jobs: packages: read uses: ./.github/workflows/e2e-podman-test.yml with: + prebuilt-conformance: true image-tag: ${{ github.sha }} suite-matrix: >- [{"suite":"additional-ca","runner":"ubuntu-26.04","podman_major":"5","podman_package_version":"5.7.0+ds2-3build1","conmon_package_version":"2.1.13+ds1-2","cmd":"OPENSHELL_E2E_PODMAN_TEST=additional_ca mise run --no-deps --skip-deps e2e:podman"}] vm-e2e: - needs: [pr_metadata, build-binaries, build-vm-driver] + needs: [pr_metadata, build-binaries, build-conformance, build-vm-driver] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' permissions: actions: read contents: read packages: read uses: ./.github/workflows/e2e-vm-test.yml + with: + prebuilt-conformance: true vm-additional-ca-e2e: - needs: [pr_metadata, build-binaries, build-vm-driver] + needs: [pr_metadata, build-binaries, build-conformance, build-vm-driver] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' permissions: actions: read @@ -361,11 +420,12 @@ jobs: packages: read uses: ./.github/workflows/e2e-vm-test.yml with: + prebuilt-conformance: true suite-name: additional-ca test-name: additional_ca docker-external-driver-e2e: - needs: [pr_metadata, build-binaries, build-gateway-plain, build-external-drivers, build-images] + needs: [pr_metadata, build-binaries, build-conformance, build-gateway-plain, build-external-drivers, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' permissions: actions: read @@ -373,6 +433,7 @@ jobs: packages: read uses: ./.github/workflows/e2e-docker-test.yml with: + prebuilt-conformance: true image-tag: ${{ github.sha }} runner: linux-arm64-cpu8 gateway-artifact: openshell-gateway-plain-aarch64-unknown-linux-gnu @@ -381,7 +442,7 @@ jobs: [{"suite":"external-driver","cmd":"mise run --no-deps --skip-deps e2e:docker:external-driver","apt_packages":"openssh-client","python_proto":false,"mcp":false}] podman-external-driver-e2e: - needs: [pr_metadata, build-binaries, build-gateway-plain, build-external-drivers, build-images] + needs: [pr_metadata, build-binaries, build-conformance, build-gateway-plain, build-external-drivers, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' permissions: actions: read @@ -389,6 +450,7 @@ jobs: packages: read uses: ./.github/workflows/e2e-podman-test.yml with: + prebuilt-conformance: true image-tag: ${{ github.sha }} gateway-artifact: openshell-gateway-plain-x86_64-unknown-linux-gnu external-driver-binary: openshell-driver-podman @@ -396,7 +458,7 @@ jobs: [{"suite":"external-driver","runner":"ubuntu-26.04","podman_major":"5","podman_package_version":"5.7.0+ds2-3build1","conmon_package_version":"2.1.13+ds1-2","cmd":"mise run --no-deps --skip-deps e2e:podman:external-driver"}] vm-external-driver-e2e: - needs: [pr_metadata, build-binaries, build-gateway-plain, build-vm-driver] + needs: [pr_metadata, build-binaries, build-conformance, build-gateway-plain, build-vm-driver] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' permissions: actions: read @@ -404,6 +466,7 @@ jobs: packages: read uses: ./.github/workflows/e2e-vm-test.yml with: + prebuilt-conformance: true gateway-artifact: openshell-gateway-plain-x86_64-unknown-linux-gnu suite-name: external-driver e2e-task: e2e:vm:external-driver @@ -420,7 +483,7 @@ jobs: image-tag: ${{ github.sha }} kubernetes-e2e: - needs: [pr_metadata, build-binaries, build-images] + needs: [pr_metadata, build-binaries, build-conformance, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' strategy: fail-fast: false @@ -444,13 +507,14 @@ jobs: packages: read uses: ./.github/workflows/e2e-kubernetes-test.yml with: + prebuilt-conformance: true image-tag: ${{ github.sha }} job-name: ${{ matrix.job_name }} agent-sandbox-version: ${{ matrix.agent_sandbox_version }} test-name: ${{ matrix.test_name }} kubernetes-app-listener-e2e: - needs: [pr_metadata, build-binaries, build-images] + needs: [pr_metadata, build-binaries, build-conformance, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' strategy: fail-fast: false @@ -478,6 +542,7 @@ jobs: packages: read uses: ./.github/workflows/e2e-kubernetes-test.yml with: + prebuilt-conformance: true image-tag: ${{ github.sha }} job-name: Kubernetes app listener (${{ matrix.name }}) e2e-task: e2e:kubernetes:app-listener @@ -487,7 +552,7 @@ jobs: listener-takeover: ${{ matrix.takeover }} kubernetes-workspace-managed-e2e: - needs: [pr_metadata, build-binaries, build-images] + needs: [pr_metadata, build-binaries, build-conformance, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' permissions: actions: read @@ -495,12 +560,13 @@ jobs: packages: read uses: ./.github/workflows/e2e-kubernetes-test.yml with: + prebuilt-conformance: true image-tag: ${{ github.sha }} job-name: Kubernetes E2E (workspace managed mode) e2e-task: e2e:kubernetes:workspace-managed kubernetes-external-driver-e2e: - needs: [pr_metadata, build-binaries, build-gateway-plain, build-external-drivers, build-images] + needs: [pr_metadata, build-binaries, build-conformance, build-gateway-plain, build-external-drivers, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' permissions: actions: read @@ -508,6 +574,7 @@ jobs: packages: read uses: ./.github/workflows/e2e-kubernetes-test.yml with: + prebuilt-conformance: true image-tag: ${{ github.sha }} job-name: Kubernetes E2E (external compute driver) e2e-task: e2e:kubernetes:external-driver @@ -516,7 +583,7 @@ jobs: cluster-images: sandbox supervisor kubernetes-workspace-operator-e2e: - needs: [pr_metadata, build-binaries, build-images] + needs: [pr_metadata, build-binaries, build-conformance, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' permissions: actions: read @@ -524,12 +591,13 @@ jobs: packages: read uses: ./.github/workflows/e2e-kubernetes-test.yml with: + prebuilt-conformance: true image-tag: ${{ github.sha }} job-name: Kubernetes E2E (workspace operator mode) e2e-task: e2e:kubernetes:workspace-operator kubernetes-ha-e2e: - needs: [pr_metadata, build-binaries, build-images] + needs: [pr_metadata, build-binaries, build-conformance, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_kubernetes_ha_e2e == 'true' permissions: actions: read @@ -537,6 +605,7 @@ jobs: packages: read uses: ./.github/workflows/e2e-kubernetes-test.yml with: + prebuilt-conformance: true image-tag: ${{ github.sha }} job-name: Kubernetes HA E2E (Rust smoke) extra-helm-values: deploy/helm/openshell/ci/values-high-availability.yaml @@ -546,7 +615,7 @@ jobs: use-envoy-gateway: true kubernetes-credential-drivers-e2e: - needs: [pr_metadata, build-binaries, build-images] + needs: [pr_metadata, build-binaries, build-conformance, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_kubernetes_credential_drivers_e2e == 'true' permissions: actions: read @@ -554,6 +623,7 @@ jobs: packages: read uses: ./.github/workflows/e2e-kubernetes-test.yml with: + prebuilt-conformance: true image-tag: ${{ github.sha }} job-name: Kubernetes Credential Drivers E2E e2e-task: e2e:kubernetes:credential-drivers diff --git a/.github/workflows/e2e-docker-test.yml b/.github/workflows/e2e-docker-test.yml index 501ded8d34..faa0121cd2 100644 --- a/.github/workflows/e2e-docker-test.yml +++ b/.github/workflows/e2e-docker-test.yml @@ -21,6 +21,10 @@ on: required: false type: string default: "" + prebuilt-conformance: + required: false + type: boolean + default: false external-driver-binary: required: false type: string @@ -67,6 +71,8 @@ jobs: persist-credentials: false - uses: ./.github/actions/setup-e2e-cli + with: + conformance: ${{ inputs.prebuilt-conformance }} - uses: ./.github/actions/setup-e2e-gateway with: diff --git a/.github/workflows/e2e-kubernetes-test.yml b/.github/workflows/e2e-kubernetes-test.yml index 867b8c1dd7..1ae616e36e 100644 --- a/.github/workflows/e2e-kubernetes-test.yml +++ b/.github/workflows/e2e-kubernetes-test.yml @@ -67,6 +67,11 @@ on: required: false type: string default: "" + prebuilt-conformance: + description: "Run conformance with the openshell-conformance artifact instead of building it" + required: false + type: boolean + default: false external-driver-binary: description: "Optional standalone compute driver binary used to compose a local test image" required: false @@ -124,6 +129,8 @@ jobs: - name: Use prebuilt OpenShell CLI uses: ./.github/actions/setup-e2e-cli + with: + conformance: ${{ inputs.prebuilt-conformance }} - name: Use prebuilt OpenShell gateway if: inputs.gateway-artifact != '' diff --git a/.github/workflows/e2e-podman-test.yml b/.github/workflows/e2e-podman-test.yml index d70ad6c60c..ac6d7d66ef 100644 --- a/.github/workflows/e2e-podman-test.yml +++ b/.github/workflows/e2e-podman-test.yml @@ -17,6 +17,10 @@ on: required: false type: string default: "" + prebuilt-conformance: + required: false + type: boolean + default: false external-driver-binary: required: false type: string @@ -52,6 +56,8 @@ jobs: persist-credentials: false - uses: ./.github/actions/setup-e2e-cli + with: + conformance: ${{ inputs.prebuilt-conformance }} - uses: ./.github/actions/setup-e2e-gateway with: diff --git a/.github/workflows/e2e-suites.yml b/.github/workflows/e2e-suites.yml index 04bd61ef37..791761befb 100644 --- a/.github/workflows/e2e-suites.yml +++ b/.github/workflows/e2e-suites.yml @@ -77,6 +77,11 @@ jobs: {"environment":"ubuntu-docker-rootful","installer":"deb","family":"features","package":"oci-image","gateway":"openshell","runner":"ubuntu-24.04"}, {"environment":"fedora-podman-rootful","installer":"rpm","family":"features","package":"oci-image","gateway":"openshell","runner":"ubuntu-24.04"}, {"environment":"fedora-podman-rootless","installer":"rpm","family":"features","package":"oci-image","gateway":"openshell","runner":"ubuntu-24.04"}, + {"environment":"ubuntu-docker-rootful","installer":"deb","family":"features","package":"host-gateway","gateway":"openshell","runner":"ubuntu-24.04"}, + {"environment":"fedora-podman-rootful","installer":"rpm","family":"features","package":"host-gateway","gateway":"openshell","runner":"ubuntu-24.04"}, + {"environment":"fedora-podman-rootless","installer":"rpm","family":"features","package":"host-gateway","gateway":"openshell","runner":"ubuntu-24.04"}, + {"environment":"ubuntu-k3s","installer":"k3s","family":"features","package":"host-gateway","gateway":"tmachine","runner":"ubuntu-24.04"}, + {"environment":"ubuntu-k3s","installer":"k3s-ha","family":"features","package":"host-gateway","gateway":"tmachine","runner":"ubuntu-24.04"}, {"environment":"fedora-podman-rootful-keycloak","installer":"rpm","family":"features","package":"keycloak-provider-refresh","gateway":"openshell","runner":"ubuntu-24.04"}, {"environment":"fedora-podman-rootless-keycloak","installer":"rpm","family":"features","package":"keycloak-provider-refresh","gateway":"openshell","runner":"ubuntu-24.04"}, {"environment":"fedora-podman-rootful-userns-default","installer":"binaries","family":"drivers/podman","gateway":"tmachine","runner":"ubuntu-24.04"}, diff --git a/.github/workflows/e2e-vm-test.yml b/.github/workflows/e2e-vm-test.yml index 28b4c81db5..82d86058b8 100644 --- a/.github/workflows/e2e-vm-test.yml +++ b/.github/workflows/e2e-vm-test.yml @@ -14,6 +14,10 @@ on: required: false type: string default: "" + prebuilt-conformance: + required: false + type: boolean + default: false suite-name: required: false type: string @@ -47,6 +51,8 @@ jobs: persist-credentials: false - uses: ./.github/actions/setup-e2e-cli + with: + conformance: ${{ inputs.prebuilt-conformance }} - uses: ./.github/actions/setup-e2e-gateway with: diff --git a/AGENTS.md b/AGENTS.md index 2ba6597b9c..8866ead489 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -153,6 +153,7 @@ ocsf_emit!(event); - `crate::ocsf_ctx()` returns the process-wide `EventContext`. It is always available (falls back to defaults in tests). - `ocsf_emit!()` is non-blocking and cannot panic. It stores the event in a thread-local and emits via `tracing::info!()`. +- In `openshell-supervisor-network`, emit with `crate::telemetry::egress_emit!` instead, or `crate::telemetry::emit_decision` for the records of one decision, so denied HTTP decisions get a deny span. Clippy rejects `ocsf_emit!` there. - The shorthand layer and JSONL layer extract the event from the thread-local. The shorthand format is derived automatically from the builder fields. - For security findings, **dual-emit**: one domain event (e.g., `SshActivityBuilder`) AND one `DetectionFindingBuilder` for the same incident. - Never log secrets, credentials, or query parameters in OCSF messages. The OCSF JSONL file may be shipped to external systems. diff --git a/CI.md b/CI.md index ff278592e7..a1a1b8bc77 100644 --- a/CI.md +++ b/CI.md @@ -218,7 +218,7 @@ nextest filterset: nix run .#openshell-test-runner -- \ --environment ubuntu-docker-rootful --installer deb \ --family conformance --package conformance \ - --filter 'binary_id(=conformance::policy_advisor)' + --filter 'test(/^policy_advisor::/)' ``` A local run does not replace the required PR E2E gate. diff --git a/Cargo.lock b/Cargo.lock index 3932bee044..d178930a4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4388,6 +4388,7 @@ dependencies = [ "openshell-bootstrap", "openshell-core", "openshell-crypto", + "openshell-otel", "openshell-policy", "openshell-providers", "openshell-sdk", @@ -5333,11 +5334,13 @@ dependencies = [ "openshell-ocsf", "openshell-ocsf-validate", "openshell-otel", + "openshell-otel-test-support", "openshell-policy", "openshell-policy-schema", "openshell-sandbox-backend", "openshell-supervisor-middleware", "openshell-supervisor-middleware-builtins", + "opentelemetry-proto", "opentelemetry_sdk", "prost-types", "rcgen", diff --git a/TESTING.md b/TESTING.md index 5e74f53c72..a3936ad649 100644 --- a/TESTING.md +++ b/TESTING.md @@ -181,6 +181,12 @@ by `OPENSHELL_BIN`. See [tests/testkit/README.md](tests/testkit/README.md) for how to write and run tests. +The conformance crate also builds `openshell-conformance`, a program that +runs the conformance suite against a gateway without cargo or nextest +(`openshell-conformance list`, `ready`, `run`). It covers only the +conformance suite. See +[tests/suites/conformance/README.md](tests/suites/conformance/README.md). + Suites live under `tests/suites/`, in Cargo workspaces with their own lockfiles: `conformance`, `features`, and one workspace per driver suite under `drivers/`. Run the `conformance` suites against a local Docker @@ -232,14 +238,14 @@ nix run .#openshell-test-runner -- \ --family conformance --package conformance ``` -It needs KVM, and tmachine only boots guests of the host's architecture. The -runner points the tests at the installed CLI and the gateway the installer -registered (`--gateway`, default `openshell`). A run passes if nextest -succeeds; nextest fails a run with no tests. The runner exits with 1 when a -test failed and 2 when the run could not complete, for example because the -environment didn't come up. It writes `junit.xml`, `nextest.log` and, after a -failure, the guest's diagnostics to `artifacts/test-results/` (`--results`), -even when the run timed out. +It needs KVM on Linux or the Hypervisor framework on macOS, and tmachine only +boots guests of the host's architecture. The runner points the tests at the +installed CLI and the gateway the installer registered (`--gateway`, default +`openshell`). A run passes if nextest succeeds; nextest fails a run with no +tests. The runner exits with 1 when a test failed and 2 when the run could not +complete, for example because the environment didn't come up. It writes +`junit.xml`, `nextest.log` and, after a failure, the guest's diagnostics to +`artifacts/test-results/` (`--results`), even when the run timed out. `--filter ` narrows the tests with a nextest filterset, and `--keep` leaves the VM running until you press Enter. Two runs on one host @@ -419,7 +425,10 @@ Suites: - Common suite (`--features e2e`) - driver-neutral CLI behavior, sandbox lifecycle, sync, port forwarding, policy, and provider tests. - Conformance (`tests/suites/conformance`) - the testkit `conformance` crate: lifecycle, the CLI, persistent workspaces, file transfer, and the policy - advisor. Driver E2E runs it with nextest against the lane's gateway. The + advisor. Driver E2E runs it with `openshell-conformance` against the lane's + gateway and writes a JUnit report under `results/`; set + `OPENSHELL_CONFORMANCE_BIN` to use a prebuilt program, as Branch E2E does + with the static build it makes once per architecture. The installed-artifact conformance lanes run the same crate through `openshell-test-runner` (see above). - Driver suites (`--features e2e-docker`, `e2e-podman`, `e2e-kubernetes`, or @@ -577,6 +586,13 @@ mise run e2e:oci-image:podman Both tasks set `OPENSHELL_TEST_CONTAINER_ENGINE`, the command that builds test images into the gateway's image store. +The `host-gateway` feature suite (`tests/suites/features/host-gateway`) checks +that a sandbox from the Python test image reaches a server the test runs on the +host through `host.openshell.internal`. CI runs it on Docker rootful, Podman +rootful and rootless, and the `k3s` and `k3s-ha` installers. The tests run on +the K3s node, so the K3s installers set the chart's `server.hostGatewayIP` to +the node's address. + Run the VM-backed Rust CLI e2e suite: ```shell diff --git a/crates/openshell-binary-identity/src/lib.rs b/crates/openshell-binary-identity/src/lib.rs index c5dc5696f3..245185bceb 100644 --- a/crates/openshell-binary-identity/src/lib.rs +++ b/crates/openshell-binary-identity/src/lib.rs @@ -13,13 +13,20 @@ use openshell_isolation_interface::contract::{BinaryIdentity, ResolveError}; #[cfg(target_os = "linux")] use openshell_isolation_interface::contract::{ExecutableIdentity, Sha256Digest}; #[cfg(target_os = "linux")] -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; #[cfg(target_os = "linux")] use std::sync::{Arc, Mutex}; #[cfg(target_os = "linux")] const EXECUTABLE_DIGEST_CACHE_CAPACITY: usize = 1_024; +/// Bounds the total cmdline bytes read from `/proc//cmdline`. This is a +/// diagnostic-read cap on the whole command line, not a per-argument limit: +/// the kernel allows a single argument larger than this cap, so one +/// maximum-size argument plus argv[0] already exceeds it and is truncated. +#[cfg(target_os = "linux")] +const CMDLINE_READ_LIMIT: usize = 128 * 1024; + /// Resolves executable identity from a Linux procfs process identifier. /// /// The configured scope bounds ancestry and cmdline collection to the observed @@ -106,20 +113,13 @@ fn resolve_linux_process( .map(|(snapshot, _)| snapshot.binary_path.clone()) .collect::>(); excluded_paths.push(snapshot.binary_path.clone()); - let cmdline_paths = cmdline_absolute_paths(&snapshot.cmdline) - .into_iter() - .chain( - ancestor_processes - .iter() - .flat_map(|(snapshot, _)| cmdline_absolute_paths(&snapshot.cmdline)), - ) - .filter(|path| !excluded_paths.contains(path)) - .fold(Vec::new(), |mut paths, path| { - if !paths.contains(&path) { - paths.push(path); - } - paths - }); + let cmdline_paths = collect_cmdline_paths( + &snapshot.cmdline, + ancestor_processes + .iter() + .map(|(snapshot, _)| snapshot.cmdline.as_slice()), + &excluded_paths, + ); // Digests computed during this call. They reach the shared cache only after // every snapshot validates, but later processes in the chain reuse them. @@ -243,7 +243,11 @@ fn cache_executable_digest( .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); if cache.len() >= EXECUTABLE_DIGEST_CACHE_CAPACITY { - cache.clear(); + // Evict a single arbitrary entry: clearing the whole cache would + // force a rehash of every live executable after one capacity crossing. + if let Some(evict) = cache.keys().next().copied() { + cache.remove(&evict); + } } cache.insert(key, digest); } @@ -335,8 +339,39 @@ fn process_stat(pid: u32) -> Result<(u32, u64), ResolveError> { #[cfg(target_os = "linux")] fn read_process_cmdline(pid: u32) -> Result, ResolveError> { + use std::io::Read as _; + let path = format!("/proc/{pid}/cmdline"); - std::fs::read(&path).map_err(|error| ResolveError::Failed(format!("read {path}: {error}"))) + let file = std::fs::File::open(&path) + // Keep the message prefix std::fs::read produced, so denial-path + // strings downstream stay byte-identical. + .map_err(|error| ResolveError::Failed(format!("read {path}: {error}")))?; + // Reading one byte past the limit makes the truncation predicate exact: + // a cmdline of exactly CMDLINE_READ_LIMIT bytes is kept whole. + let mut buffer = Vec::new(); + file.take(CMDLINE_READ_LIMIT as u64 + 1) + .read_to_end(&mut buffer) + .map_err(|error| ResolveError::Failed(format!("read {path}: {error}")))?; + Ok(cap_cmdline(buffer)) +} + +#[cfg(target_os = "linux")] +fn cap_cmdline(mut buffer: Vec) -> Vec { + if buffer.len() <= CMDLINE_READ_LIMIT { + return buffer; + } + buffer.truncate(CMDLINE_READ_LIMIT); + // Drop the last argument whether it is partial or happens to end exactly + // at the cap: a truncated fragment must not reach cmdline_absolute_paths, + // and diagnostic-only data does not justify boundary-case recovery. + if buffer.last().is_some_and(|byte| *byte == 0) { + buffer.pop(); + } + match buffer.iter().rposition(|byte| *byte == 0) { + Some(index) => buffer.truncate(index + 1), + None => buffer.clear(), + } + buffer } #[cfg(target_os = "linux")] @@ -363,6 +398,11 @@ fn executable_path(pid: u32) -> Result { } #[cfg(target_os = "linux")] +// Stack buffer by design: no heap allocation per cache miss. The array is +// zeroed on each call, a negligible cost next to hashing the executable. +// The 64 KiB size exceeds clippy's 16 KiB large_stack_arrays default, which +// is far below every thread stack in this call path. +#[allow(clippy::large_stack_arrays)] fn hash_executable(pid: u32, executable: &mut std::fs::File) -> Result { use std::fmt::Write as _; use std::io::Read as _; @@ -370,7 +410,7 @@ fn hash_executable(pid: u32, executable: &mut std::fs::File) -> Result Option { - std::fs::read_to_string(format!("/proc/{pid}/status")) - .ok()? - .lines() - .find_map(|line| line.strip_prefix("PPid:"))? - .trim() - .parse() - .ok() +#[derive(Debug, Default, PartialEq, Eq)] +struct ProcessStatus { + ppid: Option, + innermost_nspid: Option, +} + +#[cfg(target_os = "linux")] +fn read_process_status(pid: u32) -> Option { + let path = format!("/proc/{pid}/status"); + Some(parse_process_status(&std::fs::read_to_string(path).ok()?)) +} + +#[cfg(target_os = "linux")] +fn parse_process_status(status: &str) -> ProcessStatus { + let mut parsed = ProcessStatus::default(); + for line in status.lines() { + match line.split_once(':') { + Some(("PPid", value)) => parsed.ppid = value.trim().parse().ok(), + Some(("NSpid", value)) => { + parsed.innermost_nspid = value + .split_whitespace() + .next_back() + .and_then(|nspid| nspid.parse().ok()); + } + _ => {} + } + } + parsed } #[cfg(target_os = "linux")] @@ -440,28 +500,21 @@ fn nested_pid_namespace_init(pid: u32) -> Option { let mut current = pid; for _ in 0..MAX_DEPTH { - if namespace_pid(current) == Some(1) { + // One status read per hop carries both the namespace PID check and + // the parent PID for the next hop. + let status = read_process_status(current)?; + if status.innermost_nspid == Some(1) { // Host PID 1 is outside every workload. A nested namespace init // has a distinct host PID and is a valid workload ancestry root. return (current != 1).then_some(current); } - current = parent_pid(current).filter(|parent| *parent > 0 && *parent != current)?; + current = status + .ppid + .filter(|parent| *parent > 0 && *parent != current)?; } None } -#[cfg(target_os = "linux")] -fn namespace_pid(pid: u32) -> Option { - std::fs::read_to_string(format!("/proc/{pid}/status")) - .ok()? - .lines() - .find_map(|line| line.strip_prefix("NSpid:"))? - .split_whitespace() - .next_back()? - .parse() - .ok() -} - #[cfg(target_os = "linux")] fn cmdline_absolute_paths(cmdline: &[u8]) -> Vec { cmdline @@ -471,6 +524,32 @@ fn cmdline_absolute_paths(cmdline: &[u8]) -> Vec { .collect() } +#[cfg(target_os = "linux")] +fn collect_cmdline_paths<'a>( + own_cmdline: &'a [u8], + ancestor_cmdlines: impl Iterator, + excluded_paths: &[std::path::PathBuf], +) -> Vec { + // Insertion order is load-bearing: consumers compare the collected + // vectors wholesale, so the own cmdline comes first and ancestors follow + // nearest-first. The excluded-paths scan stays a bounded linear check + // over binary paths only. Candidates stream one cmdline at a time, and a + // PathBuf is cloned only for unique survivors, never for duplicates or + // excluded paths. + let mut paths = Vec::new(); + let mut seen_paths: HashSet = HashSet::new(); + for cmdline in std::iter::once(own_cmdline).chain(ancestor_cmdlines) { + for path in cmdline_absolute_paths(cmdline) { + if excluded_paths.contains(&path) || seen_paths.contains(&path) { + continue; + } + seen_paths.insert(path.clone()); + paths.push(path); + } + } + paths +} + #[cfg(all(test, target_os = "linux"))] mod tests { use super::*; @@ -523,15 +602,16 @@ mod tests { let _ = std::io::Read::read(&mut std::io::stdin(), &mut [0_u8]); } - #[test] - fn resolves_child_and_hashes_ancestor_chain() { - let parent_pid = std::process::id(); + /// Spawns `identity_helper_process` with optional extra argv entries and + /// waits for its readiness signal. Only the spawn itself reports errors: + /// callers decide how to react (the long-cmdline test skips on E2BIG). + fn spawn_identity_helper(extra_args: &[String]) -> std::io::Result { let mut child = std::process::Command::new(std::env::current_exe().unwrap()) .args(["--ignored", "--exact", "tests::identity_helper_process"]) + .args(extra_args) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) - .spawn() - .expect("spawn child"); + .spawn()?; let mut child_stdout = std::io::BufReader::new(child.stdout.take().expect("child stdout")); let mut line = String::new(); // Wait until the helper has entered the test before taking the @@ -545,7 +625,13 @@ mod tests { break; } } - let child = ChildGuard(child); + Ok(ChildGuard(child)) + } + + #[test] + fn resolves_child_and_hashes_ancestor_chain() { + let parent_pid = std::process::id(); + let child = spawn_identity_helper(&[]).expect("spawn child"); let identity = ProcfsIdentityResolver::for_process_tree(parent_pid) .resolve(child.0.id()) @@ -579,6 +665,41 @@ mod tests { assert!(cache.lock().unwrap().is_empty()); } + #[test] + fn hash_executable_matches_one_shot_sha256_over_the_same_file() { + let current_exe = std::env::current_exe().unwrap(); + let mut executable = std::fs::File::open(¤t_exe).unwrap(); + + let streamed = hash_executable(std::process::id(), &mut executable).unwrap(); + let one_shot = openshell_crypto::sha256(&std::fs::read(¤t_exe).unwrap()).unwrap(); + + assert_eq!(streamed.as_bytes(), &one_shot); + } + + #[test] + fn cache_executable_digest_evicts_one_entry_at_capacity() { + let sentinel: Sha256Digest = "ab".repeat(32).parse().unwrap(); + let cache = Mutex::new(HashMap::new()); + for index in 0..EXECUTABLE_DIGEST_CACHE_CAPACITY as u64 { + cache_executable_digest(&cache, synthetic_cache_key(index), sentinel); + } + assert_eq!( + cache.lock().unwrap().len(), + EXECUTABLE_DIGEST_CACHE_CAPACITY + ); + + let newest_key = synthetic_cache_key(EXECUTABLE_DIGEST_CACHE_CAPACITY as u64); + let newest_sentinel: Sha256Digest = "cd".repeat(32).parse().unwrap(); + cache_executable_digest(&cache, newest_key, newest_sentinel); + + // Crossing the capacity evicts exactly one entry, not the whole cache. + // Which entry drops is arbitrary, so only the size and the newest + // mapping are asserted. + let cache = cache.lock().unwrap(); + assert_eq!(cache.len(), EXECUTABLE_DIGEST_CACHE_CAPACITY); + assert_eq!(cache.get(&newest_key), Some(&newest_sentinel)); + } + #[test] fn process_tree_root_does_not_escape_into_host_ancestry() { let pid = std::process::id(); @@ -588,4 +709,198 @@ mod tests { assert!(identity.ancestors.is_empty()); } + + fn synthetic_cache_key(index: u64) -> ExecutableCacheKey { + ExecutableCacheKey { + device: 1, + inode: index, + size: 1, + mtime: 0, + mtime_nsec: 0, + ctime: 0, + ctime_nsec: 0, + } + } + + #[test] + fn resolves_child_with_cmdline_beyond_the_read_limit() { + // The kernel caps a single argument at a size that already reaches + // the whole-cmdline cap, so one oversized argument fails spawn with + // E2BIG before the child ever runs. The cmdline is instead pushed + // past CMDLINE_READ_LIMIT with several arguments that each stay under + // the per-argument limit. Separately, the whole argv+env list is + // capped at RLIMIT_STACK/4, so a host with a small stack limit can + // still reject the filler list with E2BIG; that skips the test + // instead of failing it. + let filler = "x".repeat(64 * 1024); + let fillers = [filler.clone(), filler.clone(), filler]; + let parent_pid = std::process::id(); + let child = match spawn_identity_helper(&fillers) { + Ok(child) => child, + Err(error) if error.kind() == std::io::ErrorKind::ArgumentListTooLong => { + eprintln!( + "skipping: host argv+env limit (RLIMIT_STACK/4) too small for the filler arguments" + ); + return; + } + Err(error) => panic!("spawn child: {error}"), + }; + + let identity = ProcfsIdentityResolver::for_process_tree(parent_pid) + .resolve(child.0.id()) + .expect("resolve child identity"); + assert!(identity.executable.digest.is_some()); + + let cmdline = read_process_cmdline(child.0.id()).expect("read capped cmdline"); + assert!(cmdline.len() <= CMDLINE_READ_LIMIT); + // The capped cmdline ends on an argument boundary, not mid-argument. + assert_eq!(cmdline.last(), Some(&0)); + // Every surviving filler argument is complete: the argv prefix + // (executable path plus harness flags) is far below one filler, so + // the cut lands inside the second filler and only the first one, + // which ends before the cap, survives whole. + let complete_fillers = cmdline + .split(|byte| *byte == 0) + .filter(|argument| !argument.is_empty() && argument.iter().all(|byte| *byte == b'x')) + .count(); + assert_eq!(complete_fillers, 1); + } + + #[test] + fn cap_cmdline_keeps_cmdline_at_the_limit_whole() { + let mut buffer = b"/bin/one\0".to_vec(); + buffer.extend(vec![b'a'; CMDLINE_READ_LIMIT - buffer.len() - 1]); + buffer.push(0); + assert_eq!(buffer.len(), CMDLINE_READ_LIMIT); + + assert_eq!(cap_cmdline(buffer.clone()), buffer); + } + + #[test] + fn cap_cmdline_truncates_over_limit_cmdline_and_drops_last_argument() { + let mut buffer = b"/bin/one\0".to_vec(); + buffer.extend(vec![b'a'; CMDLINE_READ_LIMIT - buffer.len() - 1]); + buffer.push(0); + buffer.push(b'x'); + assert_eq!(buffer.len(), CMDLINE_READ_LIMIT + 1); + + assert_eq!(cap_cmdline(buffer), b"/bin/one\0"); + } + + #[test] + fn cap_cmdline_drops_argument_terminated_at_the_cut_point() { + // The NUL terminating the second argument is the last byte of the + // cut region: the argument is complete, but the cap deliberately + // drops it rather than recover boundary-case arguments. + let mut buffer = b"/bin/one\0".to_vec(); + buffer.extend(vec![b'a'; CMDLINE_READ_LIMIT - buffer.len() - 1]); + buffer.push(0); + buffer.extend(b"/bin/two\0"); + assert!(buffer.len() > CMDLINE_READ_LIMIT); + + assert_eq!(cap_cmdline(buffer), b"/bin/one\0"); + } + + #[test] + fn cap_cmdline_drops_sole_argument_over_the_limit_entirely() { + let buffer = vec![b'a'; CMDLINE_READ_LIMIT + 1]; + + assert_eq!(cap_cmdline(buffer), Vec::::new()); + } + + #[test] + fn cap_cmdline_passes_short_cmdlines_through() { + let without_trailing_nul = b"/bin/one\0/bin/two".to_vec(); + assert_eq!( + cap_cmdline(without_trailing_nul.clone()), + without_trailing_nul + ); + + assert_eq!(cap_cmdline(Vec::new()), Vec::::new()); + } + + #[test] + fn read_process_cmdline_maps_a_missing_process_to_a_read_error() { + // u32::MAX - 1 is beyond the kernel PID ceiling, so reading + // /proc//cmdline fails. The message prefix must match what + // std::fs::read produced, since the string reaches identity-source + // denial paths downstream. + let pid = u32::MAX - 1; + match read_process_cmdline(pid) { + Err(ResolveError::Failed(message)) => { + let expected = format!("read /proc/{pid}/cmdline: "); + assert!( + message.starts_with(&expected), + "unexpected error message: {message}" + ); + } + other => panic!("expected a failed cmdline read, got {other:?}"), + } + } + + #[test] + fn parse_process_status_reads_ppid_and_innermost_nspid() { + let status = "Name:\tbash\nUmask:\t0022\nState:\tS\nPPid:\t1234\n\ + NSpid:\t5678\t9\n"; + let parsed = parse_process_status(status); + assert_eq!(parsed.ppid, Some(1234)); + // The last NSpid field is the innermost namespace PID. + assert_eq!(parsed.innermost_nspid, Some(9)); + } + + #[test] + fn parse_process_status_keeps_a_zero_ppid_distinct_from_a_missing_one() { + // Some(0) vs None is load-bearing: the ancestry walk's `parent > 0` + // termination filter relies on a present-but-zero PPid being Some. + let parsed = parse_process_status("Name:\tkthreadd\nPPid:\t0\nNSpid:\t2\n"); + assert_eq!(parsed.ppid, Some(0)); + assert_eq!(parsed.innermost_nspid, Some(2)); + } + + #[test] + fn parse_process_status_reports_missing_fields_as_none() { + let parsed = parse_process_status("Name:\tbash\nState:\tS\n"); + assert_eq!(parsed.ppid, None); + assert_eq!(parsed.innermost_nspid, None); + } + + #[test] + fn parse_process_status_reports_garbage_as_none() { + let parsed = parse_process_status(""); + assert_eq!(parsed.ppid, None); + assert_eq!(parsed.innermost_nspid, None); + + let parsed = parse_process_status("PPid:\tnot-a-pid\nNSpid:\talso not pids\n"); + assert_eq!(parsed.ppid, None); + assert_eq!(parsed.innermost_nspid, None); + } + + #[test] + fn read_process_status_returns_none_for_a_missing_process() { + // u32::MAX - 1 is beyond the kernel PID ceiling, so the status file + // cannot be read and the namespace walk terminates on this hop. + // parse_process_status alone never yields None: only the failed read + // does, which keeps a garbage status distinct from an absent one. + assert_eq!(read_process_status(u32::MAX - 1), None); + } + + #[test] + fn collect_cmdline_paths_preserves_order_and_deduplicates() { + let own = b"/own/exe\0/usr/bin/dupe\0relative\0".to_vec(); + let ancestors = [ + b"/anc/one\0/usr/bin/dupe\0".to_vec(), + b"/anc/one\0/anc/two\0".to_vec(), + ]; + let excluded_paths = vec![std::path::PathBuf::from("/own/exe")]; + + let paths = + collect_cmdline_paths(&own, ancestors.iter().map(Vec::as_slice), &excluded_paths); + + let expected = [ + std::path::PathBuf::from("/usr/bin/dupe"), + std::path::PathBuf::from("/anc/one"), + std::path::PathBuf::from("/anc/two"), + ]; + assert_eq!(paths, expected); + } } diff --git a/crates/openshell-cli/Cargo.toml b/crates/openshell-cli/Cargo.toml index 0cdd32ade0..d7e1c0b8e4 100644 --- a/crates/openshell-cli/Cargo.toml +++ b/crates/openshell-cli/Cargo.toml @@ -18,6 +18,7 @@ path = "src/main.rs" openshell-crypto = { workspace = true } openshell-bootstrap = { path = "../openshell-bootstrap" } openshell-core = { path = "../openshell-core", default-features = false } +openshell-otel = { path = "../openshell-otel", default-features = false } openshell-policy = { path = "../openshell-policy" } openshell-providers = { path = "../openshell-providers" } openshell-sdk = { path = "../openshell-sdk" } diff --git a/crates/openshell-cli/src/main.rs b/crates/openshell-cli/src/main.rs index 7411629483..18297f1518 100644 --- a/crates/openshell-cli/src/main.rs +++ b/crates/openshell-cli/src/main.rs @@ -2191,8 +2191,8 @@ enum PolicyCommands { name: Option, /// Add or merge an endpoint: host:port[:access[:protocol[:enforcement[:options]]]]. - /// Options include allowed-ip=..., credential rewrite flags, and - /// allow-uninspected-credentials. + /// Options include allowed-ip=..., allow-encoded-slash, credential + /// rewrite flags, and allow-uninspected-credentials. #[arg(long = "add-endpoint")] add_endpoints: Vec, diff --git a/crates/openshell-cli/src/policy_update.rs b/crates/openshell-cli/src/policy_update.rs index 2907afaaed..6aac515e14 100644 --- a/crates/openshell-cli/src/policy_update.rs +++ b/crates/openshell-cli/src/policy_update.rs @@ -268,11 +268,14 @@ fn l7_target_to_proto(target: &L7RuleTarget) -> ProtoL7RuleTarget { } } -fn ensure_websocket_credential_rewrite_protocol( +/// Require the endpoint protocol segment to be one of `allowed` for `option`. +fn ensure_option_protocol( spec: &str, endpoint: &NetworkEndpoint, + option: &str, + allowed: &[&str], ) -> Result<()> { - if matches!(endpoint.protocol.as_str(), "rest" | "websocket") { + if allowed.contains(&endpoint.protocol.as_str()) { return Ok(()); } let protocol = if endpoint.protocol.is_empty() { @@ -280,25 +283,13 @@ fn ensure_websocket_credential_rewrite_protocol( } else { endpoint.protocol.as_str() }; + let allowed = allowed + .iter() + .map(|protocol| format!("'{protocol}'")) + .collect::>() + .join(" or "); Err(miette!( - "websocket-credential-rewrite endpoint option requires --add-endpoint protocol segment to be 'rest' or 'websocket'; got '{protocol}' in '{spec}'" - )) -} - -fn ensure_request_body_credential_rewrite_protocol( - spec: &str, - endpoint: &NetworkEndpoint, -) -> Result<()> { - if endpoint.protocol == "rest" { - return Ok(()); - } - let protocol = if endpoint.protocol.is_empty() { - "" - } else { - endpoint.protocol.as_str() - }; - Err(miette!( - "request-body-credential-rewrite endpoint option requires --add-endpoint protocol segment to be 'rest'; got '{protocol}' in '{spec}'" + "{option} endpoint option requires --add-endpoint protocol segment to be {allowed}; got '{protocol}' in '{spec}'" )) } @@ -504,12 +495,16 @@ fn apply_add_endpoint_options( "allow-uninspected-credentials" => { endpoint.allow_uninspected_credentials = true; } + "allow-encoded-slash" => { + ensure_option_protocol(spec, endpoint, option, &["rest", "websocket"])?; + endpoint.allow_encoded_slash = true; + } "websocket-credential-rewrite" => { - ensure_websocket_credential_rewrite_protocol(spec, endpoint)?; + ensure_option_protocol(spec, endpoint, option, &["rest", "websocket"])?; endpoint.websocket_credential_rewrite = true; } "request-body-credential-rewrite" => { - ensure_request_body_credential_rewrite_protocol(spec, endpoint)?; + ensure_option_protocol(spec, endpoint, option, &["rest"])?; endpoint.request_body_credential_rewrite = true; } _ if option.starts_with(ALLOWED_IP_OPTION_PREFIX) => { @@ -521,7 +516,7 @@ fn apply_add_endpoint_options( } _ => { return Err(miette!( - "--add-endpoint options segment supports only 'allow-uninspected-credentials', 'websocket-credential-rewrite', 'request-body-credential-rewrite', and 'allowed-ip='; got '{option}' in '{spec}'" + "--add-endpoint options segment supports only 'allow-uninspected-credentials', 'allow-encoded-slash', 'websocket-credential-rewrite', 'request-body-credential-rewrite', and 'allowed-ip='; got '{option}' in '{spec}'" )); } } @@ -831,6 +826,45 @@ mod tests { assert!(endpoint.request_body_credential_rewrite); } + #[test] + fn parse_add_endpoint_enables_allow_encoded_slash_on_rest_and_websocket() { + for protocol in ["rest", "websocket"] { + let plan = build_policy_update_plan( + &[format!( + "registry.npmjs.org:443:full:{protocol}:enforce:allow-encoded-slash" + )], + &[], + &[], + &[], + &[], + &[], + None, + ) + .expect("plan should build"); + + let PolicyMergeOp::AddRule { rule, .. } = &plan.preview_operations[0] else { + panic!("expected add-rule preview"); + }; + assert!(rule.endpoints[0].allow_encoded_slash, "{protocol}"); + } + } + + #[test] + fn allow_encoded_slash_rejects_endpoint_without_http_protocol() { + for spec in [ + "registry.npmjs.org:443::::allow-encoded-slash", + "registry.npmjs.org:443::tcp::allow-encoded-slash", + ] { + let error = + build_policy_update_plan(&[spec.to_string()], &[], &[], &[], &[], &[], None) + .expect_err("plan should fail"); + assert!( + error.to_string().contains("to be 'rest' or 'websocket'"), + "{error}" + ); + } + } + #[test] fn parse_add_endpoint_enables_allow_uninspected_credentials() { let plan = build_policy_update_plan( diff --git a/crates/openshell-cli/src/tls.rs b/crates/openshell-cli/src/tls.rs index 41381f9a40..fd138aae5b 100644 --- a/crates/openshell-cli/src/tls.rs +++ b/crates/openshell-cli/src/tls.rs @@ -5,6 +5,7 @@ use miette::{IntoDiagnostic, Result, WrapErr}; pub use openshell_core::replica_routing::{ owner_replica, retry_unrouted, routed_to, with_owner_hint, }; +use openshell_otel::EnvTraceContextInterceptor; use openshell_sdk::{AuthConfig, ClientConfig, ClientIdentity, OpenShellClient}; use rustls::{ RootCertStore, @@ -16,7 +17,32 @@ use std::path::PathBuf; use tonic::transport::Channel; /// Concrete gRPC client type used by all commands. -pub type GrpcClient = openshell_sdk::raw::AuthedGrpcClient; +pub type GrpcClient = openshell_sdk::raw::GrpcClient< + tonic::service::interceptor::InterceptedService, +>; + +/// Interceptor stack applied to gateway requests from clients built by +/// [`grpc_client`]. +/// +/// Runs authentication header injection first, then passive W3C trace-context +/// forwarding so a CI pipeline's `TRACEPARENT` extends into the gateway's +/// spans. tonic allows a single interceptor per channel, so the two concerns +/// are composed here. +#[derive(Clone)] +pub struct GatewayInterceptor { + auth: openshell_sdk::EdgeAuthInterceptor, + trace: EnvTraceContextInterceptor, +} + +impl tonic::service::Interceptor for GatewayInterceptor { + fn call( + &mut self, + request: tonic::Request<()>, + ) -> std::result::Result, tonic::Status> { + let request = self.auth.call(request)?; + self.trace.call(request) + } +} #[derive(Clone, Debug, Default)] pub struct TlsOptions { @@ -313,9 +339,10 @@ pub async fn build_channel(server: &str, tls: &TlsOptions) -> Result { /// Build the SDK client and return its raw gRPC escape hatch. /// -/// When `tls.edge_token` is set, the returned client is wrapped with an -/// interceptor that injects authentication headers on every request. -/// Otherwise, standard mTLS is used (interceptor is a no-op). +/// The returned client carries a [`GatewayInterceptor`] that injects +/// authentication headers (when a token is set) and passively forwards the +/// `TRACEPARENT`/`TRACESTATE` trace context from the environment on every +/// request. With no token and no trace context, the interceptor is a no-op. pub async fn grpc_client(server: &str, tls: &TlsOptions) -> Result { let config = sdk_client_config(server, tls)?; // Keep the SDK's transport state machine out of every command future. @@ -323,14 +350,71 @@ pub async fn grpc_client(server: &str, tls: &TlsOptions) -> Result { let client = Box::pin(OpenShellClient::connect(config)) .await .into_diagnostic()?; - Ok(client.raw_grpc()) + // tonic allows a single interceptor per channel: compose the SDK's auth + // interceptor (sharing its live bearer slot) with the CLI's passive + // trace-context forwarding. + let interceptor = GatewayInterceptor { + auth: client.auth_interceptor(), + trace: EnvTraceContextInterceptor::from_env(), + }; + Ok(openshell_sdk::raw::GrpcClient::with_interceptor( + client.channel(), + interceptor, + )) } #[cfg(test)] mod tests { - use super::{TlsOptions, sdk_client_config}; - use openshell_sdk::AuthConfig; + use super::{GatewayInterceptor, TlsOptions, sdk_client_config}; + use crate::TEST_ENV_LOCK; + use openshell_otel::EnvTraceContextInterceptor; + use openshell_sdk::{AuthConfig, EdgeAuthInterceptor}; use std::io::Write; + use tonic::service::Interceptor as _; + + #[test] + fn gateway_interceptor_composes_auth_and_trace_forwarding() { + let _guard = TEST_ENV_LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let traceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"; + temp_env::with_vars( + [ + ("TRACEPARENT", Some(traceparent)), + ("TRACESTATE", Some("vendor=value")), + ], + || { + let mut interceptor = GatewayInterceptor { + auth: EdgeAuthInterceptor::new(Some("test-token"), None).unwrap(), + trace: EnvTraceContextInterceptor::from_env(), + }; + + let request = interceptor.call(tonic::Request::new(())).unwrap(); + + assert_eq!( + request + .metadata() + .get("authorization") + .and_then(|value| value.to_str().ok()), + Some("Bearer test-token") + ); + assert_eq!( + request + .metadata() + .get("traceparent") + .and_then(|value| value.to_str().ok()), + Some(traceparent) + ); + assert_eq!( + request + .metadata() + .get("tracestate") + .and_then(|value| value.to_str().ok()), + Some("vendor=value") + ); + }, + ); + } #[test] fn sdk_config_carries_cli_mtls_materials() { diff --git a/crates/openshell-core/README.md b/crates/openshell-core/README.md index d1ce03d653..b3d9a50349 100644 --- a/crates/openshell-core/README.md +++ b/crates/openshell-core/README.md @@ -58,6 +58,25 @@ router agree on provider defaults. Profiles define: Do not duplicate provider-specific inference behavior in callers. Add shared behavior here, then consume it from the gateway, sandbox, and router. +## Static Credential Resolution + +Static credential bindings retain endpoint authorization. The gateway includes +static `ProviderProfileCredential` auth metadata in the existing runtime +credential map under `static-env:`. Provider state extracts those entries +before storing dynamic token grants and compiles placement locally alongside the +matching static binding. Missing metadata defaults to the `Authorization` header; +malformed reserved metadata or an invalid selected placement fails closed +atomically. The same header default applies to request rewriting with standalone +resolver snapshots. +The credential's `auth_style` selects the placement; unused fields for other +styles are ignored and do not authorize additional locations. + +Request rewriting supplies the actual header name, query key, or path slot. +Header authorization is independent of authentication scheme, including Basic. +Explicit body/WebSocket rewrite policies and trusted signing retain their +existing permissions. Existing placeholder and stable-handle derivation remain +unchanged. Protobuf message shapes and service interfaces remain unchanged. + ## Middleware Contracts Built-in supervisor middleware identifiers, host-selector matching, and pure diff --git a/crates/openshell-core/src/provider_credentials.rs b/crates/openshell-core/src/provider_credentials.rs index d71f34b7c6..ab9bd26195 100644 --- a/crates/openshell-core/src/provider_credentials.rs +++ b/crates/openshell-core/src/provider_credentials.rs @@ -3,7 +3,7 @@ //! Runtime provider credential snapshots. -use crate::secrets::SecretResolver; +use crate::secrets::{CredentialPlacement, SecretResolver}; use crate::{ endpoint_path::EndpointPathPattern, host_pattern::HostPattern, @@ -67,6 +67,7 @@ struct StaticCredentialIdentityEpoch { #[derive(Debug, Clone)] struct CompiledStaticCredentialBinding { + placement: CredentialPlacement, endpoints: Vec, credential_identity: String, workload_credential_handle: String, @@ -97,6 +98,29 @@ impl fmt::Display for StaticCredentialBindingError { impl std::error::Error for StaticCredentialBindingError {} +/// Reserved entries in the existing credential map carry static placement only. +pub const STATIC_CREDENTIAL_METADATA_PREFIX: &str = "static-env:"; + +const CREDENTIAL_PATH_SLOT: &str = "{credential}"; + +fn split_static_credential_metadata( + credentials: HashMap, +) -> ( + HashMap, + HashMap, +) { + let mut metadata = HashMap::new(); + let mut grants = HashMap::new(); + for (key, credential) in credentials { + if let Some(env_key) = key.strip_prefix(STATIC_CREDENTIAL_METADATA_PREFIX) { + metadata.insert(env_key.to_string(), credential); + } else { + grants.insert(key, credential); + } + } + (metadata, grants) +} + impl ProviderCredentialState { pub fn from_environment( revision: u64, @@ -104,6 +128,7 @@ impl ProviderCredentialState { credential_expires_at_ms: HashMap, dynamic_credentials: HashMap, ) -> Self { + let (_, dynamic_credentials) = split_static_credential_metadata(dynamic_credentials); let (child_env, generation_resolver, current_resolver) = SecretResolver::from_provider_env_for_current_revision( env, @@ -173,11 +198,14 @@ impl ProviderCredentialState { non_secret_environment_keys: Vec, stable_placeholder_environment_keys: Vec, ) -> Result { + let (placement_metadata, dynamic_credentials) = + split_static_credential_metadata(dynamic_credentials); let static_credential_bindings = compile_static_credential_bindings( &env, static_credential_bindings, &non_secret_environment_keys, &stable_placeholder_environment_keys, + &placement_metadata, )?; let stable_handles = static_credential_stable_handles(&static_credential_bindings); let (child_env, generation_resolver, current_resolver) = @@ -304,11 +332,21 @@ impl ProviderCredentialState { } pub fn resolver(&self) -> Option> { - self.inner + let inner = self + .inner .read() - .expect("provider credential state poisoned") - .combined_resolver - .clone() + .expect("provider credential state poisoned"); + inner.combined_resolver.as_ref().map(|resolver| { + Arc::new( + resolver.as_ref().clone().with_placements( + inner + .static_credential_bindings + .iter() + .map(|(key, binding)| (key.clone(), binding.placement.clone())) + .collect(), + ), + ) + }) } /// Resolve provider placeholders only for credentials bound to this @@ -414,11 +452,23 @@ impl ProviderCredentialState { }) .map(|(key, epoch)| (key.clone(), epoch.revisions.clone())) .collect(); - Arc::new(resolver.scoped_to_env_keys( - &inner.known_static_credential_keys, - &allowed, - revision_fallback_allowed_revisions, - )) + Arc::new( + resolver + .as_ref() + .clone() + .with_placements( + inner + .static_credential_bindings + .iter() + .map(|(key, binding)| (key.clone(), binding.placement.clone())) + .collect(), + ) + .scoped_to_env_keys( + &inner.known_static_credential_keys, + &allowed, + revision_fallback_allowed_revisions, + ), + ) }); (resolver, classifier, revision) } @@ -694,6 +744,7 @@ impl ProviderCredentialState { credential_expires_at_ms: HashMap, dynamic_credentials: HashMap, ) -> usize { + let (_, dynamic_credentials) = split_static_credential_metadata(dynamic_credentials); let (mut child_env, generation_resolver, current_resolver) = SecretResolver::from_provider_env_for_current_revision( env, @@ -774,11 +825,14 @@ impl ProviderCredentialState { non_secret_environment_keys: Vec, stable_placeholder_environment_keys: Vec, ) -> Result { + let (placement_metadata, dynamic_credentials) = + split_static_credential_metadata(dynamic_credentials); let static_credential_bindings = match compile_static_credential_bindings( &env, static_credential_bindings, &non_secret_environment_keys, &stable_placeholder_environment_keys, + &placement_metadata, ) { Ok(bindings) => bindings, Err(error) => { @@ -939,6 +993,7 @@ impl ProviderCredentialState { .expect("provider credential state poisoned"); let dynamic_credentials = dynamic_credentials.unwrap_or_else(|| inner.current.dynamic_credentials.clone()); + let (_, dynamic_credentials) = split_static_credential_metadata(dynamic_credentials); inner.current = Arc::new(ProviderCredentialSnapshot { installation_id: uuid::Uuid::new_v4().to_string(), revision, @@ -959,7 +1014,22 @@ fn compile_static_credential_bindings( bindings: HashMap, non_secret_environment_keys: &[String], stable_placeholder_environment_keys: &[String], + placement_metadata: &HashMap, ) -> Result, StaticCredentialBindingError> { + for (key, metadata) in placement_metadata { + if !env.contains_key(key) + || !bindings.contains_key(key) + || metadata.env_vars.len() != 1 + || metadata.env_vars[0] != *key + || metadata.token_grant.is_some() + || metadata.refresh.is_some() + || !metadata.token_grant_owners.is_empty() + { + return Err(binding_error( + "static credential placement metadata does not match delivered credentials", + )); + } + } let non_secret_keys = non_secret_environment_keys .iter() .cloned() @@ -1056,6 +1126,10 @@ fn compile_static_credential_bindings( bindings .into_iter() .map(|(key, binding)| { + let placement = placement_metadata.get(&key).map_or_else( + || Ok(CredentialPlacement::Header("authorization".into())), + compile_credential_placement, + )?; let endpoints = binding .endpoints .into_iter() @@ -1065,6 +1139,7 @@ fn compile_static_credential_bindings( key, CompiledStaticCredentialBinding { endpoints, + placement, credential_identity: binding.credential_identity, workload_credential_handle: binding.workload_credential_handle, }, @@ -1073,6 +1148,52 @@ fn compile_static_credential_bindings( .collect() } +fn compile_credential_placement( + metadata: &crate::proto::ProviderProfileCredential, +) -> Result { + let style = metadata.auth_style.trim().to_ascii_lowercase(); + // Authored profiles may retain fields for other auth styles. Only the + // selected style grants a location; unused fields grant no authority. + let placement = match style.as_str() { + "basic" => CredentialPlacement::Header("authorization".into()), + "header" | "bearer" + if !metadata.header_name.trim().is_empty() + && metadata + .header_name + .trim() + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&b)) => + { + CredentialPlacement::Header(metadata.header_name.trim().to_ascii_lowercase()) + } + "query" + if !metadata.query_param.trim().is_empty() + && !metadata + .query_param + .chars() + .any(|c| c.is_control() || "&=#?".contains(c)) => + { + CredentialPlacement::QueryParam(metadata.query_param.trim().to_string()) + } + "path" + if metadata.path_template.starts_with('/') + && metadata.path_template.matches(CREDENTIAL_PATH_SLOT).count() == 1 + && !metadata + .path_template + .chars() + .any(|c| c.is_control() || "?#".contains(c)) => + { + CredentialPlacement::PathTemplate(metadata.path_template.clone()) + } + _ => { + return Err(binding_error( + "static credential placement metadata has an invalid request location", + )); + } + }; + Ok(placement) +} + fn compile_static_credential_endpoint( endpoint: StaticCredentialEndpointBinding, ) -> Result { @@ -1346,6 +1467,10 @@ mod tests { ); } + fn authorization_location() -> crate::secrets::RequestCredentialLocation { + crate::secrets::RequestCredentialLocation::Header("Authorization".into()) + } + fn binding(host: &str, port: u32, path: &str) -> StaticCredentialBinding { StaticCredentialBinding { endpoints: vec![StaticCredentialEndpointBinding { @@ -1365,15 +1490,336 @@ mod tests { binding } + fn placement_metadata(style: &str) -> HashMap { + HashMap::from([( + format!("{STATIC_CREDENTIAL_METADATA_PREFIX}API_KEY"), + crate::proto::ProviderProfileCredential { + name: "api_key".into(), + env_vars: vec!["API_KEY".into()], + auth_style: style.into(), + header_name: if matches!(style, "header" | "bearer" | "basic") { + "Authorization".into() + } else { + String::new() + }, + query_param: if style == "query" { + "api_key".into() + } else { + String::new() + }, + path_template: if style == "path" { + "/v1/{credential}/events".into() + } else { + String::new() + }, + ..Default::default() + }, + )]) + } + + #[test] + fn metadata_free_static_binding_defaults_to_authorization_across_rotation() { + let state = ProviderCredentialState::from_bound_environment( + 7, + HashMap::from([("API_KEY".into(), "initial".into())]), + HashMap::new(), + HashMap::new(), + HashMap::from([("API_KEY".into(), binding("api.example.com", 443, "/**"))]), + vec![], + ) + .unwrap(); + let reference = state.snapshot().child_env["API_KEY"].clone(); + for revision in [7, 8] { + if revision == 8 { + state + .install_bound_environment( + revision, + HashMap::from([("API_KEY".into(), "rotated".into())]), + HashMap::new(), + HashMap::new(), + HashMap::from([("API_KEY".into(), binding("api.example.com", 443, "/**"))]), + vec![], + ) + .unwrap(); + } + let resolver = state + .resolver_for_endpoint("api.example.com", 443, "/search") + .unwrap(); + assert!(resolver.resolve_placeholder(&reference).is_none()); + for token in [ + reference.clone(), + format!("vendor-OPENSHELL-RESOLVE-ENV-v{revision}_API_KEY"), + jwt_alias(&reference), + ] { + assert!( + resolver + .rewrite_header_value_for_name("Authorization", &format!("Bearer {token}")) + .unwrap() + .is_some() + ); + assert!( + resolver + .rewrite_header_value_for_name("X-Reflect", &token) + .unwrap_err() + .is_placement_mismatch() + ); + for target in [format!("/search?token={token}"), format!("/api/{token}")] { + assert!(crate::secrets::rewrite_target_for_eval(&target, &resolver).is_err()); + } + } + let current = &state.snapshot().child_env["API_KEY"]; + assert_eq!( + resolver + .rewrite_header_value_for_name("Authorization", current) + .unwrap(), + Some(if revision == 7 { "initial" } else { "rotated" }.into()) + ); + } + } + + #[test] + fn explicit_custom_header_overrides_authorization_default() { + let mut metadata = placement_metadata("header"); + metadata.values_mut().next().unwrap().header_name = "x-api-key".into(); + let state = ProviderCredentialState::from_bound_environment( + 7, + HashMap::from([("API_KEY".into(), "synthetic".into())]), + HashMap::new(), + metadata, + HashMap::from([("API_KEY".into(), binding("api.example.com", 443, "/**"))]), + vec![], + ) + .unwrap(); + let reference = state.snapshot().child_env["API_KEY"].clone(); + let resolver = state + .resolver_for_endpoint("api.example.com", 443, "/search") + .unwrap(); + assert_eq!( + resolver + .rewrite_header_value_for_name("X-Api-Key", &reference) + .unwrap(), + Some("synthetic".into()) + ); + assert!( + resolver + .rewrite_header_value_for_name("Authorization", &reference) + .unwrap_err() + .is_placement_mismatch() + ); + } + + #[test] + fn static_metadata_selects_one_location_and_preserves_other_providers() { + for style in ["bearer", "header", "basic", "query", "path"] { + let env = HashMap::from([ + ("API_KEY".into(), "synthetic".into()), + ("OTHER_KEY".into(), "other-secret".into()), + ]); + let mut other_binding = binding("other.example.com", 443, "/**"); + other_binding.credential_identity = "provider-b:OTHER_KEY".into(); + let bindings = HashMap::from([ + ("API_KEY".into(), binding("api.example.com", 443, "/**")), + ("OTHER_KEY".into(), other_binding), + ]); + let state = ProviderCredentialState::from_bound_environment( + 7, + env.clone(), + HashMap::new(), + HashMap::new(), + bindings.clone(), + vec![], + ) + .unwrap(); + // Older documented profiles populated all three placement fields. + // Only auth_style's selected field may grant request authority. + let mut metadata = placement_metadata(style); + let declaration = metadata.values_mut().next().unwrap(); + declaration.header_name = if style == "basic" { + "x-unused-for-basic" + } else { + "Authorization" + } + .into(); + declaration.query_param = "api_key".into(); + declaration.path_template = "/v1/{credential}/events".into(); + assert_eq!( + state + .install_bound_environment(8, env, HashMap::new(), metadata, bindings, vec![]) + .unwrap(), + 2 + ); + assert!(state.snapshot().dynamic_credentials.is_empty()); + assert_eq!( + state + .resolver_for_endpoint("other.example.com", 443, "/") + .unwrap() + .rewrite_header_value_for_name( + "Authorization", + &state.snapshot().child_env["OTHER_KEY"], + ) + .unwrap(), + Some("other-secret".into()) + ); + let reference = state.snapshot().child_env["API_KEY"].clone(); + let resolver = state + .resolver_for_endpoint("api.example.com", 443, "/v1/chat") + .unwrap(); + assert!( + resolver + .rewrite_header_value_for_name("X-Reflect", &reference) + .is_err() + ); + let header = resolver.rewrite_header_value_for_name("Authorization", &reference); + let query = crate::secrets::rewrite_target_for_eval( + &format!("/v1/chat?api_key={reference}"), + &resolver, + ); + let path = crate::secrets::rewrite_target_for_eval( + &format!("/v1/{reference}/events"), + &resolver, + ); + assert_eq!( + header.is_ok(), + matches!(style, "bearer" | "header" | "basic") + ); + assert_eq!(query.is_ok(), style == "query"); + assert_eq!(path.is_ok(), style == "path"); + } + } + + #[test] + fn malformed_static_metadata_revokes_static_material_and_preserves_real_grants() { + for malformed in 0..11 { + let state = ProviderCredentialState::from_bound_environment( + 7, + HashMap::from([("API_KEY".into(), "synthetic".into())]), + HashMap::new(), + HashMap::new(), + HashMap::from([("API_KEY".into(), binding("api.example.com", 443, "/**"))]), + vec![], + ) + .unwrap(); + let mut metadata = placement_metadata("header"); + let key = format!("{STATIC_CREDENTIAL_METADATA_PREFIX}API_KEY"); + let value = metadata.get_mut(&key).unwrap(); + match malformed { + 0 => value.env_vars = vec!["OTHER".into()], + 1 => { + value.token_grant = Some(crate::proto::ProviderCredentialTokenGrant::default()); + } + 2 => value.refresh = Some(crate::proto::ProviderCredentialRefresh::default()), + 3 => value.token_grant_owners = vec!["owner".into()], + 4 => value.auth_style = "unsupported".into(), + 5 => { + let value = metadata.remove(&key).unwrap(); + metadata.insert(format!("{STATIC_CREDENTIAL_METADATA_PREFIX}OTHER"), value); + } + 6 => value.header_name.clear(), + 7 => value.header_name = "Bad Header".into(), + 8 => { + value.auth_style = "path".into(); + value.header_name.clear(); + value.path_template = "/no-slot".into(); + } + 9 => { + value.auth_style = "query".into(); + value.query_param = "api?key".into(); + } + 10 => value.auth_style.clear(), + _ => unreachable!(), + } + metadata.insert( + "real-grant".into(), + crate::proto::ProviderProfileCredential { + token_grant: Some(crate::proto::ProviderCredentialTokenGrant::default()), + ..Default::default() + }, + ); + assert!( + state + .install_bound_environment( + 8, + HashMap::from([("API_KEY".into(), "replacement".into())]), + HashMap::new(), + metadata, + HashMap::from([("API_KEY".into(), binding("api.example.com", 443, "/**"))]), + vec![], + ) + .is_err() + ); + assert!(state.snapshot().child_env.is_empty()); + assert_eq!(state.snapshot().dynamic_credentials.len(), 1); + assert!( + state + .snapshot() + .dynamic_credentials + .contains_key("real-grant") + ); + } + } + + #[test] + fn stable_references_survive_metadata_arrival_and_use_the_current_location() { + let handle = "a".repeat(64); + let bindings = HashMap::from([( + "API_KEY".into(), + stable_binding("api.example.com", 443, "/**", &handle), + )]); + let state = ProviderCredentialState::from_bound_environment_with_stable_placeholders( + 7, + HashMap::from([("API_KEY".into(), "synthetic".into())]), + HashMap::new(), + HashMap::new(), + bindings.clone(), + vec![], + vec!["API_KEY".into()], + ) + .unwrap(); + let reference = state.snapshot().child_env["API_KEY"].clone(); + for style in [None, Some("header"), Some("query"), None] { + state + .install_bound_environment_with_stable_placeholders( + 8, + HashMap::from([("API_KEY".into(), "synthetic".into())]), + HashMap::new(), + style.map_or_else(HashMap::new, placement_metadata), + bindings.clone(), + vec![], + vec!["API_KEY".into()], + ) + .unwrap(); + assert_eq!(state.snapshot().child_env["API_KEY"], reference); + let resolver = state + .resolver_for_endpoint("api.example.com", 443, "/v1/chat") + .unwrap(); + let query = crate::secrets::rewrite_target_for_eval( + &format!("/v1/chat?api_key={reference}"), + &resolver, + ); + assert_eq!(query.is_ok(), style == Some("query")); + assert_eq!( + resolver + .rewrite_header_value_for_name("Authorization", &reference) + .is_ok(), + style != Some("query") + ); + } + } + fn assert_binding_validation_error( env: HashMap, bindings: HashMap, non_secret_environment_keys: Vec, expected: &str, ) { - let error = - compile_static_credential_bindings(&env, bindings, &non_secret_environment_keys, &[]) - .expect_err("malformed provider metadata must fail validation"); + let error = compile_static_credential_bindings( + &env, + bindings, + &non_secret_environment_keys, + &[], + &HashMap::new(), + ) + .expect_err("malformed provider metadata must fail validation"); assert_eq!(error.to_string(), expected); } @@ -1472,7 +1918,12 @@ mod tests { let allowed = state .resolver_for_endpoint("api.example.com", 443, "/v1/messages?stream=true") .expect("resolver"); - assert_eq!(allowed.resolve_placeholder(placeholder), Some("secret")); + assert_eq!( + allowed + .with_request_location(authorization_location()) + .resolve_placeholder(placeholder), + Some("secret") + ); for (host, port, path) in [ ("example.com", 443, "/v1/messages"), @@ -1483,7 +1934,7 @@ mod tests { .resolver_for_endpoint(host, port, path) .expect("resolver"); let error = denied - .rewrite_header_value(placeholder) + .rewrite_header_value_for_name("Authorization", placeholder) .expect_err("endpoint mismatch must fail closed"); assert!(error.is_endpoint_mismatch(), "{host}:{port}{path}"); } @@ -1522,16 +1973,22 @@ mod tests { .resolver_for_endpoint("tools.example.com", 443, "/mcp/local") .expect("endpoint resolver"); assert_eq!( - resolver.resolve_placeholder(&stable_placeholder), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder(&stable_placeholder), Some("old") ); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:ORDINARY_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:ORDINARY_TOKEN"), None, "ordinary endpoint-bound credentials remain revision scoped" ); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v1_ORDINARY_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v1_ORDINARY_TOKEN"), Some("ordinary") ); @@ -1559,16 +2016,22 @@ mod tests { .resolver_for_endpoint("tools.example.com", 443, "/mcp/local") .expect("refreshed endpoint resolver"); assert_eq!( - refreshed.resolve_placeholder(&stable_placeholder), + refreshed + .with_request_location(authorization_location()) + .resolve_placeholder(&stable_placeholder), Some("new") ); assert_eq!( - refreshed.resolve_placeholder("openshell:resolve:env:EXTERNAL_ASSERTION"), + refreshed + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:EXTERNAL_ASSERTION"), None, "an identityless current-name alias must remain unavailable" ); assert_eq!( - refreshed.resolve_placeholder("openshell:resolve:env:ORDINARY_TOKEN"), + refreshed + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:ORDINARY_TOKEN"), None ); @@ -1591,12 +2054,16 @@ mod tests { .resolver_for_endpoint("tools.example.com", 443, "/mcp/local") .expect("replacement endpoint resolver"); assert_eq!( - replacement.resolve_placeholder(&stable_placeholder), + replacement + .with_request_location(authorization_location()) + .resolve_placeholder(&stable_placeholder), None, "a placeholder issued for an earlier provider identity must be revoked" ); assert_eq!( - replacement.resolve_placeholder(&state.snapshot().child_env["EXTERNAL_ASSERTION"]), + replacement + .with_request_location(authorization_location()) + .resolve_placeholder(&state.snapshot().child_env["EXTERNAL_ASSERTION"]), Some("replacement") ); } @@ -1650,16 +2117,20 @@ mod tests { .resolver_for_endpoint("api.example.com", 443, "/v1/chat/completions") .expect("authorized endpoint resolver"); assert!( - resolver.resolve_placeholder(&original) + resolver + .with_request_location(authorization_location()) + .resolve_placeholder(&original) == Some(format!("synthetic-{revision}").as_str()) ); assert!( resolver + .with_request_location(authorization_location()) .resolve_placeholder("openshell:resolve:env:API_KEY") .is_none() ); assert!( resolver + .with_request_location(authorization_location()) .resolve_placeholder("openshell:resolve:env:v1_API_KEY") .is_none() ); @@ -1681,6 +2152,7 @@ mod tests { reconstructed .resolver_for_endpoint("api.example.com", 443, "/v1/chat/completions") .expect("reconstructed resolver") + .with_request_location(authorization_location()) .resolve_placeholder(&original) == Some("synthetic-13") ); @@ -1692,7 +2164,11 @@ mod tests { let resolver = reconstructed .resolver_for_endpoint(host, port, path) .expect("endpoint-scoped resolver"); - assert!(resolver.rewrite_header_value(&original).is_err()); + assert!( + resolver + .rewrite_header_value_for_name("Authorization", &original) + .is_err() + ); } // Current-value expiry must not fall back to a previously valid value. @@ -1710,7 +2186,12 @@ mod tests { let resolver = reconstructed .resolver_for_endpoint("api.example.com", 443, "/v1/chat/completions") .expect("expired resolver"); - assert!(resolver.resolve_placeholder(&original).is_none()); + assert!( + resolver + .with_request_location(authorization_location()) + .resolve_placeholder(&original) + .is_none() + ); // An activated detach removes the current binding and all authority for // a still-running process, even when it retains its original reference. @@ -1729,7 +2210,10 @@ mod tests { assert!( state .resolver_for_endpoint("api.example.com", 443, "/v1/chat/completions") - .is_none_or(|resolver| resolver.resolve_placeholder(&original).is_none()) + .is_none_or(|resolver| resolver + .with_request_location(authorization_location()) + .resolve_placeholder(&original) + .is_none()) ); } @@ -1856,11 +2340,15 @@ mod tests { .resolver_for_endpoint("a.example.com", 443, "/a/check") .expect("endpoint A resolver"); assert_eq!( - resolver_a.resolve_placeholder("openshell:resolve:env:v7_KEY_A"), + resolver_a + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v7_KEY_A"), Some("secret-a") ); assert_eq!( - resolver_a.resolve_placeholder("openshell:resolve:env:v7_KEY_B"), + resolver_a + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v7_KEY_B"), None, "credential B must not resolve at endpoint A" ); @@ -1869,11 +2357,15 @@ mod tests { .resolver_for_endpoint("b.example.com", 443, "/b/check") .expect("endpoint B resolver"); assert_eq!( - resolver_b.resolve_placeholder("openshell:resolve:env:v7_KEY_B"), + resolver_b + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v7_KEY_B"), Some("secret-b") ); assert_eq!( - resolver_b.resolve_placeholder("openshell:resolve:env:v7_KEY_A"), + resolver_b + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v7_KEY_A"), None, "credential A must not resolve at endpoint B" ); @@ -1912,7 +2404,9 @@ mod tests { .resolver_for_endpoint("new.example.com", 443, "/v2/messages") .expect("replacement endpoint resolver"); assert_eq!( - replacement.resolve_placeholder("openshell:resolve:env:v2_API_KEY"), + replacement + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v2_API_KEY"), Some("new") ); @@ -1920,7 +2414,7 @@ mod tests { .resolver_for_endpoint("old.example.com", 443, "/v1/messages") .expect("restricted resolver"); let error = removed - .rewrite_header_value("openshell:resolve:env:v2_API_KEY") + .rewrite_header_value_for_name("Authorization", "openshell:resolve:env:v2_API_KEY") .expect_err("replaced endpoint binding must no longer authorize the credential"); assert!(error.is_endpoint_mismatch()); } @@ -1948,7 +2442,12 @@ mod tests { &format!("/bot{placeholder}/sendMessage?stream=true"), ) .expect("syntax-redacted path should select the binding"); - assert_eq!(resolver.resolve_placeholder(placeholder), Some("secret")); + assert_eq!( + resolver + .with_request_location(authorization_location()) + .resolve_placeholder(placeholder), + Some("secret") + ); } #[test] @@ -1974,7 +2473,9 @@ mod tests { ) .expect("syntax-redacted alias path should select the binding"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v7_API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v7_API_KEY"), Some("secret") ); } @@ -2021,7 +2522,9 @@ mod tests { .resolver_for_endpoint("unrelated.example", 1234, "/") .expect("resolver"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v3_GCP_PROJECT_ID"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v3_GCP_PROJECT_ID"), Some("project") ); } @@ -2076,7 +2579,9 @@ mod tests { .resolver_for_endpoint("api.example.com", 443, "/v1") .expect("resolver"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v1_API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v1_API_KEY"), Some("recovered"), "a metadata failure must not permanently strand placeholders from the same provider identity" ); @@ -2128,7 +2633,9 @@ mod tests { .resolver_for_endpoint("api.example.com", 443, "/v1") .expect("resolver"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v1_API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v1_API_KEY"), Some("new"), "the running process placeholder should recover against the current same-identity secret" ); @@ -2167,11 +2674,15 @@ mod tests { .resolver_for_endpoint("api.example.com", 443, "/v1") .expect("resolver"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v1_API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v1_API_KEY"), Some("old") ); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v2_API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v2_API_KEY"), Some("new") ); } @@ -2202,6 +2713,7 @@ mod tests { state .resolver_for_endpoint("api.example.com", 443, "/v1") .expect("resolver") + .with_request_location(authorization_location()) .resolve_placeholder(&workload_placeholder), None, "the expired access token must fail closed" @@ -2210,7 +2722,10 @@ mod tests { state .resolver_for_endpoint("api.example.com", 443, "/v1") .expect("resolver") - .rewrite_header_value(&format!("Bearer {}", jwt_alias(&workload_placeholder))) + .rewrite_header_value_for_name( + "Authorization", + &format!("Bearer {}", jwt_alias(&workload_placeholder)) + ) .is_err(), "wrapping a placeholder must not bypass access-token expiry" ); @@ -2231,17 +2746,24 @@ mod tests { .resolver_for_endpoint("api.example.com", 443, "/v1") .expect("resolver"); assert_eq!( - resolver.resolve_placeholder(&workload_placeholder), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder(&workload_placeholder), Some("current") ); assert_eq!( resolver - .rewrite_header_value(&format!("Bearer {}", jwt_alias(&workload_placeholder))) + .rewrite_header_value_for_name( + "Authorization", + &format!("Bearer {}", jwt_alias(&workload_placeholder)) + ) .expect("refresh updates the JWT alias"), Some("Bearer current".to_string()) ); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v2_API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v2_API_KEY"), None, "refresh-managed credentials must not expose revision aliases" ); @@ -2283,6 +2805,7 @@ mod tests { state .resolver_for_endpoint("api.example.com", 443, "/v1") .expect("resolver") + .with_request_location(authorization_location()) .resolve_placeholder(&workload_placeholder), Some(token.as_str()) ); @@ -2315,6 +2838,7 @@ mod tests { reconstructed .resolver_for_endpoint("api.example.com", 443, "/v1") .expect("resolver") + .with_request_location(authorization_location()) .resolve_placeholder(&workload_placeholder), Some("token-14") ); @@ -2322,7 +2846,10 @@ mod tests { reconstructed .resolver_for_endpoint("api.example.com", 443, "/v1") .expect("resolver") - .rewrite_header_value(&format!("Bearer {}", jwt_alias(&workload_placeholder))) + .rewrite_header_value_for_name( + "Authorization", + &format!("Bearer {}", jwt_alias(&workload_placeholder)) + ) .expect("the JWT alias survives supervisor reconstruction"), Some("Bearer token-14".to_string()) ); @@ -2363,10 +2890,18 @@ mod tests { let current = state .resolver_for_endpoint("new.example.com", 443, "/v2/messages") .expect("current resolver"); - assert_eq!(current.resolve_placeholder(&old_placeholder), None); + assert_eq!( + current + .with_request_location(authorization_location()) + .resolve_placeholder(&old_placeholder), + None + ); assert!( current - .rewrite_header_value(&format!("Bearer {}", jwt_alias(&old_placeholder))) + .rewrite_header_value_for_name( + "Authorization", + &format!("Bearer {}", jwt_alias(&old_placeholder)) + ) .is_err(), "a JWT alias must not carry a revoked identity into the new epoch" ); @@ -2377,27 +2912,32 @@ mod tests { ] { assert!( current - .rewrite_header_value(&format!("Bearer {alias}")) + .rewrite_header_value_for_name("Authorization", &format!("Bearer {alias}")) .is_err(), "identityless and malformed JWT aliases must fail closed" ); } assert_eq!( - current.resolve_placeholder(&state.snapshot().child_env["API_KEY"]), + current + .with_request_location(authorization_location()) + .resolve_placeholder(&state.snapshot().child_env["API_KEY"]), Some("new") ); let wrong_endpoint = state .resolver_for_endpoint("old.example.com", 443, "/v1/messages") .expect("endpoint-scoped resolver"); let error = wrong_endpoint - .rewrite_header_value(&state.snapshot().child_env["API_KEY"]) + .rewrite_header_value_for_name("Authorization", &state.snapshot().child_env["API_KEY"]) .expect_err("new handle must not resolve at the old endpoint"); assert!(error.is_endpoint_mismatch()); let error = wrong_endpoint - .rewrite_header_value(&format!( - "Bearer {}", - jwt_alias(&state.snapshot().child_env["API_KEY"]) - )) + .rewrite_header_value_for_name( + "Authorization", + &format!( + "Bearer {}", + jwt_alias(&state.snapshot().child_env["API_KEY"]) + ), + ) .expect_err("JWT wrapping preserves endpoint authorization"); assert!(error.is_endpoint_mismatch()); } @@ -2442,7 +2982,9 @@ mod tests { .resolver_for_endpoint("api.example.com", 443, "/v2/inference") .expect("resolver"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v50_API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v50_API_KEY"), Some("secret-6"), "an aged-out placeholder may use the current secret across revisions in both numeric directions while its provider identity is unchanged" ); @@ -2484,26 +3026,35 @@ mod tests { .resolver_for_endpoint("b.example.com", 443, "/v1") .expect("resolver"); assert_eq!( - resolver.resolve_placeholder(&format!("openshell:resolve:env:v{}_API_KEY", u64::MAX)), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder(&format!("openshell:resolve:env:v{}_API_KEY", u64::MAX)), None, "an opaque revision from another provider identity must fail closed even when it is numerically greater" ); assert!( resolver - .rewrite_header_value(&format!( - "Bearer {}", - jwt_alias(&format!("openshell:resolve:env:v{}_API_KEY", u64::MAX)) - )) + .rewrite_header_value_for_name( + "Authorization", + &format!( + "Bearer {}", + jwt_alias(&format!("openshell:resolve:env:v{}_API_KEY", u64::MAX)) + ) + ) .is_err(), "a JWT alias must not resolve a replacement provider's credential" ); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:API_KEY"), None, "an identityless canonical placeholder must not resolve a replacement provider" ); assert_eq!( - resolver.resolve_placeholder("vendor-OPENSHELL-RESOLVE-ENV-API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("vendor-OPENSHELL-RESOLVE-ENV-API_KEY"), None, "an identityless provider alias must not resolve a replacement provider" ); @@ -2546,17 +3097,23 @@ mod tests { .resolver_for_endpoint("b.example.com", 443, "/v1") .expect("resolver"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v1_API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v1_API_KEY"), None, "a placeholder issued before detach must not resolve to a replacement provider" ); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:API_KEY"), None, "an identityless canonical placeholder must not cross a failed refresh into a replacement identity" ); assert_eq!( - resolver.resolve_placeholder("vendor-OPENSHELL-RESOLVE-ENV-API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("vendor-OPENSHELL-RESOLVE-ENV-API_KEY"), None, "an identityless provider alias must not cross a failed refresh into a replacement identity" ); @@ -2600,12 +3157,16 @@ mod tests { .resolver_for_endpoint("a.example.com", 443, "/v1") .expect("resolver"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v1_API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v1_API_KEY"), None, "a successful empty environment represents detach and must invalidate old membership" ); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v4_API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v4_API_KEY"), Some("reattached") ); } @@ -2638,19 +3199,27 @@ mod tests { let resolver = state.resolver().expect("resolver"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v10_GITHUB_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v10_GITHUB_TOKEN"), Some("old") ); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v11_GITHUB_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v11_GITHUB_TOKEN"), Some("new") ); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:GITHUB_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:GITHUB_TOKEN"), Some("new") ); assert_eq!( - resolver.resolve_placeholder("provider-OPENSHELL-RESOLVE-ENV-GITHUB_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("provider-OPENSHELL-RESOLVE-ENV-GITHUB_TOKEN"), Some("new") ); } @@ -2669,15 +3238,21 @@ mod tests { assert!(state.snapshot().child_env.is_empty()); let resolver = state.resolver().expect("old resolver retained"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v10_GITHUB_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v10_GITHUB_TOKEN"), Some("old") ); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:GITHUB_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:GITHUB_TOKEN"), None ); assert_eq!( - resolver.resolve_placeholder("provider-OPENSHELL-RESOLVE-ENV-GITHUB_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("provider-OPENSHELL-RESOLVE-ENV-GITHUB_TOKEN"), None ); } @@ -2707,11 +3282,15 @@ mod tests { let resolver = state.resolver().expect("resolver"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v10_GITHUB_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v10_GITHUB_TOKEN"), None ); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v11_GITHUB_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v11_GITHUB_TOKEN"), Some("new") ); } @@ -3075,11 +3654,15 @@ mod tests { .resolver_for_endpoint("api.example.com", 443, "/") .unwrap(); assert_eq!( - resolver.resolve_placeholder(&old_placeholder), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder(&old_placeholder), Some("first-secret") ); assert_eq!( - resolver.resolve_placeholder(&live.snapshot().child_env["API_KEY"]), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder(&live.snapshot().child_env["API_KEY"]), Some("second-secret"), ); } @@ -3138,7 +3721,12 @@ mod tests { let resolver = observer .resolver_for_endpoint("api.example.com", 443, "/") .unwrap(); - assert!(resolver.resolve_placeholder(&old_placeholder).is_none()); + assert!( + resolver + .with_request_location(authorization_location()) + .resolve_placeholder(&old_placeholder) + .is_none() + ); } #[test] @@ -3303,7 +3891,9 @@ mod tests { let resolver = state.resolver().expect("resolver"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v10_GITHUB_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v10_GITHUB_TOKEN"), Some("new-19") ); } @@ -3328,7 +3918,9 @@ mod tests { let resolver = state.resolver().expect("retained resolver"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v10_GITHUB_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v10_GITHUB_TOKEN"), None ); } diff --git a/crates/openshell-core/src/secrets.rs b/crates/openshell-core/src/secrets.rs index 2e51c9de28..6f4856be4c 100644 --- a/crates/openshell-core/src/secrets.rs +++ b/crates/openshell-core/src/secrets.rs @@ -114,9 +114,18 @@ pub struct UnresolvedPlaceholderError { enum UnresolvedPlaceholderReason { Unavailable, EndpointMismatch, + PlacementMismatch, } +pub const CREDENTIAL_PLACEMENT_MISMATCH: &str = + "credential is not authorized for the request location"; + impl UnresolvedPlaceholderError { + #[must_use] + pub fn is_placement_mismatch(&self) -> bool { + self.reason == UnresolvedPlaceholderReason::PlacementMismatch + } + #[must_use] pub fn unavailable(location: &'static str) -> Self { unresolved(location) @@ -136,6 +145,7 @@ impl fmt::Display for UnresolvedPlaceholderError { match self.reason { UnresolvedPlaceholderReason::Unavailable => "credential placeholder could not be resolved", + UnresolvedPlaceholderReason::PlacementMismatch => CREDENTIAL_PLACEMENT_MISMATCH, UnresolvedPlaceholderReason::EndpointMismatch => "credential is not authorized for the request endpoint", }, @@ -180,9 +190,31 @@ pub struct RewriteTargetResult { // SecretResolver // --------------------------------------------------------------------------- +/// Credential-specific destinations authorized by the provider profile. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CredentialPlacement { + Header(String), + QueryParam(String), + PathTemplate(String), +} + +/// The actual destination of a workload-provided credential placeholder. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RequestCredentialLocation { + Header(String), + QueryParam(String), + Path(String), + Body, + WebSocket, +} + #[derive(Clone, Default)] pub struct SecretResolver { by_placeholder: HashMap, + // Enabled only by the secret-free policy projection below, never by runtime callers. + policy_redaction_only: bool, + placements: HashMap, + request_location: Option, denied_env_keys: HashSet, identity_bound_env_keys: HashSet, revision_fallback_allowed_revisions: HashMap>>, @@ -292,6 +324,9 @@ impl SecretResolver { let resolver = |by_placeholder: HashMap| { (!by_placeholder.is_empty()).then_some(Self { by_placeholder, + policy_redaction_only: false, + placements: HashMap::new(), + request_location: None, denied_env_keys: HashSet::new(), identity_bound_env_keys: HashSet::new(), revision_fallback_allowed_revisions: HashMap::new(), @@ -347,6 +382,9 @@ impl SecretResolver { child_env, Some(Self { by_placeholder, + policy_redaction_only: false, + placements: HashMap::new(), + request_location: None, denied_env_keys: HashSet::new(), identity_bound_env_keys: HashSet::new(), revision_fallback_allowed_revisions: HashMap::new(), @@ -357,11 +395,13 @@ impl SecretResolver { pub fn merge<'a>(resolvers: impl IntoIterator) -> Option { let mut by_placeholder = HashMap::new(); + let mut placements = HashMap::new(); let mut denied_env_keys = HashSet::new(); let mut identity_bound_env_keys = HashSet::new(); let mut revision_fallback_allowed_revisions = HashMap::new(); for resolver in resolvers { by_placeholder.extend(resolver.by_placeholder.clone()); + placements.extend(resolver.placements.clone()); denied_env_keys.extend(resolver.denied_env_keys.iter().cloned()); identity_bound_env_keys.extend(resolver.identity_bound_env_keys.iter().cloned()); revision_fallback_allowed_revisions @@ -372,6 +412,9 @@ impl SecretResolver { } else { Some(Self { by_placeholder, + policy_redaction_only: false, + placements, + request_location: None, denied_env_keys, identity_bound_env_keys, revision_fallback_allowed_revisions, @@ -405,12 +448,104 @@ impl SecretResolver { .collect(); Self { by_placeholder, + policy_redaction_only: self.policy_redaction_only, + placements: self.placements.clone(), + request_location: None, denied_env_keys, identity_bound_env_keys: bound_keys.clone(), revision_fallback_allowed_revisions, } } + /// Attach mandatory placement metadata to a resolver snapshot. + pub(crate) fn with_placements( + mut self, + placements: HashMap, + ) -> Self { + self.placements = placements; + self + } + + /// Select the actual structured request destination before resolving input. + #[must_use] + pub fn with_request_location(&self, location: RequestCredentialLocation) -> Self { + let mut scoped = self.clone(); + scoped.request_location = Some(location); + scoped + } + + fn placement_permits(&self, placeholder: &str) -> bool { + if self.policy_redaction_only { + return true; + } + let Some(key) = placeholder_env_key(placeholder) else { + return true; + }; + // Context-free reads are trusted configuration/expiry lookups only. + // Bound credentials still require an actual request destination. + if self.request_location.is_none() + && !self.identity_bound_env_keys.contains(key) + && !self.placements.contains_key(key) + { + return true; + } + if matches!( + self.request_location, + Some(RequestCredentialLocation::Body | RequestCredentialLocation::WebSocket) + ) { + // Only explicit endpoint-policy rewrite wrappers choose these contexts. + return true; + } + let Some(placement) = self.placements.get(key) else { + return matches!(self.request_location.as_ref(), Some(RequestCredentialLocation::Header(name)) if name.eq_ignore_ascii_case("authorization")); + }; + match (placement, self.request_location.as_ref()) { + ( + CredentialPlacement::Header(expected), + Some(RequestCredentialLocation::Header(actual)), + ) => expected.eq_ignore_ascii_case(actual), + ( + CredentialPlacement::QueryParam(expected), + Some(RequestCredentialLocation::QueryParam(actual)), + ) => expected == actual, + ( + CredentialPlacement::PathTemplate(expected), + Some(RequestCredentialLocation::Path(actual)), + ) => { + // Normalize only this credential's slot; all other path text must match. + let mut normalized = String::new(); + let mut pos = 0; + let mut slots = 0; + while pos < actual.len() { + let next_canonical = actual[pos..] + .find(PLACEHOLDER_PREFIX) + .map(|offset| pos + offset); + let next_alias = actual[pos..] + .find(PROVIDER_ALIAS_MARKER) + .map(|offset| alias_start_for_marker(actual, pos + offset)); + let next = [next_canonical, next_alias].into_iter().flatten().min(); + let Some(start) = next else { + normalized.push_str(&actual[pos..]); + break; + }; + normalized.push_str(&actual[pos..start]); + let Some((end, token)) = self.credential_token_at(actual, start) else { + return false; + }; + if placeholder_env_key(token) == Some(key) { + normalized.push_str("{credential}"); + slots += 1; + } else { + normalized.push_str(token); + } + pos = end; + } + slots == 1 && normalized == *expected + } + _ => false, + } + } + fn unresolved_for( &self, location: &'static str, @@ -420,6 +555,8 @@ impl SecretResolver { .is_some_and(|key| self.denied_env_keys.contains(key)) { UnresolvedPlaceholderReason::EndpointMismatch + } else if !self.placement_permits(placeholder) { + UnresolvedPlaceholderReason::PlacementMismatch } else { UnresolvedPlaceholderReason::Unavailable }; @@ -435,6 +572,9 @@ impl SecretResolver { // handle retains the original endpoint, epoch, and expiration constraints. let canonical_alias = alias_env_key(value).map(placeholder_for_env_key); let value = canonical_alias.as_deref().unwrap_or(value); + if !self.placement_permits(value) { + return None; + } if placeholder_env_key(value).is_some_and(|key| self.identity_bound_env_keys.contains(key)) && revisioned_placeholder_parts(value).is_none() && stable_placeholder_parts(value).is_none() @@ -510,6 +650,9 @@ impl SecretResolver { reason: UnresolvedPlaceholderReason::EndpointMismatch, }); } + if !self.placement_permits(value) { + return Err(self.unresolved_for(location, value)); + } Ok(self.resolve_placeholder(value)) } @@ -519,6 +662,15 @@ impl SecretResolver { .map(|s| s.expires_at_ms) } + pub fn rewrite_header_value_for_name( + &self, + name: &str, + value: &str, + ) -> Result, UnresolvedPlaceholderError> { + self.with_request_location(RequestCredentialLocation::Header(name.to_string())) + .rewrite_header_value(value) + } + pub fn rewrite_header_value( &self, value: &str, @@ -562,6 +714,15 @@ impl SecretResolver { &self, text: &mut String, location: &'static str, + ) -> Result { + self.with_request_location(RequestCredentialLocation::Body) + .rewrite_text_placeholders_at_location(text, location) + } + + fn rewrite_text_placeholders_at_location( + &self, + text: &mut String, + location: &'static str, ) -> Result { if !contains_raw_reserved_marker(text) { return Ok(0); @@ -627,7 +788,8 @@ impl SecretResolver { &self, text: &mut String, ) -> Result { - self.rewrite_text_placeholders(text, "websocket") + self.with_request_location(RequestCredentialLocation::WebSocket) + .rewrite_text_placeholders_at_location(text, "websocket") } fn credential_token_at<'a>( @@ -677,7 +839,7 @@ impl SecretResolver { } let mut rewritten = decoded; - let replacements = self.rewrite_text_placeholders(&mut rewritten, "header")?; + let replacements = self.rewrite_text_placeholders_at_location(&mut rewritten, "header")?; if replacements == 0 { return Ok(None); @@ -1092,6 +1254,9 @@ fn rewrite_uri_path( path: &str, resolver: &SecretResolver, ) -> Result, UnresolvedPlaceholderError> { + let scoped = + resolver.with_request_location(RequestCredentialLocation::Path(percent_decode(path))); + let resolver = &scoped; let segments: Vec<&str> = path.split('/').collect(); let mut resolved_segments = Vec::with_capacity(segments.len()); let mut redacted_segments = Vec::with_capacity(segments.len()); @@ -1202,8 +1367,11 @@ fn rewrite_uri_query_params( let decoded_value = percent_decode(value); if contains_raw_reserved_marker(&decoded_value) { let mut rewritten = decoded_value.clone(); + let decoded_key = percent_decode(&key.replace('+', " ")); + let scoped = resolver + .with_request_location(RequestCredentialLocation::QueryParam(decoded_key)); let replacements = - resolver.rewrite_text_placeholders(&mut rewritten, "query_param")?; + scoped.rewrite_text_placeholders_at_location(&mut rewritten, "query_param")?; if replacements == 0 || contains_raw_reserved_marker(&rewritten) { return Err(unresolved("query_param")); } @@ -1326,10 +1494,12 @@ pub fn rewrite_header_line_checked( return Ok(line.to_string()); }; - resolver.rewrite_header_value(value.trim())?.map_or_else( - || Ok(line.to_string()), - |rewritten| Ok(format!("{name}: {rewritten}")), - ) + resolver + .rewrite_header_value_for_name(name, value.trim())? + .map_or_else( + || Ok(line.to_string()), + |rewritten| Ok(format!("{name}: {rewritten}")), + ) } /// Resolve placeholders in a request target (path + query) for OPA evaluation. @@ -1418,9 +1588,10 @@ pub fn redact_target_for_policy(target: &str) -> Result SecretResolver { + let (_, resolver) = SecretResolver::from_provider_env_for_revision( + HashMap::from([("TOKEN".to_string(), "secret-value".to_string())]), + HashMap::new(), + 1, + ); + resolver + .unwrap() + .with_placements(HashMap::from([("TOKEN".to_string(), placement)])) + } + + #[test] + fn missing_placement_map_defaults_request_rewriting_to_authorization() { + let (_, resolver) = SecretResolver::from_provider_env(HashMap::from([( + "TOKEN".into(), + "synthetic".into(), + )])); + let resolver = resolver.unwrap(); + for token in [ + "openshell:resolve:env:TOKEN", + "vendor-OPENSHELL-RESOLVE-ENV-TOKEN", + ] { + for scheme in ["Bearer", "token"] { + assert_eq!( + resolver + .rewrite_header_value_for_name( + "aUtHoRiZaTiOn", + &format!("{scheme} {token}") + ) + .unwrap(), + Some(format!("{scheme} synthetic")) + ); + } + let basic = base64::engine::general_purpose::STANDARD.encode(format!("user:{token}")); + assert!( + resolver + .rewrite_header_value_for_name("Authorization", &format!("Basic {basic}")) + .unwrap() + .is_some() + ); + for name in ["X-Reflect", "X-Api-Key"] { + assert!( + resolver + .rewrite_header_value_for_name(name, token) + .unwrap_err() + .is_placement_mismatch() + ); + assert!( + resolver + .rewrite_header_value_for_name(name, &format!("Basic {basic}")) + .unwrap_err() + .is_placement_mismatch() + ); + } + for target in [ + format!("/search?token={token}"), + format!( + "/search?token={}&token={}", + fully_percent_encoded(token), + fully_percent_encoded(token) + ), + format!("/api/{token}"), + ] { + assert!( + rewrite_target_for_eval(&target, &resolver) + .unwrap_err() + .is_placement_mismatch() + ); + } + } + // These methods are called only after explicit endpoint-policy opt-in. + assert_eq!( + resolver + .rewrite_text_placeholders( + &mut "openshell:resolve:env:TOKEN".into(), + "request_body" + ) + .unwrap(), + 1 + ); + assert_eq!( + resolver + .rewrite_websocket_text_placeholders(&mut "openshell:resolve:env:TOKEN".into()) + .unwrap(), + 1 + ); + assert_eq!( + resolver + .resolve_current_env_key_checked("TOKEN", "sigv4") + .unwrap(), + Some("synthetic") + ); + } + + #[test] + fn static_header_placement_cannot_be_overridden_by_request_location() { + let resolver = placement_resolver(CredentialPlacement::Header("Authorization".into())); + let token = "openshell:resolve:env:v1_TOKEN"; + assert_eq!( + resolver + .rewrite_header_value_for_name("aUtHoRiZaTiOn", &format!("token {token}")) + .unwrap(), + Some("token secret-value".into()) + ); + assert!( + resolver + .rewrite_header_value_for_name("X-Reflect", token) + .unwrap_err() + .is_placement_mismatch() + ); + assert!( + rewrite_target_for_eval(&format!("/search?q={token}"), &resolver) + .unwrap_err() + .is_placement_mismatch() + ); + assert!( + rewrite_target_for_eval(&format!("/bot{token}/events"), &resolver) + .unwrap_err() + .is_placement_mismatch() + ); + assert_eq!( + resolver + .rewrite_text_placeholders(&mut token.to_string(), "body") + .unwrap(), + 1 + ); + assert_eq!( + resolver + .rewrite_websocket_text_placeholders(&mut token.to_string()) + .unwrap(), + 1 + ); + assert!(resolver.resolve_placeholder(token).is_none()); + let basic = base64::engine::general_purpose::STANDARD.encode(format!("user:{token}")); + assert!( + resolver + .rewrite_header_value_for_name("Authorization", &format!("Basic {basic}")) + .unwrap() + .is_some() + ); + assert!( + resolver + .rewrite_header_value_for_name("X-Reflect", &format!("Basic {basic}")) + .unwrap_err() + .is_placement_mismatch() + ); + } + + #[test] + fn query_placement_checks_each_decoded_parameter_name() { + let resolver = placement_resolver(CredentialPlacement::QueryParam("api_key".into())); + let token = "openshell:resolve:env:v1_TOKEN"; + let valid = format!("/search?%61pi_key={token}"); + assert_eq!( + rewrite_target_for_eval(&valid, &resolver).unwrap().resolved, + "/search?%61pi_key=secret-value" + ); + for target in [ + format!("/search?other={token}"), + format!("/search?api_key={token}&other={token}"), + "/search?other=vendor-OPENSHELL-RESOLVE-ENV-v1_TOKEN".to_string(), + ] { + assert!( + rewrite_target_for_eval(&target, &resolver) + .unwrap_err() + .is_placement_mismatch() + ); + } + assert!( + rewrite_target_for_eval( + &format!("/search?api_key={token}&api_key={token}"), + &resolver + ) + .is_ok() + ); + } + + #[test] + fn path_placement_checks_exact_slot_and_literal_template() { + let resolver = placement_resolver(CredentialPlacement::PathTemplate( + "/bot{credential}/events".into(), + )); + let token = "openshell:resolve:env:v1_TOKEN"; + assert_eq!( + rewrite_target_for_eval(&format!("/bot{token}/events"), &resolver) + .unwrap() + .resolved, + "/botsecret-value/events" + ); + assert!( + rewrite_target_for_eval(&format!("/bot{token}/echo"), &resolver) + .unwrap_err() + .is_placement_mismatch() + ); + assert!( + rewrite_target_for_eval(&format!("/echo/{token}/events"), &resolver) + .unwrap_err() + .is_placement_mismatch() + ); + assert!( + rewrite_target_for_eval( + &format!("/bot{}/events", fully_percent_encoded(token)), + &resolver + ) + .is_ok() + ); + let alias_resolver = placement_resolver(CredentialPlacement::PathTemplate( + "/{credential}/events".into(), + )); + assert!( + rewrite_target_for_eval( + "/vendor-OPENSHELL-RESOLVE-ENV-v1_TOKEN/events", + &alias_resolver + ) + .is_ok() + ); + } + fn fully_percent_encoded(marker: &str) -> String { const HEX: &[u8; 16] = b"0123456789ABCDEF"; let mut encoded = String::with_capacity(marker.len() * 3); @@ -1553,7 +1942,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "CUSTOM_TOKEN".into(), + CredentialPlacement::Header("x-api-key".into()), + )])); assert_eq!( rewrite_header_line("x-api-key: openshell:resolve:env:CUSTOM_TOKEN", &resolver), @@ -1632,7 +2024,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "CHAT_APP_TOKEN".into(), + CredentialPlacement::Header("x-app-token".into()), + )])); assert_eq!( rewrite_header_line( @@ -1711,6 +2106,12 @@ mod tests { x-api-key: {token_value}\r\n\ Content-Length: 0\r\n\r\n" ); + let resolver = resolver.map(|resolver| { + resolver.with_placements(HashMap::from([( + "CUSTOM_SERVICE_TOKEN".into(), + CredentialPlacement::Header("x-api-key".into()), + )])) + }); let result = rewrite_http_header_block(raw.as_bytes(), resolver.as_ref()).expect("should succeed"); @@ -1815,6 +2216,12 @@ mod tests { .into_iter() .collect(), ); + let resolver = resolver.map(|resolver| { + resolver.with_placements(HashMap::from([( + "YOUTUBE_API_KEY".into(), + CredentialPlacement::QueryParam("key".into()), + )])) + }); let placeholder = child_env.get("YOUTUBE_API_KEY").unwrap(); let raw = format!( @@ -1843,6 +2250,12 @@ mod tests { .into_iter() .collect(), ); + let resolver = resolver.map(|resolver| { + resolver.with_placements(HashMap::from([( + "API_KEY".into(), + CredentialPlacement::QueryParam("token".into()), + )])) + }); let placeholder = child_env.get("API_KEY").unwrap(); let raw = format!("GET /api?token={placeholder} HTTP/1.1\r\nHost: x\r\n\r\n"); @@ -1863,6 +2276,12 @@ mod tests { .into_iter() .collect(), ); + let resolver = resolver.map(|resolver| { + resolver.with_placements(HashMap::from([( + "KEY".into(), + CredentialPlacement::QueryParam("key".into()), + )])) + }); let placeholder = child_env.get("KEY").unwrap(); let raw = format!("GET /api?key={placeholder}&format=json HTTP/1.1\r\nHost: x\r\n\r\n"); @@ -2004,7 +2423,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "TOKEN".into(), + CredentialPlacement::PathTemplate("/api/{credential}/data".into()), + )])); let placeholder = child_env.get("TOKEN").unwrap(); let raw = format!("GET /api/{placeholder}/data HTTP/1.1\r\nHost: x\r\n\r\n"); @@ -2032,7 +2454,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "TELEGRAM_TOKEN".into(), + CredentialPlacement::PathTemplate("/bot{credential}/sendMessage".into()), + )])); let placeholder = child_env.get("TELEGRAM_TOKEN").unwrap(); let raw = format!( @@ -2067,6 +2492,16 @@ mod tests { let resolver = resolver.expect("resolver"); let org_ph = child_env.get("ORG_ID").unwrap(); let key_ph = child_env.get("API_KEY").unwrap(); + let resolver = resolver.with_placements(HashMap::from([ + ( + "ORG_ID".into(), + CredentialPlacement::PathTemplate(format!("/orgs/{{credential}}/keys/{key_ph}")), + ), + ( + "API_KEY".into(), + CredentialPlacement::PathTemplate(format!("/orgs/{org_ph}/keys/{{credential}}")), + ), + ])); let raw = format!("GET /orgs/{org_ph}/keys/{key_ph} HTTP/1.1\r\nHost: x\r\n\r\n"); let result = @@ -2100,7 +2535,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "TOKEN".into(), + CredentialPlacement::PathTemplate("/bot{credential}/method".into()), + )])); let placeholder = child_env.get("TOKEN").unwrap(); let raw = format!("GET /bot{placeholder}/method?format=json HTTP/1.1\r\nHost: x\r\n\r\n"); @@ -2121,7 +2559,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "BAD".into(), + CredentialPlacement::PathTemplate("/api/{credential}/data".into()), + )])); let placeholder = child_env.get("BAD").unwrap(); let raw = format!("GET /api/{placeholder}/data HTTP/1.1\r\nHost: x\r\n\r\n"); @@ -2139,7 +2580,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "BAD".into(), + CredentialPlacement::PathTemplate("/api/{credential}".into()), + )])); let placeholder = child_env.get("BAD").unwrap(); let raw = format!("GET /api/{placeholder} HTTP/1.1\r\nHost: x\r\n\r\n"); @@ -2157,7 +2601,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "BAD".into(), + CredentialPlacement::PathTemplate("/api/{credential}".into()), + )])); let placeholder = child_env.get("BAD").unwrap(); let raw = format!("GET /api/{placeholder} HTTP/1.1\r\nHost: x\r\n\r\n"); @@ -2175,7 +2622,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "BAD".into(), + CredentialPlacement::PathTemplate("/api/{credential}".into()), + )])); let placeholder = child_env.get("BAD").unwrap(); let raw = format!("GET /api/{placeholder} HTTP/1.1\r\nHost: x\r\n\r\n"); @@ -2196,7 +2646,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "TOKEN".into(), + CredentialPlacement::PathTemplate("/api/{credential}/data".into()), + )])); let placeholder = child_env.get("TOKEN").unwrap(); // Space in the credential should trigger path validation rejection @@ -2283,7 +2736,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "API_TOKEN".into(), + CredentialPlacement::QueryParam("token".into()), + )])); let encoded = "openshell%3Aresolve%3Aenv%3AAPI_TOKEN"; let raw = format!("GET /api?token={encoded} HTTP/1.1\r\nHost: x\r\n\r\n"); @@ -2305,16 +2761,31 @@ mod tests { [ ("KEY1".to_string(), "secret1".to_string()), ("KEY2".to_string(), "secret2".to_string()), + ("KEY3".to_string(), "secret3".to_string()), ] .into_iter() .collect(), ); + let resolver = resolver.map(|resolver| { + resolver.with_placements(HashMap::from([ + ( + "KEY1".into(), + CredentialPlacement::PathTemplate("/api/{credential}".into()), + ), + ( + "KEY2".into(), + CredentialPlacement::QueryParam("token".into()), + ), + ("KEY3".into(), CredentialPlacement::Header("x-auth".into())), + ])) + }); let ph1 = child_env.get("KEY1").unwrap(); let ph2 = child_env.get("KEY2").unwrap(); + let ph3 = child_env.get("KEY3").unwrap(); let raw = format!( "GET /api/{ph1}?token={ph2} HTTP/1.1\r\n\ - x-auth: {ph1}\r\n\r\n" + x-auth: {ph3}\r\n\r\n" ); let result = rewrite_http_header_block(raw.as_bytes(), resolver.as_ref()).expect("should succeed"); @@ -2498,7 +2969,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "TOKEN".into(), + CredentialPlacement::PathTemplate("/bot{credential}/sendMessage".into()), + )])); let placeholder = child_env.get("TOKEN").unwrap(); let result = rewrite_target_for_eval(&format!("/bot{placeholder}/sendMessage"), &resolver) @@ -2516,7 +2990,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "KEY".into(), + CredentialPlacement::QueryParam("key".into()), + )])); let placeholder = child_env.get("KEY").unwrap(); let result = @@ -2614,7 +3091,13 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([ + ( + "TOKEN".into(), + CredentialPlacement::PathTemplate("/bot{credential}/method".into()), + ), + ("KEY".into(), CredentialPlacement::QueryParam("key".into())), + ])); let tok_ph = child_env.get("TOKEN").unwrap(); let key_ph = child_env.get("KEY").unwrap(); diff --git a/crates/openshell-driver-vm/README.md b/crates/openshell-driver-vm/README.md index a0371bacfb..cb9433d08e 100644 --- a/crates/openshell-driver-vm/README.md +++ b/crates/openshell-driver-vm/README.md @@ -70,7 +70,7 @@ By default `mise run gateway:vm`: - Persists the gateway SQLite DB under `.cache/gateway-vm/gateway.db`. - Places the VM driver state (per-sandbox `overlay.ext4`, image cache, and `run/compute-driver.sock`) under `/tmp/openshell-vm-driver-$USER-vm-dev/` so the AF_UNIX socket path stays under macOS `SUN_LEN`. - Writes `.cache/gateway-vm/gateway.toml` with `[openshell.drivers.vm].driver_dir = "$PWD/target/debug"` so the freshly built `openshell-driver-vm` is used instead of an older installed copy from `~/.local/libexec/openshell`, `/usr/libexec/openshell`, or `/usr/local/libexec`. -- Enables OTLP trace export, OCSF log export, and the agent trace relay to `http://127.0.0.1:4317` only when a local collector is listening there. Otherwise, it omits the OTLP configuration to avoid repeated export failures. +- Enables OTLP trace export, OCSF log export, and the agent trace relay only when `OPENSHELL_DEV_OTLP_ENDPOINT` names a collector, for example `http://127.0.0.1:4317`. Otherwise, it omits the OTLP configuration. For GPU passthrough (VFIO), pass `-- --gpu` and run with root privileges: diff --git a/crates/openshell-isolation-interface/src/contract.rs b/crates/openshell-isolation-interface/src/contract.rs index de2a01975f..bd328fc0a1 100644 --- a/crates/openshell-isolation-interface/src/contract.rs +++ b/crates/openshell-isolation-interface/src/contract.rs @@ -141,7 +141,13 @@ pub struct BackendDescriptor { } /// Exact non-root identity selected before the immutable workload is created. +/// +/// Validation and normalization run in [`ResolvedWorkloadIdentity::new`] and +/// again when a value is decoded from a serialized descriptor: deserialization +/// routes through the same constructor, so an invalid identity fails to decode +/// instead of bypassing the contract. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(try_from = "RawResolvedWorkloadIdentity")] pub struct ResolvedWorkloadIdentity { /// Effective and real user ID used by sandbox and all workload children. pub uid: u32, @@ -187,6 +193,32 @@ impl ResolvedWorkloadIdentity { } } +/// Unvalidated wire shape of [`ResolvedWorkloadIdentity`], existing only so +/// serde decoding can route through [`ResolvedWorkloadIdentity::new`]. +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct RawResolvedWorkloadIdentity { + uid: u32, + gid: u32, + supplementary_gids: Vec, + source: String, + resource_digest: String, +} + +impl TryFrom for ResolvedWorkloadIdentity { + type Error = BackendError; + + fn try_from(raw: RawResolvedWorkloadIdentity) -> Result { + Self::new( + raw.uid, + raw.gid, + raw.supplementary_gids, + raw.source, + raw.resource_digest, + ) + } +} + /// The trusted sandbox context, constructed by trusted common code after the /// control plane assigns the resource to the admitted sandbox. /// diff --git a/crates/openshell-isolation-interface/tests/backend_conformance.rs b/crates/openshell-isolation-interface/tests/backend_conformance.rs index e34e6215bd..9928b02073 100644 --- a/crates/openshell-isolation-interface/tests/backend_conformance.rs +++ b/crates/openshell-isolation-interface/tests/backend_conformance.rs @@ -77,6 +77,115 @@ fn workload_identity_rejects_root_and_normalizes_groups() { assert_eq!(identity.supplementary_gids, vec![1002, 1003]); } +#[test] +fn workload_identity_deserialization_enforces_new_invariants() { + fn payload(uid: u32, gid: u32, gids: &[u32], source: &str, digest: &str) -> serde_json::Value { + serde_json::json!({ + "uid": uid, + "gid": gid, + "supplementary_gids": gids, + "source": source, + "resource_digest": digest, + }) + } + + assert!( + serde_json::from_value::(payload( + 0, + 1000, + &[], + "policy", + "sha256:test" + )) + .is_err() + ); + assert!( + serde_json::from_value::(payload( + 1000, + 0, + &[], + "policy", + "sha256:test" + )) + .is_err() + ); + assert!( + serde_json::from_value::(payload( + 1000, + 1000, + &[0], + "policy", + "sha256:test" + )) + .is_err() + ); + assert!( + serde_json::from_value::(payload( + 1000, + 1000, + &[], + "", + "sha256:test" + )) + .is_err() + ); + assert!( + serde_json::from_value::(payload(1000, 1000, &[], "policy", "")) + .is_err() + ); + assert!( + serde_json::from_value::(payload( + 1000, + 1000, + &[], + " ", + "sha256:test" + )) + .is_err() + ); + assert!( + serde_json::from_value::(payload(1000, 1000, &[], "policy", " ")) + .is_err() + ); + + let identity = workload_identity(); + let encoded = serde_json::to_value(&identity).unwrap(); + assert_eq!( + serde_json::from_value::(encoded).unwrap(), + identity + ); + + let decoded = serde_json::from_value::(payload( + 1000, + 1000, + &[1003, 1001, 1002, 1003], + "policy", + "sha256:test", + )) + .unwrap(); + assert_eq!(decoded.supplementary_gids, vec![1001, 1002, 1003]); + + let decoded = serde_json::from_value::(payload( + 1000, + 1000, + &[1000, 1001], + "policy", + "sha256:test", + )) + .unwrap(); + assert_eq!(decoded.supplementary_gids, vec![1001]); +} + +#[test] +fn boundary_confirmation_rejects_invalid_embedded_identity() { + let encoded = serde_json::to_value(confirmation()).unwrap(); + serde_json::from_value::(encoded.clone()).unwrap(); + + let mut invalid = encoded; + invalid["identity"]["uid"] = serde_json::json!(0); + assert!(serde_json::from_value::(invalid).is_err()); +} + #[test] fn confirmation_rejects_missing_generation_or_identity_drift() { let expected = workload_identity(); diff --git a/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+container+host+security_control+trace.json b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+container+host+security_control+trace.json new file mode 100644 index 0000000000..873d9a449c --- /dev/null +++ b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+container+host+security_control+trace.json @@ -0,0 +1,8273 @@ +{ + "$defs": { + "kb_article": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "src_url" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "KB Article", + "type": "object" + }, + "long_string": { + "additionalProperties": false, + "properties": { + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "value" + ], + "title": "Long String", + "type": "object" + }, + "enrichment": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "short_desc": { + "title": "Short Description", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "data", + "name", + "value" + ], + "title": "Enrichment", + "type": "object" + }, + "ldap_person": { + "additionalProperties": false, + "properties": { + "cost_center": { + "title": "Cost Center", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "deleted_time": { + "title": "Deleted Time", + "type": "integer" + }, + "department": { + "title": "Department", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "email_addrs": { + "items": { + "type": "string" + }, + "title": "Email Addresses", + "type": "array" + }, + "employee_uid": { + "title": "Employee ID", + "type": "string" + }, + "given_name": { + "title": "Given Name", + "type": "string" + }, + "hire_time": { + "title": "Hire Time", + "type": "integer" + }, + "job_title": { + "title": "Job Title", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "last_login_time": { + "title": "Last Login", + "type": "integer" + }, + "ldap_cn": { + "title": "LDAP Common Name", + "type": "string" + }, + "ldap_dn": { + "title": "LDAP Distinguished Name", + "type": "string" + }, + "leave_time": { + "title": "Leave Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "manager": { + "$ref": "#/$defs/user", + "title": "Manager" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "office_location": { + "title": "Office Location", + "type": "string" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "surname": { + "title": "Surname", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + } + }, + "title": "LDAP Person", + "type": "object" + }, + "email_auth": { + "additionalProperties": false, + "properties": { + "dkim": { + "title": "DKIM Status", + "type": "string" + }, + "dkim_domain": { + "title": "DKIM Domain", + "type": "string" + }, + "dkim_signature": { + "title": "DKIM Signature", + "type": "string" + }, + "dmarc": { + "title": "DMARC Status", + "type": "string" + }, + "dmarc_override": { + "title": "DMARC Override", + "type": "string" + }, + "dmarc_policy": { + "title": "DMARC Policy", + "type": "string" + }, + "spf": { + "title": "SPF Status", + "type": "string" + } + }, + "title": "Email Authentication", + "type": "object" + }, + "epss": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "percentile": { + "title": "EPSS Percentile", + "type": "number" + }, + "score": { + "title": "EPPS Score", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "score" + ], + "title": "EPSS", + "type": "object" + }, + "device_hw_info": { + "additionalProperties": false, + "properties": { + "bios_date": { + "title": "BIOS Date", + "type": "string" + }, + "bios_manufacturer": { + "title": "BIOS Manufacturer", + "type": "string" + }, + "bios_ver": { + "title": "BIOS Version", + "type": "string" + }, + "chassis": { + "title": "Chassis", + "type": "string" + }, + "cpu_architecture": { + "title": "CPU Architecture", + "type": "string" + }, + "cpu_architecture_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "CPU Architecture ID", + "type": "integer" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "cpu_cores": { + "title": "CPU Cores", + "type": "integer" + }, + "cpu_count": { + "title": "CPU Count", + "type": "integer" + }, + "cpu_speed": { + "title": "Processor Speed", + "type": "integer" + }, + "cpu_type": { + "title": "Processor Type", + "type": "string" + }, + "desktop_display": { + "$ref": "#/$defs/display", + "title": "Desktop Display" + }, + "gpu_count": { + "title": "GPU Count", + "type": "integer" + }, + "gpu_info_list": { + "items": { + "$ref": "#/$defs/gpu_info" + }, + "title": "GPU information", + "type": "array" + }, + "keyboard_info": { + "$ref": "#/$defs/keyboard_info", + "title": "Keyboard Information" + }, + "ram_size": { + "title": "RAM Size", + "type": "integer" + }, + "serial_number": { + "title": "Serial Number", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Device Hardware Info", + "type": "object" + }, + "network_proxy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "title": "Network Proxy Endpoint", + "type": "object" + }, + "data_classification": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "category_id" + ] + }, + { + "required": [ + "confidentiality_id" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "category_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Category ID", + "type": "integer" + }, + "classifier_details": { + "$ref": "#/$defs/classifier_details", + "title": "Classifier Details" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "discovery_details": { + "items": { + "$ref": "#/$defs/discovery_details" + }, + "title": "Discovery Details", + "type": "array" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "status_details": { + "items": { + "type": "string" + }, + "title": "Status Details", + "type": "array" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Data Classification", + "type": "object" + }, + "http_response": { + "additionalProperties": false, + "properties": { + "body_length": { + "title": "Response Body Length", + "type": "integer" + }, + "code": { + "title": "Response Code", + "type": "integer" + }, + "content_type": { + "title": "HTTP Content Type", + "type": "string" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "latency": { + "title": "Latency", + "type": "integer" + }, + "length": { + "title": "Response Length", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "code" + ], + "title": "HTTP Response", + "type": "object" + }, + "resource_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "criticality": { + "title": "Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Resource Relationship" + }, + "role": { + "title": "Role", + "type": "string" + }, + "role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Role ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Resource Details", + "type": "object" + }, + "display": { + "additionalProperties": false, + "properties": { + "color_depth": { + "title": "Color Depth", + "type": "integer" + }, + "physical_height": { + "title": "Physical Height", + "type": "integer" + }, + "physical_orientation": { + "title": "Physical Orientation", + "type": "integer" + }, + "physical_width": { + "title": "Physical Width", + "type": "integer" + }, + "scale_factor": { + "title": "Scale Factor", + "type": "integer" + } + }, + "title": "Display", + "type": "object" + }, + "scim": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "error_message": { + "title": "Last Error Message", + "type": "string" + }, + "is_group_provisioning_enabled": { + "title": "SCIM Group Provisioning Enabled", + "type": "boolean" + }, + "is_user_provisioning_enabled": { + "title": "SCIM User Provisioning Enabled", + "type": "boolean" + }, + "last_run_time": { + "title": "Last Sync Time", + "type": "integer" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "scim_group_schema": { + "title": "SCIM Group Schema" + }, + "scim_user_schema": { + "title": "SCIM User Schema" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "External ID", + "type": "string" + }, + "url_string": { + "title": "SCIM Endpoint URL", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + }, + "version": { + "title": "SCIM Version", + "type": "string" + } + }, + "title": "SCIM", + "type": "object" + }, + "programmatic_credential": { + "additionalProperties": false, + "properties": { + "last_used_time": { + "title": "Last Used Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Programmatic Credential", + "type": "object" + }, + "os": { + "additionalProperties": false, + "properties": { + "build": { + "title": "OS Build", + "type": "string" + }, + "country": { + "title": "Country", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "edition": { + "title": "OS Edition", + "type": "string" + }, + "kernel_release": { + "title": "Kernel Release", + "type": "string" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "sp_name": { + "title": "OS Service Pack", + "type": "string" + }, + "sp_ver": { + "title": "OS Service Pack Version", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 99, + 100, + 101, + 200, + 201, + 300, + 301, + 302, + 400, + 401, + 402 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "Operating System (OS)", + "type": "object" + }, + "tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Tactic", + "type": "object" + }, + "dns_answer": { + "additionalProperties": false, + "properties": { + "class": { + "title": "Resource Record Class", + "type": "string" + }, + "flag_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "type": "integer" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "packet_uid": { + "title": "Packet UID", + "type": "integer" + }, + "rdata": { + "title": "DNS RData", + "type": "string" + }, + "ttl": { + "title": "TTL", + "type": "integer" + }, + "type": { + "title": "Resource Record Type", + "type": "string" + } + }, + "required": [ + "rdata" + ], + "title": "DNS Answer", + "type": "object" + }, + "discovery_details": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "occurrence_details": { + "$ref": "#/$defs/occurrence_details", + "title": "Occurrence Details" + }, + "occurrences": { + "items": { + "$ref": "#/$defs/occurrence_details" + }, + "title": "Occurrences", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "title": "Discovery Details", + "type": "object" + }, + "sso": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "SAML Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "duration_mins": { + "title": "SSO Session Duration", + "type": "integer" + }, + "idle_timeout": { + "title": "SSO Idle Timeout", + "type": "integer" + }, + "login_endpoint": { + "title": "SSO Login Endpoint", + "type": "string" + }, + "logout_endpoint": { + "title": "SSO Logout Endpoint", + "type": "string" + }, + "metadata_endpoint": { + "title": "SSO Metadata Endpoint", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "title": "Scopes", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + } + }, + "title": "SSO", + "type": "object" + }, + "token": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_renewable": { + "title": "Renewable", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Token", + "type": "object" + }, + "api": { + "additionalProperties": false, + "properties": { + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "request": { + "$ref": "#/$defs/request", + "title": "API Request Details" + }, + "response": { + "$ref": "#/$defs/response", + "title": "API Response Details" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "token": { + "$ref": "#/$defs/token", + "title": "Token" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "operation" + ], + "title": "API", + "type": "object" + }, + "d3fend": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "d3f_tactic" + ] + }, + { + "required": [ + "d3f_technique" + ] + } + ], + "properties": { + "d3f_tactic": { + "$ref": "#/$defs/d3f_tactic", + "title": "MITRE D3FEND\u2122 Tactic" + }, + "d3f_technique": { + "$ref": "#/$defs/d3f_technique", + "title": "MITRE D3FEND\u2122 Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122", + "type": "object" + }, + "reputation": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Reputation Score", + "type": "number" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "score": { + "title": "Reputation Score", + "type": "string" + }, + "score_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "Reputation Score ID", + "type": "integer" + } + }, + "required": [ + "base_score", + "score_id" + ], + "title": "Reputation", + "type": "object" + }, + "mitigation": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "countermeasures": { + "items": { + "$ref": "#/$defs/d3fend" + }, + "title": "Countermeasures", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Mitigation", + "type": "object" + }, + "location": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "city" + ] + }, + { + "required": [ + "country" + ] + }, + { + "required": [ + "postal_code" + ] + }, + { + "required": [ + "region" + ] + } + ], + "properties": { + "aerial_height": { + "title": "Aerial Height", + "type": "string" + }, + "city": { + "title": "City", + "type": "string" + }, + "continent": { + "title": "Continent", + "type": "string" + }, + "coordinates": { + "items": { + "type": "number" + }, + "title": "Coordinates", + "type": "array" + }, + "country": { + "title": "Country", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "geodetic_altitude": { + "title": "Geodetic Altitude", + "type": "string" + }, + "geodetic_vertical_accuracy": { + "title": "Geodetic Vertical Accuracy", + "type": "string" + }, + "geohash": { + "title": "Geohash", + "type": "string" + }, + "horizontal_accuracy": { + "title": "Horizontal Accuracy", + "type": "string" + }, + "is_on_premises": { + "title": "On-Premises", + "type": "boolean" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "lat": { + "title": "Latitude", + "type": "number" + }, + "long": { + "title": "Longitude", + "type": "number" + }, + "postal_code": { + "title": "Postal Code", + "type": "string" + }, + "pressure_altitude": { + "title": "Pressure Altitude", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + } + }, + "title": "Geo Location", + "type": "object" + }, + "idp": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "auth_factors": { + "items": { + "$ref": "#/$defs/auth_factor" + }, + "title": "Authentication Factors", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprint": { + "$ref": "#/$defs/fingerprint", + "title": "Certificate Fingerprint" + }, + "has_mfa": { + "title": "MFA Enforced", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scim": { + "$ref": "#/$defs/scim", + "title": "SCIM" + }, + "sso": { + "$ref": "#/$defs/sso", + "title": "SSO" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "Configuration URL", + "type": "string" + } + }, + "title": "Identity Provider", + "type": "object" + }, + "message_context": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "application" + ] + }, + { + "required": [ + "service" + ] + } + ], + "properties": { + "ai_role": { + "title": "AI Role", + "type": "string" + }, + "ai_role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "AI Role ID", + "type": "integer" + }, + "application": { + "$ref": "#/$defs/application", + "title": "Application" + }, + "completion_tokens": { + "title": "Completion Tokens", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "prompt_tokens": { + "title": "Prompt Tokens", + "type": "integer" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "total_tokens": { + "title": "Total Tokens", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Message Context", + "type": "object" + }, + "process": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ancestry": { + "items": { + "$ref": "#/$defs/process_entity" + }, + "title": "Ancestry", + "type": "array" + }, + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "environment_variables": { + "items": { + "$ref": "#/$defs/environment_variable" + }, + "title": "Environment Variables", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hosted_services": { + "items": { + "$ref": "#/$defs/win_win_service" + }, + "title": "Hosted Services", + "type": "array" + }, + "integrity": { + "title": "Integrity", + "type": "string" + }, + "integrity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Integrity Level", + "type": "integer" + }, + "lineage": { + "items": { + "type": "string" + }, + "title": "Lineage", + "type": "array" + }, + "loaded_modules": { + "items": { + "type": "string" + }, + "title": "Loaded Modules", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "parent_process": { + "$ref": "#/$defs/process", + "title": "Parent Process" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "ptid": { + "title": "Process Thread ID", + "type": "integer" + }, + "sandbox": { + "title": "Sandbox", + "type": "string" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "terminated_time": { + "title": "Terminated Time", + "type": "integer" + }, + "tid": { + "title": "Thread ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + }, + "working_directory": { + "title": "Working Directory", + "type": "string" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + } + }, + "title": "Process", + "type": "object" + }, + "vulnerability": { + "additionalProperties": false, + "oneOf": [ + { + "not": { + "required": [ + "cve", + "cwe" + ] + }, + "required": [ + "advisory" + ] + }, + { + "not": { + "required": [ + "advisory", + "cwe" + ] + }, + "required": [ + "cve" + ] + }, + { + "not": { + "required": [ + "advisory", + "cve" + ] + }, + "required": [ + "cwe" + ] + } + ], + "properties": { + "advisory": { + "$ref": "#/$defs/advisory", + "title": "Security Advisory" + }, + "affected_code": { + "items": { + "$ref": "#/$defs/affected_code" + }, + "title": "Affected Code", + "type": "array" + }, + "affected_packages": { + "items": { + "$ref": "#/$defs/affected_package" + }, + "title": "Affected Software Packages", + "type": "array" + }, + "category": { + "title": "Category", + "type": "string" + }, + "cve": { + "$ref": "#/$defs/cve", + "title": "CVE" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "dependency_chain": { + "title": "Dependency Chain", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "exploit_last_seen_time": { + "title": "Exploit Last Seen Time", + "type": "integer" + }, + "exploit_ref_url": { + "title": "Exploit URL", + "type": "string" + }, + "exploit_requirement": { + "title": "Exploit Requirement", + "type": "string" + }, + "exploit_type": { + "title": "Exploit Type", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "fix_coverage": { + "title": "Fix Coverage", + "type": "string" + }, + "fix_coverage_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Fix Coverage ID", + "type": "integer" + }, + "is_exploit_available": { + "title": "Exploit Availability", + "type": "boolean" + }, + "is_fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "packages": { + "items": { + "$ref": "#/$defs/package" + }, + "title": "Software Packages", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_vulnerabilities": { + "items": { + "type": "string" + }, + "title": "Related Vulnerability IDs", + "type": "array" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Vulnerability Details", + "type": "object" + }, + "affected_code": { + "additionalProperties": false, + "properties": { + "end_column": { + "title": "End Column", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "rule": { + "$ref": "#/$defs/rule", + "title": "Related Rule" + }, + "start_column": { + "title": "Start Column", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "required": [ + "file" + ], + "title": "Affected Code", + "type": "object" + }, + "keyboard_info": { + "additionalProperties": false, + "properties": { + "function_keys": { + "title": "Function Keys", + "type": "integer" + }, + "ime": { + "title": "IME", + "type": "string" + }, + "keyboard_layout": { + "title": "Keyboard Layout", + "type": "string" + }, + "keyboard_subtype": { + "title": "Keyboard Subtype", + "type": "integer" + }, + "keyboard_type": { + "title": "Keyboard Type", + "type": "string" + } + }, + "title": "Keyboard Information", + "type": "object" + }, + "certificate": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "is_self_signed": { + "title": "Certificate Self-Signed", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Distinguished Name", + "type": "string" + }, + "sans": { + "items": { + "$ref": "#/$defs/san" + }, + "title": "Subject Alternative Names", + "type": "array" + }, + "serial_number": { + "title": "Certificate Serial Number", + "type": "string" + }, + "subject": { + "title": "Subject Distinguished Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "issuer", + "serial_number" + ], + "title": "Digital Certificate", + "type": "object" + }, + "graph": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "edges": { + "items": { + "$ref": "#/$defs/edge" + }, + "title": "Edges", + "type": "array" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "nodes": { + "items": { + "$ref": "#/$defs/node" + }, + "title": "Nodes", + "type": "array" + }, + "query_language": { + "title": "Query Language", + "type": "string" + }, + "query_language_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Query Language ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "nodes" + ], + "title": "Graph", + "type": "object" + }, + "account": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "is_disabled": { + "title": "Disabled", + "type": "boolean" + }, + "is_locked": { + "title": "Locked", + "type": "boolean" + }, + "is_on_premises_sync_enabled": { + "title": "On-Premises Sync Enabled", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Account", + "type": "object" + }, + "authorization": { + "additionalProperties": false, + "properties": { + "decision": { + "title": "Authorization Decision/Outcome", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "title": "Authorization Result", + "type": "object" + }, + "autonomous_system": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "number" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "number": { + "title": "Number", + "type": "integer" + } + }, + "title": "Autonomous System", + "type": "object" + }, + "classifier_details": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "Classifier Details", + "type": "object" + }, + "session": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "expiration_reason": { + "title": "Expiration Reason", + "type": "string" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_mfa": { + "title": "Multi Factor Authentication", + "type": "boolean" + }, + "is_remote": { + "title": "Remote", + "type": "boolean" + }, + "is_vpn": { + "title": "VPN Session", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "terminal": { + "title": "Terminal", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + } + }, + "title": "Session", + "type": "object" + }, + "d3f_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Technique", + "type": "object" + }, + "image": { + "additionalProperties": false, + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Image", + "type": "object" + }, + "application": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "criticality": { + "title": "Business Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Application Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Application Name", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Application Relationship" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "sbom": { + "$ref": "#/$defs/sbom", + "title": "Related SBOM" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Application Type", + "type": "string" + }, + "uid": { + "title": "Application ID", + "type": "string" + }, + "uid_alt": { + "title": "Application Alternative ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "version": { + "title": "Application Version", + "type": "string" + } + }, + "title": "Application", + "type": "object" + }, + "script": { + "additionalProperties": false, + "properties": { + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "script_content": { + "$ref": "#/$defs/long_string", + "title": "Script Content" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "script_content", + "type_id" + ], + "title": "Script", + "type": "object" + }, + "affected_package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "fixed_in_version": { + "title": "Fixed In Version", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Affected Software Package", + "type": "object" + }, + "cis_control": { + "additionalProperties": false, + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "CIS Control", + "type": "object" + }, + "analytic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Analytic", + "type": "object" + }, + "edge": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "relation": { + "title": "Relation", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "target": { + "title": "Target", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "source", + "target" + ], + "title": "Edge", + "type": "object" + }, + "actor": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "app_name" + ] + }, + { + "required": [ + "app_uid" + ] + }, + { + "required": [ + "invoked_by" + ] + }, + { + "required": [ + "process" + ] + }, + { + "required": [ + "session" + ] + }, + { + "required": [ + "user" + ] + } + ], + "properties": { + "app_name": { + "title": "Application Name", + "type": "string" + }, + "app_uid": { + "title": "Application ID", + "type": "string" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "idp": { + "$ref": "#/$defs/idp", + "title": "Identity Provider" + }, + "invoked_by": { + "title": "Invoked by", + "type": "string" + }, + "process": { + "$ref": "#/$defs/process", + "title": "Process" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + } + }, + "title": "Actor", + "type": "object" + }, + "cloud": { + "additionalProperties": false, + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "cloud_partition": { + "title": "Cloud Partition", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "project_uid": { + "title": "Project ID", + "type": "string" + }, + "provider": { + "title": "Cloud Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + }, + "zone": { + "title": "Cloud Availability Zone", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "Cloud", + "type": "object" + }, + "trace": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "span": { + "$ref": "#/$defs/span", + "title": "Span" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Trace", + "type": "object" + }, + "ai_model": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ai_provider": { + "title": "AI Provider", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "ai_provider" + ], + "title": "AI Model", + "type": "object" + }, + "file": { + "additionalProperties": false, + "properties": { + "parent_folder": { + "title": "Parent Folder", + "type": "string" + }, + "is_system": { + "title": "System", + "type": "boolean" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "accessor": { + "$ref": "#/$defs/user", + "title": "Accessor" + }, + "is_readonly": { + "title": "Read-Only", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "ext": { + "title": "File Extension", + "type": "string" + }, + "company_name": { + "title": "Company Name", + "type": "string" + }, + "attributes": { + "title": "Attributes", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uri": { + "title": "File URI", + "type": "string" + }, + "is_deleted": { + "title": "Deleted", + "type": "boolean" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "drive_type": { + "title": "Drive Type", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "imported_symbols": { + "items": { + "type": "string" + }, + "title": "Imported Symbols", + "type": "array" + }, + "signature": { + "$ref": "#/$defs/digital_signature", + "title": "Digital Signature" + }, + "is_encrypted": { + "title": "Encrypted", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "accessed_time": { + "title": "Accessed Time", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "internal_name": { + "title": "Internal Name", + "type": "string" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "drive_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Drive Type ID", + "type": "integer" + }, + "security_descriptor": { + "title": "Security Descriptor", + "type": "string" + }, + "volume": { + "title": "Volume", + "type": "string" + }, + "modifier": { + "$ref": "#/$defs/user", + "title": "Modifier" + }, + "mime_type": { + "title": "MIME type", + "type": "string" + }, + "encryption_details": { + "$ref": "#/$defs/encryption_details", + "title": "Encryption Details" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Digital Signatures", + "type": "array" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "File", + "type": "object" + }, + "remediation": { + "additionalProperties": false, + "properties": { + "cis_controls": { + "items": { + "$ref": "#/$defs/cis_control" + }, + "title": "CIS Controls", + "type": "array" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + } + }, + "required": [ + "desc" + ], + "title": "Remediation", + "type": "object" + }, + "email": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "from" + ] + }, + { + "required": [ + "to" + ] + } + ], + "properties": { + "cc": { + "items": { + "type": "string" + }, + "title": "Cc", + "type": "array" + }, + "cc_mailboxes": { + "items": { + "type": "string" + }, + "title": "Cc Mailboxes", + "type": "array" + }, + "delivered_to": { + "title": "Delivered To", + "type": "string" + }, + "delivered_to_list": { + "items": { + "type": "string" + }, + "title": "Delivered To List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "from": { + "title": "From", + "type": "string" + }, + "from_list": { + "items": { + "type": "string" + }, + "title": "From List", + "type": "array" + }, + "from_mailbox": { + "title": "From Mailbox", + "type": "string" + }, + "from_mailboxes": { + "items": { + "type": "string" + }, + "title": "From Mailboxes", + "type": "array" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "is_read": { + "title": "Read", + "type": "boolean" + }, + "message_uid": { + "title": "Message UID", + "type": "string" + }, + "raw_header": { + "title": "Raw Header", + "type": "string" + }, + "reply_to": { + "title": "Reply To", + "type": "string" + }, + "reply_to_list": { + "items": { + "type": "string" + }, + "title": "Reply To List", + "type": "array" + }, + "reply_to_mailboxes": { + "items": { + "type": "string" + }, + "title": "Reply To Mailboxes", + "type": "array" + }, + "return_path": { + "title": "Return Path", + "type": "string" + }, + "sender": { + "title": "Sender", + "type": "string" + }, + "sender_mailbox": { + "title": "Sender Mailbox", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "smtp_from": { + "title": "SMTP From", + "type": "string" + }, + "smtp_to": { + "items": { + "type": "string" + }, + "title": "SMTP To", + "type": "array" + }, + "subject": { + "title": "Subject", + "type": "string" + }, + "to": { + "items": { + "type": "string" + }, + "title": "To", + "type": "array" + }, + "to_mailboxes": { + "items": { + "type": "string" + }, + "title": "To Mailboxes", + "type": "array" + }, + "uid": { + "title": "Email Thread UID", + "type": "string" + }, + "urls": { + "items": { + "$ref": "#/$defs/url" + }, + "title": "URLs", + "type": "array" + }, + "x_originating_ip": { + "items": { + "type": "string" + }, + "title": "X-Originating-IP", + "type": "array" + } + }, + "title": "Email", + "type": "object" + }, + "environment_variable": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Environment Variable", + "type": "object" + }, + "technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Technique", + "type": "object" + }, + "http_request": { + "additionalProperties": false, + "properties": { + "args": { + "title": "HTTP Arguments", + "type": "string" + }, + "body_length": { + "title": "Request Body Length", + "type": "integer" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "http_method": { + "enum": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "title": "HTTP Method", + "type": "string" + }, + "length": { + "title": "Request Length", + "type": "integer" + }, + "referrer": { + "title": "HTTP Referrer", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "user_agent": { + "title": "HTTP User-Agent", + "type": "string" + }, + "version": { + "title": "HTTP Version", + "type": "string" + }, + "x_forwarded_for": { + "items": { + "type": "string" + }, + "title": "X-Forwarded-For", + "type": "array" + } + }, + "title": "HTTP Request", + "type": "object" + }, + "cvss": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Base Score", + "type": "number" + }, + "depth": { + "enum": [ + "Base", + "Environmental", + "Temporal" + ], + "title": "CVSS Depth", + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/$defs/metric" + }, + "title": "Metrics", + "type": "array" + }, + "overall_score": { + "title": "Overall Score", + "type": "number" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "vector_string": { + "title": "Vector String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "base_score", + "version" + ], + "title": "CVSS Score", + "type": "object" + }, + "fingerprint": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "algorithm_id", + "value" + ], + "title": "Fingerprint", + "type": "object" + }, + "process_entity": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "path" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Process Entity", + "type": "object" + }, + "observable": { + "additionalProperties": false, + "properties": { + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Observable", + "type": "object" + }, + "http_header": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "HTTP Header", + "type": "object" + }, + "attack": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "sub_technique" + ] + }, + { + "required": [ + "tactic" + ] + }, + { + "required": [ + "technique" + ] + } + ], + "properties": { + "mitigation": { + "$ref": "#/$defs/mitigation", + "title": "MITRE Mitigation" + }, + "sub_technique": { + "$ref": "#/$defs/sub_technique", + "title": "MITRE Sub-technique" + }, + "tactic": { + "$ref": "#/$defs/tactic", + "title": "MITRE Tactic" + }, + "tactics": { + "items": { + "$ref": "#/$defs/tactic" + }, + "title": "Tactics", + "type": "array" + }, + "technique": { + "$ref": "#/$defs/technique", + "title": "MITRE Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "type": "object" + }, + "network_interface": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "mac" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "open_ports": { + "items": { + "$ref": "#/$defs/port_info" + }, + "title": "Open Ports", + "type": "array" + }, + "subnet_prefix": { + "title": "Subnet Prefix Length", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Network Interface", + "type": "object" + }, + "user": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "account" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "forward_addr": { + "title": "Forwarding Address", + "type": "string" + }, + "full_name": { + "title": "Full Name", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "has_mfa": { + "title": "MFA Assigned", + "type": "boolean" + }, + "ldap_person": { + "$ref": "#/$defs/ldap_person", + "title": "LDAP Person" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "programmatic_credentials": { + "items": { + "$ref": "#/$defs/programmatic_credential" + }, + "title": "Programmatic Credentials", + "type": "array" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "User", + "type": "object" + }, + "auth_factor": { + "additionalProperties": false, + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "factor_type": { + "title": "Factor Type", + "type": "string" + }, + "factor_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Factor Type ID", + "type": "integer" + }, + "is_hotp": { + "title": "HMAC-based One-time Password (HOTP)", + "type": "boolean" + }, + "is_totp": { + "title": "Time-based One-time Password (TOTP)", + "type": "boolean" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "security_questions": { + "items": { + "type": "string" + }, + "title": "Security Questions", + "type": "array" + } + }, + "required": [ + "factor_type_id" + ], + "title": "Authentication Factor", + "type": "object" + }, + "osint": { + "additionalProperties": false, + "properties": { + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "email_auth": { + "$ref": "#/$defs/email_auth", + "title": "Related Email Authentication" + }, + "external_uid": { + "title": "External ID", + "type": "string" + }, + "tlp": { + "enum": [ + "AMBER", + "AMBER STRICT", + "CLEAR", + "GREEN", + "RED", + "WHITE" + ], + "title": "Traffic Light Protocol", + "type": "string" + }, + "detection_pattern": { + "title": "Detection Pattern", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Related Subdomains", + "type": "array" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uploaded_time": { + "title": "Uploaded Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "comment": { + "title": "Analyst Comments", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "value": { + "title": "Indicator", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "intrusion_sets": { + "items": { + "type": "string" + }, + "title": "Intrusion Sets", + "type": "array" + }, + "script": { + "$ref": "#/$defs/script", + "title": "Related Script Data" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "Related File" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "email": { + "$ref": "#/$defs/email", + "title": "Related Email" + }, + "kill_chain": { + "items": { + "$ref": "#/$defs/kill_chain_phase" + }, + "title": "Kill Chain", + "type": "array" + }, + "detection_pattern_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Detection Pattern Type ID", + "type": "integer" + }, + "detection_pattern_type": { + "title": "Detection Pattern", + "type": "string" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "threat_actor": { + "$ref": "#/$defs/threat_actor", + "title": "Threat Actor" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 99 + ], + "title": "Indicator Type ID", + "type": "integer" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "answers": { + "items": { + "$ref": "#/$defs/dns_answer" + }, + "title": "Related DNS Answers", + "type": "array" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "campaign": { + "$ref": "#/$defs/campaign", + "title": "Campaign" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "subnet": { + "title": "Related Subnet", + "type": "string" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Related Digital Signatures", + "type": "array" + }, + "whois": { + "$ref": "#/$defs/whois", + "title": "WHOIS" + }, + "vulnerabilities": { + "items": { + "$ref": "#/$defs/vulnerability" + }, + "title": "Related Vulnerabilities", + "type": "array" + } + }, + "required": [ + "type_id", + "value" + ], + "title": "OSINT", + "type": "object" + }, + "san": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "title": "Subject Alternative Name", + "type": "object" + }, + "occurrence_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cell_name" + ] + }, + { + "required": [ + "column_name" + ] + }, + { + "required": [ + "column_number" + ] + }, + { + "required": [ + "end_line" + ] + }, + { + "required": [ + "json_path" + ] + }, + { + "required": [ + "page_number" + ] + }, + { + "required": [ + "record_index_in_array" + ] + }, + { + "required": [ + "row_number" + ] + }, + { + "required": [ + "start_line" + ] + } + ], + "properties": { + "cell_name": { + "title": "Cell Name", + "type": "string" + }, + "column_name": { + "title": "Column Name", + "type": "string" + }, + "column_number": { + "title": "Column Number", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "json_path": { + "title": "JSON Path", + "type": "string" + }, + "page_number": { + "title": "Page Number", + "type": "integer" + }, + "record_index_in_array": { + "title": "Record Index in Array", + "type": "integer" + }, + "row_number": { + "title": "Row Number", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "title": "Occurrence Details", + "type": "object" + }, + "firewall_rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "condition": { + "title": "Condition", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "match_details": { + "items": { + "type": "string" + }, + "title": "Match Details", + "type": "array" + }, + "match_location": { + "title": "Match Location", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "sensitivity": { + "title": "Sensitivity", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Firewall Rule", + "type": "object" + }, + "threat_actor": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Threat Actor Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Threat Actor Type ID", + "type": "integer" + } + }, + "required": [ + "name" + ], + "title": "Threat Actor", + "type": "object" + }, + "package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Package", + "type": "object" + }, + "transformation_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Transformation Info", + "type": "object" + }, + "response": { + "additionalProperties": false, + "properties": { + "code": { + "title": "Response Code", + "type": "integer" + }, + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "error": { + "title": "Error Code", + "type": "string" + }, + "error_message": { + "title": "Error Message", + "type": "string" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "message": { + "title": "Message", + "type": "string" + } + }, + "title": "Response Elements", + "type": "object" + }, + "reporter": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Reporter", + "type": "object" + }, + "url": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "path" + ] + }, + { + "required": [ + "url_string" + ] + } + ], + "properties": { + "categories": { + "items": { + "type": "string" + }, + "title": "Website Categorization", + "type": "array" + }, + "category_ids": { + "items": { + "enum": [ + 0, + 1, + 3, + 4, + 5, + 6, + 7, + 9, + 11, + 14, + 15, + 16, + 17, + 18, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 40, + 43, + 44, + 45, + 46, + 47, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 63, + 64, + 65, + 66, + 67, + 68, + 71, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 92, + 93, + 95, + 96, + 97, + 98, + 99, + 101, + 102, + 103, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 118, + 121 + ], + "type": "integer" + }, + "title": "Website Categorization IDs", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "query_string": { + "title": "HTTP Query String", + "type": "string" + }, + "resource_type": { + "title": "Resource Type", + "type": "string" + }, + "scheme": { + "title": "Scheme", + "type": "string" + }, + "subdomain": { + "title": "Subdomain", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Uniform Resource Locator", + "type": "object" + }, + "object": { + "additionalProperties": true, + "properties": {}, + "title": "Object", + "type": "object" + }, + "extension": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "version" + ], + "title": "Schema Extension", + "type": "object" + }, + "gpu_info": { + "additionalProperties": false, + "properties": { + "bus_type": { + "title": "Bus Type", + "type": "string" + }, + "bus_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Bus Type ID", + "type": "integer" + }, + "cores": { + "title": "Cores", + "type": "integer" + }, + "model": { + "title": "Model", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "vram_mode": { + "title": "VRAM Mode", + "type": "string" + }, + "vram_mode_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "VRAM Mode ID", + "type": "integer" + }, + "vram_size": { + "title": "VRAM Size", + "type": "integer" + } + }, + "title": "GPU Information", + "type": "object" + }, + "port_info": { + "additionalProperties": false, + "properties": { + "port": { + "title": "Port", + "type": "integer" + }, + "protocol_name": { + "title": "Protocol Name", + "type": "string" + }, + "protocol_num": { + "title": "Protocol Number", + "type": "integer" + } + }, + "required": [ + "port" + ], + "title": "Port Information", + "type": "object" + }, + "service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Service", + "type": "object" + }, + "metric": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Metric", + "type": "object" + }, + "request": { + "additionalProperties": false, + "properties": { + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Request Elements", + "type": "object" + }, + "sub_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Sub-technique", + "type": "object" + }, + "container": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_driver": { + "title": "Network Driver", + "type": "string" + }, + "orchestrator": { + "title": "Orchestrator", + "type": "string" + }, + "pod_uuid": { + "title": "Pod UUID", + "type": "string" + }, + "runtime": { + "title": "Runtime", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Container", + "type": "object" + }, + "malware": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "classification_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 99 + ], + "type": "integer" + }, + "title": "Classification IDs", + "type": "array" + }, + "classifications": { + "items": { + "type": "string" + }, + "title": "Classifications", + "type": "array" + }, + "cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "CVE List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "path": { + "title": "Path", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "classification_ids" + ], + "title": "Malware", + "type": "object" + }, + "win_win_service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "service_category_id" + ] + }, + { + "required": [ + "service_dependencies" + ] + }, + { + "required": [ + "service_error_control_id" + ] + }, + { + "required": [ + "service_start_name" + ] + }, + { + "required": [ + "service_start_type_id" + ] + }, + { + "required": [ + "service_type_id" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "hosting_process": { + "$ref": "#/$defs/process_entity", + "title": "Hosting Process" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "load_order_group": { + "title": "Load Order Group", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "service_category": { + "title": "Service Category", + "type": "string" + }, + "service_category_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Service Category ID", + "type": "integer" + }, + "service_dependencies": { + "items": { + "type": "string" + }, + "title": "Service Dependencies", + "type": "array" + }, + "service_dll_file": { + "$ref": "#/$defs/file", + "title": "Service DLL" + }, + "service_error_control": { + "title": "Service Error Control", + "type": "string" + }, + "service_error_control_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Error Control ID", + "type": "integer" + }, + "service_file": { + "$ref": "#/$defs/file", + "title": "Service File" + }, + "service_start_name": { + "title": "Service Start Name", + "type": "string" + }, + "service_start_type": { + "title": "Service Start Type", + "type": "string" + }, + "service_start_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Service Start Type ID", + "type": "integer" + }, + "service_type": { + "title": "Service Type", + "type": "string" + }, + "service_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Type ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Windows Service", + "type": "object" + }, + "group": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "privileges": { + "items": { + "type": "string" + }, + "title": "Privileges", + "type": "array" + }, + "type": { + "title": "Group Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "Group", + "type": "object" + }, + "device": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "region": { + "title": "Region", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "os_machine_uuid": { + "title": "OS Machine UUID", + "type": "string" + }, + "hypervisor": { + "title": "Hypervisor", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "imei_list": { + "items": { + "type": "string" + }, + "title": "IMEI List", + "type": "array" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "is_shared": { + "title": "Shared Device", + "type": "boolean" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "meid": { + "title": "MEID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "is_personal": { + "title": "Personal Device", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "iccid": { + "title": "ICCID", + "type": "string" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "udid": { + "title": "Unique Device Identifier", + "type": "string" + }, + "model": { + "title": "Model", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "boot_time": { + "title": "Boot Time", + "type": "integer" + }, + "is_supervised": { + "title": "Supervised Device", + "type": "boolean" + }, + "is_managed": { + "title": "Managed Device", + "type": "boolean" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "is_trusted": { + "title": "Trusted Device", + "type": "boolean" + }, + "boot_uid": { + "title": "Boot UID", + "type": "string" + }, + "network_interfaces": { + "items": { + "$ref": "#/$defs/network_interface" + }, + "title": "Network Interfaces", + "type": "array" + }, + "autoscale_uid": { + "title": "Autoscale UID", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "subnet": { + "title": "Subnet", + "type": "string" + }, + "eid": { + "title": "EID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "imei": { + "title": "IMEI", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "is_mobile_account_active": { + "title": "Mobile Account Active", + "type": "boolean" + }, + "is_compliant": { + "title": "Compliant Device", + "type": "boolean" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Device", + "type": "object" + }, + "campaign": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Campaign", + "type": "object" + }, + "cwe": { + "additionalProperties": false, + "properties": { + "caption": { + "title": "Caption", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "CWE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CWE", + "type": "object" + }, + "digital_signature": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "developer_uid": { + "title": "Developer UID", + "type": "string" + }, + "digest": { + "$ref": "#/$defs/fingerprint", + "title": "Message Digest" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "State ID", + "type": "integer" + } + }, + "required": [ + "algorithm_id" + ], + "title": "Digital Signature", + "type": "object" + }, + "sbom": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "package": { + "$ref": "#/$defs/package", + "title": "Software Package" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "software_components": { + "items": { + "$ref": "#/$defs/software_component" + }, + "title": "Software Components", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "SBOM ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "package", + "software_components" + ], + "title": "Software Bill of Materials", + "type": "object" + }, + "malware_scan_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "end_time": { + "title": "End Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_files": { + "title": "Scanned Files", + "type": "integer" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "num_volumes": { + "title": "Number of Volumes", + "type": "integer" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Scan UID", + "type": "string" + }, + "unique_malware_count": { + "title": "Unique Malware Count", + "type": "integer" + } + }, + "required": [ + "type_id" + ], + "title": "Malware Scan Info", + "type": "object" + }, + "kill_chain_phase": { + "additionalProperties": false, + "properties": { + "phase": { + "title": "Kill Chain Phase", + "type": "string" + }, + "phase_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Kill Chain Phase ID", + "type": "integer" + } + }, + "required": [ + "phase_id" + ], + "title": "Kill Chain Phase", + "type": "object" + }, + "organization": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "ou_name": { + "title": "Org Unit Name", + "type": "string" + }, + "ou_uid": { + "title": "Org Unit ID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Organization", + "type": "object" + }, + "encryption_details": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Encryption Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Encryption Algorithm ID", + "type": "integer" + }, + "key_length": { + "title": "Encryption Key Length", + "type": "integer" + }, + "key_uid": { + "title": "Key UID", + "type": "string" + }, + "type": { + "title": "Encryption Type", + "type": "string" + } + }, + "title": "Encryption Details", + "type": "object" + }, + "logger": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "log_format": { + "title": "Log Format", + "type": "string" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Logger", + "type": "object" + }, + "feature": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Feature", + "type": "object" + }, + "timespan": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "duration" + ] + }, + { + "required": [ + "duration_days" + ] + }, + { + "required": [ + "duration_hours" + ] + }, + { + "required": [ + "duration_mins" + ] + }, + { + "required": [ + "duration_months" + ] + }, + { + "required": [ + "duration_secs" + ] + }, + { + "required": [ + "duration_weeks" + ] + }, + { + "required": [ + "duration_years" + ] + }, + { + "required": [ + "end_time" + ] + }, + { + "required": [ + "start_time" + ] + } + ], + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "duration_days": { + "title": "Duration Days", + "type": "integer" + }, + "duration_hours": { + "title": "Duration Hours", + "type": "integer" + }, + "duration_mins": { + "title": "Duration Minutes", + "type": "integer" + }, + "duration_months": { + "title": "Duration Months", + "type": "integer" + }, + "duration_secs": { + "title": "Duration Seconds", + "type": "integer" + }, + "duration_weeks": { + "title": "Duration Weeks", + "type": "integer" + }, + "duration_years": { + "title": "Duration Years", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Time Span Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Time Span Type ID", + "type": "integer" + } + }, + "title": "Time Span", + "type": "object" + }, + "rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Rule", + "type": "object" + }, + "software_component": { + "additionalProperties": false, + "properties": { + "author": { + "title": "Author", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "related_component": { + "title": "Related Component", + "type": "string" + }, + "relationship": { + "title": "Relationship", + "type": "string" + }, + "relationship_id": { + "enum": [ + 0, + 1, + 99 + ], + "title": "Relationship ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Component", + "type": "object" + }, + "agent": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Agent Name", + "type": "string" + }, + "policies": { + "items": { + "$ref": "#/$defs/policy" + }, + "title": "Agent Policies", + "type": "array" + }, + "type": { + "title": "Agent Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Agent ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate Agent ID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Agent Version", + "type": "string" + } + }, + "title": "Agent", + "type": "object" + }, + "whois": { + "additionalProperties": false, + "properties": { + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "created_time": { + "title": "Registered At", + "type": "integer" + }, + "dnssec_status": { + "title": "DNSSEC Status", + "type": "string" + }, + "dnssec_status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "DNSSEC Status ID", + "type": "integer" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "domain_contacts": { + "items": { + "$ref": "#/$defs/domain_contact" + }, + "title": "Domain Contacts", + "type": "array" + }, + "email_addr": { + "title": "Registrar Abuse Email Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "last_seen_time": { + "title": "Last Updated At", + "type": "integer" + }, + "name_servers": { + "items": { + "type": "string" + }, + "title": "Name Servers", + "type": "array" + }, + "phone_number": { + "title": "Registrar Abuse Phone Number", + "type": "string" + }, + "registrar": { + "title": "Domain Registrar", + "type": "string" + }, + "status": { + "title": "Domain Status", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Subdomains", + "type": "array" + }, + "subnet": { + "title": "Subnet Block", + "type": "string" + } + }, + "title": "WHOIS", + "type": "object" + }, + "key_value_object": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "value" + ] + }, + { + "required": [ + "values" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + }, + "values": { + "items": { + "type": "string" + }, + "title": "Values", + "type": "array" + } + }, + "required": [ + "name" + ], + "title": "Key:Value object", + "type": "object" + }, + "metadata": { + "additionalProperties": false, + "properties": { + "log_format": { + "title": "Log Source Format", + "type": "string" + }, + "transformation_info_list": { + "items": { + "$ref": "#/$defs/transformation_info" + }, + "title": "Transformation Info", + "type": "array" + }, + "uid": { + "title": "Event UID", + "type": "string" + }, + "original_event_uid": { + "title": "Original Event ID", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "extensions": { + "items": { + "$ref": "#/$defs/extension" + }, + "title": "Schema Extensions", + "type": "array" + }, + "source": { + "title": "Source", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "loggers": { + "items": { + "$ref": "#/$defs/logger" + }, + "title": "Loggers", + "type": "array" + }, + "processed_time": { + "title": "Processed Time", + "type": "integer" + }, + "log_source": { + "title": "Log Source", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "debug": { + "items": { + "type": "string" + }, + "title": "Debug Information", + "type": "array" + }, + "profiles": { + "items": { + "type": "string" + }, + "title": "Profiles", + "type": "array" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "event_code": { + "title": "Event Code", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "reporter": { + "$ref": "#/$defs/reporter", + "title": "Reporter" + }, + "correlation_uid": { + "title": "Correlation UID", + "type": "string" + }, + "extension": { + "$ref": "#/$defs/extension", + "title": "Schema Extension" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "sequence": { + "title": "Sequence Number", + "type": "integer" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "original_time": { + "title": "Original Time", + "type": "string" + }, + "total_queued_duration": { + "$ref": "#/$defs/timespan", + "title": "Total Queued Duration" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "product", + "version" + ], + "title": "Metadata", + "type": "object" + }, + "node": { + "additionalProperties": false, + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Node", + "type": "object" + }, + "product": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "feature": { + "$ref": "#/$defs/feature", + "title": "Feature" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Product", + "type": "object" + }, + "advisory": { + "additionalProperties": false, + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "Related CVEs", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Advisory ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Advisory", + "type": "object" + }, + "network_endpoint": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "title": "Network Endpoint", + "type": "object" + }, + "d3f_tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Tactic", + "type": "object" + }, + "cve": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "cvss": { + "items": { + "$ref": "#/$defs/cvss" + }, + "title": "CVSS Score", + "type": "array" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "cwe_uid": { + "title": "CWE UID", + "type": "string" + }, + "cwe_url": { + "title": "CWE URL", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "epss": { + "$ref": "#/$defs/epss", + "title": "EPSS" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "title": { + "title": "Title", + "type": "string" + }, + "type": { + "title": "Vulnerability Type", + "type": "string" + }, + "uid": { + "title": "CVE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CVE", + "type": "object" + }, + "domain_contact": { + "additionalProperties": false, + "properties": { + "email_addr": { + "title": "Contact Email", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Contact Location Information" + }, + "name": { + "title": "Name", + "type": "string" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "type": { + "title": "Domain Contact Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Domain Contact Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Domain Contact", + "type": "object" + }, + "policy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "type" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "is_applied": { + "title": "Applied", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Policy", + "type": "object" + }, + "span": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "end_time", + "start_time", + "uid" + ], + "title": "Span", + "type": "object" + } + }, + "$id": "https://schema.ocsf.io/schema/classes/api_activity", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "unmapped": { + "$ref": "#/$defs/object", + "title": "Unmapped Data" + }, + "message": { + "title": "Message", + "type": "string" + }, + "http_response": { + "$ref": "#/$defs/http_response", + "title": "HTTP Response" + }, + "disposition": { + "title": "Disposition", + "type": "string" + }, + "category_uid": { + "const": 6, + "title": "Category ID", + "type": "integer" + }, + "action": { + "title": "Action", + "type": "string" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "api": { + "$ref": "#/$defs/api", + "title": "API Details" + }, + "message_context": { + "$ref": "#/$defs/message_context", + "title": "Message Context" + }, + "risk_details": { + "title": "Risk Details", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "status_detail": { + "title": "Status Detail", + "type": "string" + }, + "confidence_score": { + "title": "Confidence Score", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "disposition_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 99 + ], + "title": "Disposition ID", + "type": "integer" + }, + "type_name": { + "title": "Type Name", + "type": "string" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "raw_data": { + "title": "Raw Data", + "type": "string" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "actor": { + "$ref": "#/$defs/actor", + "title": "Actor" + }, + "trace": { + "$ref": "#/$defs/trace", + "title": "Trace" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "status": { + "title": "Status", + "type": "string" + }, + "ai_model": { + "$ref": "#/$defs/ai_model", + "title": "AI Model" + }, + "activity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Activity ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "resources": { + "items": { + "$ref": "#/$defs/resource_details" + }, + "title": "Resources Array", + "type": "array" + }, + "observables": { + "items": { + "$ref": "#/$defs/observable" + }, + "title": "Observables", + "type": "array" + }, + "enrichments": { + "items": { + "$ref": "#/$defs/enrichment" + }, + "title": "Enrichments", + "type": "array" + }, + "http_request": { + "$ref": "#/$defs/http_request", + "title": "HTTP Request" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "raw_data_hash": { + "$ref": "#/$defs/fingerprint", + "title": "Raw Data Hash" + }, + "action_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Action ID", + "type": "integer" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "firewall_rule": { + "$ref": "#/$defs/firewall_rule", + "title": "Firewall Rule" + }, + "class_uid": { + "const": 6003, + "title": "Class ID", + "type": "integer" + }, + "activity_name": { + "title": "Activity", + "type": "string" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "class_name": { + "title": "Class", + "type": "string" + }, + "timezone_offset": { + "title": "Timezone Offset", + "type": "integer" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "raw_data_size": { + "title": "Raw Data Size", + "type": "integer" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "dst_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Destination Endpoint" + }, + "is_alert": { + "title": "Alert", + "type": "boolean" + }, + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "category_name": { + "title": "Category", + "type": "string" + }, + "malware_scan_info": { + "$ref": "#/$defs/malware_scan_info", + "title": "Malware Scan Info" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "count": { + "title": "Count", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "src_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Source Endpoint" + }, + "metadata": { + "$ref": "#/$defs/metadata", + "title": "Metadata" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "required": [ + "activity_id", + "actor", + "api", + "category_uid", + "class_uid", + "metadata", + "severity_id", + "src_endpoint", + "time", + "type_uid" + ], + "title": "API Activity", + "type": "object" +} diff --git a/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+container+host+trace.json b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+container+host+trace.json new file mode 100644 index 0000000000..a0a313d187 --- /dev/null +++ b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+container+host+trace.json @@ -0,0 +1,8138 @@ +{ + "$defs": { + "kb_article": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "src_url" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "KB Article", + "type": "object" + }, + "long_string": { + "additionalProperties": false, + "properties": { + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "value" + ], + "title": "Long String", + "type": "object" + }, + "enrichment": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "short_desc": { + "title": "Short Description", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "data", + "name", + "value" + ], + "title": "Enrichment", + "type": "object" + }, + "ldap_person": { + "additionalProperties": false, + "properties": { + "cost_center": { + "title": "Cost Center", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "deleted_time": { + "title": "Deleted Time", + "type": "integer" + }, + "department": { + "title": "Department", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "email_addrs": { + "items": { + "type": "string" + }, + "title": "Email Addresses", + "type": "array" + }, + "employee_uid": { + "title": "Employee ID", + "type": "string" + }, + "given_name": { + "title": "Given Name", + "type": "string" + }, + "hire_time": { + "title": "Hire Time", + "type": "integer" + }, + "job_title": { + "title": "Job Title", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "last_login_time": { + "title": "Last Login", + "type": "integer" + }, + "ldap_cn": { + "title": "LDAP Common Name", + "type": "string" + }, + "ldap_dn": { + "title": "LDAP Distinguished Name", + "type": "string" + }, + "leave_time": { + "title": "Leave Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "manager": { + "$ref": "#/$defs/user", + "title": "Manager" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "office_location": { + "title": "Office Location", + "type": "string" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "surname": { + "title": "Surname", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + } + }, + "title": "LDAP Person", + "type": "object" + }, + "email_auth": { + "additionalProperties": false, + "properties": { + "dkim": { + "title": "DKIM Status", + "type": "string" + }, + "dkim_domain": { + "title": "DKIM Domain", + "type": "string" + }, + "dkim_signature": { + "title": "DKIM Signature", + "type": "string" + }, + "dmarc": { + "title": "DMARC Status", + "type": "string" + }, + "dmarc_override": { + "title": "DMARC Override", + "type": "string" + }, + "dmarc_policy": { + "title": "DMARC Policy", + "type": "string" + }, + "spf": { + "title": "SPF Status", + "type": "string" + } + }, + "title": "Email Authentication", + "type": "object" + }, + "epss": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "percentile": { + "title": "EPSS Percentile", + "type": "number" + }, + "score": { + "title": "EPPS Score", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "score" + ], + "title": "EPSS", + "type": "object" + }, + "device_hw_info": { + "additionalProperties": false, + "properties": { + "bios_date": { + "title": "BIOS Date", + "type": "string" + }, + "bios_manufacturer": { + "title": "BIOS Manufacturer", + "type": "string" + }, + "bios_ver": { + "title": "BIOS Version", + "type": "string" + }, + "chassis": { + "title": "Chassis", + "type": "string" + }, + "cpu_architecture": { + "title": "CPU Architecture", + "type": "string" + }, + "cpu_architecture_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "CPU Architecture ID", + "type": "integer" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "cpu_cores": { + "title": "CPU Cores", + "type": "integer" + }, + "cpu_count": { + "title": "CPU Count", + "type": "integer" + }, + "cpu_speed": { + "title": "Processor Speed", + "type": "integer" + }, + "cpu_type": { + "title": "Processor Type", + "type": "string" + }, + "desktop_display": { + "$ref": "#/$defs/display", + "title": "Desktop Display" + }, + "gpu_count": { + "title": "GPU Count", + "type": "integer" + }, + "gpu_info_list": { + "items": { + "$ref": "#/$defs/gpu_info" + }, + "title": "GPU information", + "type": "array" + }, + "keyboard_info": { + "$ref": "#/$defs/keyboard_info", + "title": "Keyboard Information" + }, + "ram_size": { + "title": "RAM Size", + "type": "integer" + }, + "serial_number": { + "title": "Serial Number", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Device Hardware Info", + "type": "object" + }, + "network_proxy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "title": "Network Proxy Endpoint", + "type": "object" + }, + "data_classification": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "category_id" + ] + }, + { + "required": [ + "confidentiality_id" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "category_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Category ID", + "type": "integer" + }, + "classifier_details": { + "$ref": "#/$defs/classifier_details", + "title": "Classifier Details" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "discovery_details": { + "items": { + "$ref": "#/$defs/discovery_details" + }, + "title": "Discovery Details", + "type": "array" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "status_details": { + "items": { + "type": "string" + }, + "title": "Status Details", + "type": "array" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Data Classification", + "type": "object" + }, + "http_response": { + "additionalProperties": false, + "properties": { + "body_length": { + "title": "Response Body Length", + "type": "integer" + }, + "code": { + "title": "Response Code", + "type": "integer" + }, + "content_type": { + "title": "HTTP Content Type", + "type": "string" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "latency": { + "title": "Latency", + "type": "integer" + }, + "length": { + "title": "Response Length", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "code" + ], + "title": "HTTP Response", + "type": "object" + }, + "resource_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "criticality": { + "title": "Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Resource Relationship" + }, + "role": { + "title": "Role", + "type": "string" + }, + "role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Role ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Resource Details", + "type": "object" + }, + "display": { + "additionalProperties": false, + "properties": { + "color_depth": { + "title": "Color Depth", + "type": "integer" + }, + "physical_height": { + "title": "Physical Height", + "type": "integer" + }, + "physical_orientation": { + "title": "Physical Orientation", + "type": "integer" + }, + "physical_width": { + "title": "Physical Width", + "type": "integer" + }, + "scale_factor": { + "title": "Scale Factor", + "type": "integer" + } + }, + "title": "Display", + "type": "object" + }, + "scim": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "error_message": { + "title": "Last Error Message", + "type": "string" + }, + "is_group_provisioning_enabled": { + "title": "SCIM Group Provisioning Enabled", + "type": "boolean" + }, + "is_user_provisioning_enabled": { + "title": "SCIM User Provisioning Enabled", + "type": "boolean" + }, + "last_run_time": { + "title": "Last Sync Time", + "type": "integer" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "scim_group_schema": { + "title": "SCIM Group Schema" + }, + "scim_user_schema": { + "title": "SCIM User Schema" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "External ID", + "type": "string" + }, + "url_string": { + "title": "SCIM Endpoint URL", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + }, + "version": { + "title": "SCIM Version", + "type": "string" + } + }, + "title": "SCIM", + "type": "object" + }, + "programmatic_credential": { + "additionalProperties": false, + "properties": { + "last_used_time": { + "title": "Last Used Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Programmatic Credential", + "type": "object" + }, + "os": { + "additionalProperties": false, + "properties": { + "build": { + "title": "OS Build", + "type": "string" + }, + "country": { + "title": "Country", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "edition": { + "title": "OS Edition", + "type": "string" + }, + "kernel_release": { + "title": "Kernel Release", + "type": "string" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "sp_name": { + "title": "OS Service Pack", + "type": "string" + }, + "sp_ver": { + "title": "OS Service Pack Version", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 99, + 100, + 101, + 200, + 201, + 300, + 301, + 302, + 400, + 401, + 402 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "Operating System (OS)", + "type": "object" + }, + "tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Tactic", + "type": "object" + }, + "dns_answer": { + "additionalProperties": false, + "properties": { + "class": { + "title": "Resource Record Class", + "type": "string" + }, + "flag_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "type": "integer" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "packet_uid": { + "title": "Packet UID", + "type": "integer" + }, + "rdata": { + "title": "DNS RData", + "type": "string" + }, + "ttl": { + "title": "TTL", + "type": "integer" + }, + "type": { + "title": "Resource Record Type", + "type": "string" + } + }, + "required": [ + "rdata" + ], + "title": "DNS Answer", + "type": "object" + }, + "discovery_details": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "occurrence_details": { + "$ref": "#/$defs/occurrence_details", + "title": "Occurrence Details" + }, + "occurrences": { + "items": { + "$ref": "#/$defs/occurrence_details" + }, + "title": "Occurrences", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "title": "Discovery Details", + "type": "object" + }, + "sso": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "SAML Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "duration_mins": { + "title": "SSO Session Duration", + "type": "integer" + }, + "idle_timeout": { + "title": "SSO Idle Timeout", + "type": "integer" + }, + "login_endpoint": { + "title": "SSO Login Endpoint", + "type": "string" + }, + "logout_endpoint": { + "title": "SSO Logout Endpoint", + "type": "string" + }, + "metadata_endpoint": { + "title": "SSO Metadata Endpoint", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "title": "Scopes", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + } + }, + "title": "SSO", + "type": "object" + }, + "token": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_renewable": { + "title": "Renewable", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Token", + "type": "object" + }, + "api": { + "additionalProperties": false, + "properties": { + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "request": { + "$ref": "#/$defs/request", + "title": "API Request Details" + }, + "response": { + "$ref": "#/$defs/response", + "title": "API Response Details" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "token": { + "$ref": "#/$defs/token", + "title": "Token" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "operation" + ], + "title": "API", + "type": "object" + }, + "d3fend": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "d3f_tactic" + ] + }, + { + "required": [ + "d3f_technique" + ] + } + ], + "properties": { + "d3f_tactic": { + "$ref": "#/$defs/d3f_tactic", + "title": "MITRE D3FEND\u2122 Tactic" + }, + "d3f_technique": { + "$ref": "#/$defs/d3f_technique", + "title": "MITRE D3FEND\u2122 Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122", + "type": "object" + }, + "reputation": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Reputation Score", + "type": "number" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "score": { + "title": "Reputation Score", + "type": "string" + }, + "score_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "Reputation Score ID", + "type": "integer" + } + }, + "required": [ + "base_score", + "score_id" + ], + "title": "Reputation", + "type": "object" + }, + "mitigation": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "countermeasures": { + "items": { + "$ref": "#/$defs/d3fend" + }, + "title": "Countermeasures", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Mitigation", + "type": "object" + }, + "location": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "city" + ] + }, + { + "required": [ + "country" + ] + }, + { + "required": [ + "postal_code" + ] + }, + { + "required": [ + "region" + ] + } + ], + "properties": { + "aerial_height": { + "title": "Aerial Height", + "type": "string" + }, + "city": { + "title": "City", + "type": "string" + }, + "continent": { + "title": "Continent", + "type": "string" + }, + "coordinates": { + "items": { + "type": "number" + }, + "title": "Coordinates", + "type": "array" + }, + "country": { + "title": "Country", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "geodetic_altitude": { + "title": "Geodetic Altitude", + "type": "string" + }, + "geodetic_vertical_accuracy": { + "title": "Geodetic Vertical Accuracy", + "type": "string" + }, + "geohash": { + "title": "Geohash", + "type": "string" + }, + "horizontal_accuracy": { + "title": "Horizontal Accuracy", + "type": "string" + }, + "is_on_premises": { + "title": "On-Premises", + "type": "boolean" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "lat": { + "title": "Latitude", + "type": "number" + }, + "long": { + "title": "Longitude", + "type": "number" + }, + "postal_code": { + "title": "Postal Code", + "type": "string" + }, + "pressure_altitude": { + "title": "Pressure Altitude", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + } + }, + "title": "Geo Location", + "type": "object" + }, + "idp": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "auth_factors": { + "items": { + "$ref": "#/$defs/auth_factor" + }, + "title": "Authentication Factors", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprint": { + "$ref": "#/$defs/fingerprint", + "title": "Certificate Fingerprint" + }, + "has_mfa": { + "title": "MFA Enforced", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scim": { + "$ref": "#/$defs/scim", + "title": "SCIM" + }, + "sso": { + "$ref": "#/$defs/sso", + "title": "SSO" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "Configuration URL", + "type": "string" + } + }, + "title": "Identity Provider", + "type": "object" + }, + "message_context": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "application" + ] + }, + { + "required": [ + "service" + ] + } + ], + "properties": { + "ai_role": { + "title": "AI Role", + "type": "string" + }, + "ai_role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "AI Role ID", + "type": "integer" + }, + "application": { + "$ref": "#/$defs/application", + "title": "Application" + }, + "completion_tokens": { + "title": "Completion Tokens", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "prompt_tokens": { + "title": "Prompt Tokens", + "type": "integer" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "total_tokens": { + "title": "Total Tokens", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Message Context", + "type": "object" + }, + "process": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ancestry": { + "items": { + "$ref": "#/$defs/process_entity" + }, + "title": "Ancestry", + "type": "array" + }, + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "environment_variables": { + "items": { + "$ref": "#/$defs/environment_variable" + }, + "title": "Environment Variables", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hosted_services": { + "items": { + "$ref": "#/$defs/win_win_service" + }, + "title": "Hosted Services", + "type": "array" + }, + "integrity": { + "title": "Integrity", + "type": "string" + }, + "integrity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Integrity Level", + "type": "integer" + }, + "lineage": { + "items": { + "type": "string" + }, + "title": "Lineage", + "type": "array" + }, + "loaded_modules": { + "items": { + "type": "string" + }, + "title": "Loaded Modules", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "parent_process": { + "$ref": "#/$defs/process", + "title": "Parent Process" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "ptid": { + "title": "Process Thread ID", + "type": "integer" + }, + "sandbox": { + "title": "Sandbox", + "type": "string" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "terminated_time": { + "title": "Terminated Time", + "type": "integer" + }, + "tid": { + "title": "Thread ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + }, + "working_directory": { + "title": "Working Directory", + "type": "string" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + } + }, + "title": "Process", + "type": "object" + }, + "vulnerability": { + "additionalProperties": false, + "oneOf": [ + { + "not": { + "required": [ + "cve", + "cwe" + ] + }, + "required": [ + "advisory" + ] + }, + { + "not": { + "required": [ + "advisory", + "cwe" + ] + }, + "required": [ + "cve" + ] + }, + { + "not": { + "required": [ + "advisory", + "cve" + ] + }, + "required": [ + "cwe" + ] + } + ], + "properties": { + "advisory": { + "$ref": "#/$defs/advisory", + "title": "Security Advisory" + }, + "affected_code": { + "items": { + "$ref": "#/$defs/affected_code" + }, + "title": "Affected Code", + "type": "array" + }, + "affected_packages": { + "items": { + "$ref": "#/$defs/affected_package" + }, + "title": "Affected Software Packages", + "type": "array" + }, + "category": { + "title": "Category", + "type": "string" + }, + "cve": { + "$ref": "#/$defs/cve", + "title": "CVE" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "dependency_chain": { + "title": "Dependency Chain", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "exploit_last_seen_time": { + "title": "Exploit Last Seen Time", + "type": "integer" + }, + "exploit_ref_url": { + "title": "Exploit URL", + "type": "string" + }, + "exploit_requirement": { + "title": "Exploit Requirement", + "type": "string" + }, + "exploit_type": { + "title": "Exploit Type", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "fix_coverage": { + "title": "Fix Coverage", + "type": "string" + }, + "fix_coverage_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Fix Coverage ID", + "type": "integer" + }, + "is_exploit_available": { + "title": "Exploit Availability", + "type": "boolean" + }, + "is_fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "packages": { + "items": { + "$ref": "#/$defs/package" + }, + "title": "Software Packages", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_vulnerabilities": { + "items": { + "type": "string" + }, + "title": "Related Vulnerability IDs", + "type": "array" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Vulnerability Details", + "type": "object" + }, + "affected_code": { + "additionalProperties": false, + "properties": { + "end_column": { + "title": "End Column", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "rule": { + "$ref": "#/$defs/rule", + "title": "Related Rule" + }, + "start_column": { + "title": "Start Column", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "required": [ + "file" + ], + "title": "Affected Code", + "type": "object" + }, + "keyboard_info": { + "additionalProperties": false, + "properties": { + "function_keys": { + "title": "Function Keys", + "type": "integer" + }, + "ime": { + "title": "IME", + "type": "string" + }, + "keyboard_layout": { + "title": "Keyboard Layout", + "type": "string" + }, + "keyboard_subtype": { + "title": "Keyboard Subtype", + "type": "integer" + }, + "keyboard_type": { + "title": "Keyboard Type", + "type": "string" + } + }, + "title": "Keyboard Information", + "type": "object" + }, + "certificate": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "is_self_signed": { + "title": "Certificate Self-Signed", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Distinguished Name", + "type": "string" + }, + "sans": { + "items": { + "$ref": "#/$defs/san" + }, + "title": "Subject Alternative Names", + "type": "array" + }, + "serial_number": { + "title": "Certificate Serial Number", + "type": "string" + }, + "subject": { + "title": "Subject Distinguished Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "issuer", + "serial_number" + ], + "title": "Digital Certificate", + "type": "object" + }, + "graph": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "edges": { + "items": { + "$ref": "#/$defs/edge" + }, + "title": "Edges", + "type": "array" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "nodes": { + "items": { + "$ref": "#/$defs/node" + }, + "title": "Nodes", + "type": "array" + }, + "query_language": { + "title": "Query Language", + "type": "string" + }, + "query_language_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Query Language ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "nodes" + ], + "title": "Graph", + "type": "object" + }, + "account": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "is_disabled": { + "title": "Disabled", + "type": "boolean" + }, + "is_locked": { + "title": "Locked", + "type": "boolean" + }, + "is_on_premises_sync_enabled": { + "title": "On-Premises Sync Enabled", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Account", + "type": "object" + }, + "authorization": { + "additionalProperties": false, + "properties": { + "decision": { + "title": "Authorization Decision/Outcome", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "title": "Authorization Result", + "type": "object" + }, + "autonomous_system": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "number" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "number": { + "title": "Number", + "type": "integer" + } + }, + "title": "Autonomous System", + "type": "object" + }, + "classifier_details": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "Classifier Details", + "type": "object" + }, + "session": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "expiration_reason": { + "title": "Expiration Reason", + "type": "string" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_mfa": { + "title": "Multi Factor Authentication", + "type": "boolean" + }, + "is_remote": { + "title": "Remote", + "type": "boolean" + }, + "is_vpn": { + "title": "VPN Session", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "terminal": { + "title": "Terminal", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + } + }, + "title": "Session", + "type": "object" + }, + "d3f_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Technique", + "type": "object" + }, + "image": { + "additionalProperties": false, + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Image", + "type": "object" + }, + "application": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "criticality": { + "title": "Business Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Application Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Application Name", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Application Relationship" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "sbom": { + "$ref": "#/$defs/sbom", + "title": "Related SBOM" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Application Type", + "type": "string" + }, + "uid": { + "title": "Application ID", + "type": "string" + }, + "uid_alt": { + "title": "Application Alternative ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "version": { + "title": "Application Version", + "type": "string" + } + }, + "title": "Application", + "type": "object" + }, + "script": { + "additionalProperties": false, + "properties": { + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "script_content": { + "$ref": "#/$defs/long_string", + "title": "Script Content" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "script_content", + "type_id" + ], + "title": "Script", + "type": "object" + }, + "affected_package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "fixed_in_version": { + "title": "Fixed In Version", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Affected Software Package", + "type": "object" + }, + "cis_control": { + "additionalProperties": false, + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "CIS Control", + "type": "object" + }, + "analytic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Analytic", + "type": "object" + }, + "edge": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "relation": { + "title": "Relation", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "target": { + "title": "Target", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "source", + "target" + ], + "title": "Edge", + "type": "object" + }, + "actor": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "app_name" + ] + }, + { + "required": [ + "app_uid" + ] + }, + { + "required": [ + "invoked_by" + ] + }, + { + "required": [ + "process" + ] + }, + { + "required": [ + "session" + ] + }, + { + "required": [ + "user" + ] + } + ], + "properties": { + "app_name": { + "title": "Application Name", + "type": "string" + }, + "app_uid": { + "title": "Application ID", + "type": "string" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "idp": { + "$ref": "#/$defs/idp", + "title": "Identity Provider" + }, + "invoked_by": { + "title": "Invoked by", + "type": "string" + }, + "process": { + "$ref": "#/$defs/process", + "title": "Process" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + } + }, + "title": "Actor", + "type": "object" + }, + "cloud": { + "additionalProperties": false, + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "cloud_partition": { + "title": "Cloud Partition", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "project_uid": { + "title": "Project ID", + "type": "string" + }, + "provider": { + "title": "Cloud Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + }, + "zone": { + "title": "Cloud Availability Zone", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "Cloud", + "type": "object" + }, + "trace": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "span": { + "$ref": "#/$defs/span", + "title": "Span" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Trace", + "type": "object" + }, + "ai_model": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ai_provider": { + "title": "AI Provider", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "ai_provider" + ], + "title": "AI Model", + "type": "object" + }, + "file": { + "additionalProperties": false, + "properties": { + "parent_folder": { + "title": "Parent Folder", + "type": "string" + }, + "is_system": { + "title": "System", + "type": "boolean" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "accessor": { + "$ref": "#/$defs/user", + "title": "Accessor" + }, + "is_readonly": { + "title": "Read-Only", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "ext": { + "title": "File Extension", + "type": "string" + }, + "company_name": { + "title": "Company Name", + "type": "string" + }, + "attributes": { + "title": "Attributes", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uri": { + "title": "File URI", + "type": "string" + }, + "is_deleted": { + "title": "Deleted", + "type": "boolean" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "drive_type": { + "title": "Drive Type", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "imported_symbols": { + "items": { + "type": "string" + }, + "title": "Imported Symbols", + "type": "array" + }, + "signature": { + "$ref": "#/$defs/digital_signature", + "title": "Digital Signature" + }, + "is_encrypted": { + "title": "Encrypted", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "accessed_time": { + "title": "Accessed Time", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "internal_name": { + "title": "Internal Name", + "type": "string" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "drive_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Drive Type ID", + "type": "integer" + }, + "security_descriptor": { + "title": "Security Descriptor", + "type": "string" + }, + "volume": { + "title": "Volume", + "type": "string" + }, + "modifier": { + "$ref": "#/$defs/user", + "title": "Modifier" + }, + "mime_type": { + "title": "MIME type", + "type": "string" + }, + "encryption_details": { + "$ref": "#/$defs/encryption_details", + "title": "Encryption Details" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Digital Signatures", + "type": "array" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "File", + "type": "object" + }, + "remediation": { + "additionalProperties": false, + "properties": { + "cis_controls": { + "items": { + "$ref": "#/$defs/cis_control" + }, + "title": "CIS Controls", + "type": "array" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + } + }, + "required": [ + "desc" + ], + "title": "Remediation", + "type": "object" + }, + "email": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "from" + ] + }, + { + "required": [ + "to" + ] + } + ], + "properties": { + "cc": { + "items": { + "type": "string" + }, + "title": "Cc", + "type": "array" + }, + "cc_mailboxes": { + "items": { + "type": "string" + }, + "title": "Cc Mailboxes", + "type": "array" + }, + "delivered_to": { + "title": "Delivered To", + "type": "string" + }, + "delivered_to_list": { + "items": { + "type": "string" + }, + "title": "Delivered To List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "from": { + "title": "From", + "type": "string" + }, + "from_list": { + "items": { + "type": "string" + }, + "title": "From List", + "type": "array" + }, + "from_mailbox": { + "title": "From Mailbox", + "type": "string" + }, + "from_mailboxes": { + "items": { + "type": "string" + }, + "title": "From Mailboxes", + "type": "array" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "is_read": { + "title": "Read", + "type": "boolean" + }, + "message_uid": { + "title": "Message UID", + "type": "string" + }, + "raw_header": { + "title": "Raw Header", + "type": "string" + }, + "reply_to": { + "title": "Reply To", + "type": "string" + }, + "reply_to_list": { + "items": { + "type": "string" + }, + "title": "Reply To List", + "type": "array" + }, + "reply_to_mailboxes": { + "items": { + "type": "string" + }, + "title": "Reply To Mailboxes", + "type": "array" + }, + "return_path": { + "title": "Return Path", + "type": "string" + }, + "sender": { + "title": "Sender", + "type": "string" + }, + "sender_mailbox": { + "title": "Sender Mailbox", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "smtp_from": { + "title": "SMTP From", + "type": "string" + }, + "smtp_to": { + "items": { + "type": "string" + }, + "title": "SMTP To", + "type": "array" + }, + "subject": { + "title": "Subject", + "type": "string" + }, + "to": { + "items": { + "type": "string" + }, + "title": "To", + "type": "array" + }, + "to_mailboxes": { + "items": { + "type": "string" + }, + "title": "To Mailboxes", + "type": "array" + }, + "uid": { + "title": "Email Thread UID", + "type": "string" + }, + "urls": { + "items": { + "$ref": "#/$defs/url" + }, + "title": "URLs", + "type": "array" + }, + "x_originating_ip": { + "items": { + "type": "string" + }, + "title": "X-Originating-IP", + "type": "array" + } + }, + "title": "Email", + "type": "object" + }, + "environment_variable": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Environment Variable", + "type": "object" + }, + "technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Technique", + "type": "object" + }, + "http_request": { + "additionalProperties": false, + "properties": { + "args": { + "title": "HTTP Arguments", + "type": "string" + }, + "body_length": { + "title": "Request Body Length", + "type": "integer" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "http_method": { + "enum": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "title": "HTTP Method", + "type": "string" + }, + "length": { + "title": "Request Length", + "type": "integer" + }, + "referrer": { + "title": "HTTP Referrer", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "user_agent": { + "title": "HTTP User-Agent", + "type": "string" + }, + "version": { + "title": "HTTP Version", + "type": "string" + }, + "x_forwarded_for": { + "items": { + "type": "string" + }, + "title": "X-Forwarded-For", + "type": "array" + } + }, + "title": "HTTP Request", + "type": "object" + }, + "cvss": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Base Score", + "type": "number" + }, + "depth": { + "enum": [ + "Base", + "Environmental", + "Temporal" + ], + "title": "CVSS Depth", + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/$defs/metric" + }, + "title": "Metrics", + "type": "array" + }, + "overall_score": { + "title": "Overall Score", + "type": "number" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "vector_string": { + "title": "Vector String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "base_score", + "version" + ], + "title": "CVSS Score", + "type": "object" + }, + "fingerprint": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "algorithm_id", + "value" + ], + "title": "Fingerprint", + "type": "object" + }, + "process_entity": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "path" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Process Entity", + "type": "object" + }, + "observable": { + "additionalProperties": false, + "properties": { + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Observable", + "type": "object" + }, + "http_header": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "HTTP Header", + "type": "object" + }, + "attack": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "sub_technique" + ] + }, + { + "required": [ + "tactic" + ] + }, + { + "required": [ + "technique" + ] + } + ], + "properties": { + "mitigation": { + "$ref": "#/$defs/mitigation", + "title": "MITRE Mitigation" + }, + "sub_technique": { + "$ref": "#/$defs/sub_technique", + "title": "MITRE Sub-technique" + }, + "tactic": { + "$ref": "#/$defs/tactic", + "title": "MITRE Tactic" + }, + "tactics": { + "items": { + "$ref": "#/$defs/tactic" + }, + "title": "Tactics", + "type": "array" + }, + "technique": { + "$ref": "#/$defs/technique", + "title": "MITRE Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "type": "object" + }, + "network_interface": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "mac" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "open_ports": { + "items": { + "$ref": "#/$defs/port_info" + }, + "title": "Open Ports", + "type": "array" + }, + "subnet_prefix": { + "title": "Subnet Prefix Length", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Network Interface", + "type": "object" + }, + "user": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "account" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "forward_addr": { + "title": "Forwarding Address", + "type": "string" + }, + "full_name": { + "title": "Full Name", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "has_mfa": { + "title": "MFA Assigned", + "type": "boolean" + }, + "ldap_person": { + "$ref": "#/$defs/ldap_person", + "title": "LDAP Person" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "programmatic_credentials": { + "items": { + "$ref": "#/$defs/programmatic_credential" + }, + "title": "Programmatic Credentials", + "type": "array" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "User", + "type": "object" + }, + "auth_factor": { + "additionalProperties": false, + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "factor_type": { + "title": "Factor Type", + "type": "string" + }, + "factor_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Factor Type ID", + "type": "integer" + }, + "is_hotp": { + "title": "HMAC-based One-time Password (HOTP)", + "type": "boolean" + }, + "is_totp": { + "title": "Time-based One-time Password (TOTP)", + "type": "boolean" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "security_questions": { + "items": { + "type": "string" + }, + "title": "Security Questions", + "type": "array" + } + }, + "required": [ + "factor_type_id" + ], + "title": "Authentication Factor", + "type": "object" + }, + "osint": { + "additionalProperties": false, + "properties": { + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "email_auth": { + "$ref": "#/$defs/email_auth", + "title": "Related Email Authentication" + }, + "external_uid": { + "title": "External ID", + "type": "string" + }, + "tlp": { + "enum": [ + "AMBER", + "AMBER STRICT", + "CLEAR", + "GREEN", + "RED", + "WHITE" + ], + "title": "Traffic Light Protocol", + "type": "string" + }, + "detection_pattern": { + "title": "Detection Pattern", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Related Subdomains", + "type": "array" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uploaded_time": { + "title": "Uploaded Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "comment": { + "title": "Analyst Comments", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "value": { + "title": "Indicator", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "intrusion_sets": { + "items": { + "type": "string" + }, + "title": "Intrusion Sets", + "type": "array" + }, + "script": { + "$ref": "#/$defs/script", + "title": "Related Script Data" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "Related File" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "email": { + "$ref": "#/$defs/email", + "title": "Related Email" + }, + "kill_chain": { + "items": { + "$ref": "#/$defs/kill_chain_phase" + }, + "title": "Kill Chain", + "type": "array" + }, + "detection_pattern_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Detection Pattern Type ID", + "type": "integer" + }, + "detection_pattern_type": { + "title": "Detection Pattern", + "type": "string" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "threat_actor": { + "$ref": "#/$defs/threat_actor", + "title": "Threat Actor" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 99 + ], + "title": "Indicator Type ID", + "type": "integer" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "answers": { + "items": { + "$ref": "#/$defs/dns_answer" + }, + "title": "Related DNS Answers", + "type": "array" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "campaign": { + "$ref": "#/$defs/campaign", + "title": "Campaign" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "subnet": { + "title": "Related Subnet", + "type": "string" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Related Digital Signatures", + "type": "array" + }, + "whois": { + "$ref": "#/$defs/whois", + "title": "WHOIS" + }, + "vulnerabilities": { + "items": { + "$ref": "#/$defs/vulnerability" + }, + "title": "Related Vulnerabilities", + "type": "array" + } + }, + "required": [ + "type_id", + "value" + ], + "title": "OSINT", + "type": "object" + }, + "san": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "title": "Subject Alternative Name", + "type": "object" + }, + "occurrence_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cell_name" + ] + }, + { + "required": [ + "column_name" + ] + }, + { + "required": [ + "column_number" + ] + }, + { + "required": [ + "end_line" + ] + }, + { + "required": [ + "json_path" + ] + }, + { + "required": [ + "page_number" + ] + }, + { + "required": [ + "record_index_in_array" + ] + }, + { + "required": [ + "row_number" + ] + }, + { + "required": [ + "start_line" + ] + } + ], + "properties": { + "cell_name": { + "title": "Cell Name", + "type": "string" + }, + "column_name": { + "title": "Column Name", + "type": "string" + }, + "column_number": { + "title": "Column Number", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "json_path": { + "title": "JSON Path", + "type": "string" + }, + "page_number": { + "title": "Page Number", + "type": "integer" + }, + "record_index_in_array": { + "title": "Record Index in Array", + "type": "integer" + }, + "row_number": { + "title": "Row Number", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "title": "Occurrence Details", + "type": "object" + }, + "firewall_rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "condition": { + "title": "Condition", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "match_details": { + "items": { + "type": "string" + }, + "title": "Match Details", + "type": "array" + }, + "match_location": { + "title": "Match Location", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "sensitivity": { + "title": "Sensitivity", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Firewall Rule", + "type": "object" + }, + "threat_actor": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Threat Actor Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Threat Actor Type ID", + "type": "integer" + } + }, + "required": [ + "name" + ], + "title": "Threat Actor", + "type": "object" + }, + "package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Package", + "type": "object" + }, + "transformation_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Transformation Info", + "type": "object" + }, + "response": { + "additionalProperties": false, + "properties": { + "code": { + "title": "Response Code", + "type": "integer" + }, + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "error": { + "title": "Error Code", + "type": "string" + }, + "error_message": { + "title": "Error Message", + "type": "string" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "message": { + "title": "Message", + "type": "string" + } + }, + "title": "Response Elements", + "type": "object" + }, + "reporter": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Reporter", + "type": "object" + }, + "url": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "path" + ] + }, + { + "required": [ + "url_string" + ] + } + ], + "properties": { + "categories": { + "items": { + "type": "string" + }, + "title": "Website Categorization", + "type": "array" + }, + "category_ids": { + "items": { + "enum": [ + 0, + 1, + 3, + 4, + 5, + 6, + 7, + 9, + 11, + 14, + 15, + 16, + 17, + 18, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 40, + 43, + 44, + 45, + 46, + 47, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 63, + 64, + 65, + 66, + 67, + 68, + 71, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 92, + 93, + 95, + 96, + 97, + 98, + 99, + 101, + 102, + 103, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 118, + 121 + ], + "type": "integer" + }, + "title": "Website Categorization IDs", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "query_string": { + "title": "HTTP Query String", + "type": "string" + }, + "resource_type": { + "title": "Resource Type", + "type": "string" + }, + "scheme": { + "title": "Scheme", + "type": "string" + }, + "subdomain": { + "title": "Subdomain", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Uniform Resource Locator", + "type": "object" + }, + "object": { + "additionalProperties": true, + "properties": {}, + "title": "Object", + "type": "object" + }, + "extension": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "version" + ], + "title": "Schema Extension", + "type": "object" + }, + "gpu_info": { + "additionalProperties": false, + "properties": { + "bus_type": { + "title": "Bus Type", + "type": "string" + }, + "bus_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Bus Type ID", + "type": "integer" + }, + "cores": { + "title": "Cores", + "type": "integer" + }, + "model": { + "title": "Model", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "vram_mode": { + "title": "VRAM Mode", + "type": "string" + }, + "vram_mode_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "VRAM Mode ID", + "type": "integer" + }, + "vram_size": { + "title": "VRAM Size", + "type": "integer" + } + }, + "title": "GPU Information", + "type": "object" + }, + "port_info": { + "additionalProperties": false, + "properties": { + "port": { + "title": "Port", + "type": "integer" + }, + "protocol_name": { + "title": "Protocol Name", + "type": "string" + }, + "protocol_num": { + "title": "Protocol Number", + "type": "integer" + } + }, + "required": [ + "port" + ], + "title": "Port Information", + "type": "object" + }, + "service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Service", + "type": "object" + }, + "metric": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Metric", + "type": "object" + }, + "request": { + "additionalProperties": false, + "properties": { + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Request Elements", + "type": "object" + }, + "sub_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Sub-technique", + "type": "object" + }, + "container": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_driver": { + "title": "Network Driver", + "type": "string" + }, + "orchestrator": { + "title": "Orchestrator", + "type": "string" + }, + "pod_uuid": { + "title": "Pod UUID", + "type": "string" + }, + "runtime": { + "title": "Runtime", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Container", + "type": "object" + }, + "malware": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "classification_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 99 + ], + "type": "integer" + }, + "title": "Classification IDs", + "type": "array" + }, + "classifications": { + "items": { + "type": "string" + }, + "title": "Classifications", + "type": "array" + }, + "cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "CVE List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "path": { + "title": "Path", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "classification_ids" + ], + "title": "Malware", + "type": "object" + }, + "win_win_service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "service_category_id" + ] + }, + { + "required": [ + "service_dependencies" + ] + }, + { + "required": [ + "service_error_control_id" + ] + }, + { + "required": [ + "service_start_name" + ] + }, + { + "required": [ + "service_start_type_id" + ] + }, + { + "required": [ + "service_type_id" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "hosting_process": { + "$ref": "#/$defs/process_entity", + "title": "Hosting Process" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "load_order_group": { + "title": "Load Order Group", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "service_category": { + "title": "Service Category", + "type": "string" + }, + "service_category_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Service Category ID", + "type": "integer" + }, + "service_dependencies": { + "items": { + "type": "string" + }, + "title": "Service Dependencies", + "type": "array" + }, + "service_dll_file": { + "$ref": "#/$defs/file", + "title": "Service DLL" + }, + "service_error_control": { + "title": "Service Error Control", + "type": "string" + }, + "service_error_control_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Error Control ID", + "type": "integer" + }, + "service_file": { + "$ref": "#/$defs/file", + "title": "Service File" + }, + "service_start_name": { + "title": "Service Start Name", + "type": "string" + }, + "service_start_type": { + "title": "Service Start Type", + "type": "string" + }, + "service_start_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Service Start Type ID", + "type": "integer" + }, + "service_type": { + "title": "Service Type", + "type": "string" + }, + "service_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Type ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Windows Service", + "type": "object" + }, + "group": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "privileges": { + "items": { + "type": "string" + }, + "title": "Privileges", + "type": "array" + }, + "type": { + "title": "Group Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "Group", + "type": "object" + }, + "device": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "region": { + "title": "Region", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "os_machine_uuid": { + "title": "OS Machine UUID", + "type": "string" + }, + "hypervisor": { + "title": "Hypervisor", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "imei_list": { + "items": { + "type": "string" + }, + "title": "IMEI List", + "type": "array" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "is_shared": { + "title": "Shared Device", + "type": "boolean" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "meid": { + "title": "MEID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "is_personal": { + "title": "Personal Device", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "iccid": { + "title": "ICCID", + "type": "string" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "udid": { + "title": "Unique Device Identifier", + "type": "string" + }, + "model": { + "title": "Model", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "boot_time": { + "title": "Boot Time", + "type": "integer" + }, + "is_supervised": { + "title": "Supervised Device", + "type": "boolean" + }, + "is_managed": { + "title": "Managed Device", + "type": "boolean" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "is_trusted": { + "title": "Trusted Device", + "type": "boolean" + }, + "boot_uid": { + "title": "Boot UID", + "type": "string" + }, + "network_interfaces": { + "items": { + "$ref": "#/$defs/network_interface" + }, + "title": "Network Interfaces", + "type": "array" + }, + "autoscale_uid": { + "title": "Autoscale UID", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "subnet": { + "title": "Subnet", + "type": "string" + }, + "eid": { + "title": "EID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "imei": { + "title": "IMEI", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "is_mobile_account_active": { + "title": "Mobile Account Active", + "type": "boolean" + }, + "is_compliant": { + "title": "Compliant Device", + "type": "boolean" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Device", + "type": "object" + }, + "campaign": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Campaign", + "type": "object" + }, + "cwe": { + "additionalProperties": false, + "properties": { + "caption": { + "title": "Caption", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "CWE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CWE", + "type": "object" + }, + "digital_signature": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "developer_uid": { + "title": "Developer UID", + "type": "string" + }, + "digest": { + "$ref": "#/$defs/fingerprint", + "title": "Message Digest" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "State ID", + "type": "integer" + } + }, + "required": [ + "algorithm_id" + ], + "title": "Digital Signature", + "type": "object" + }, + "sbom": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "package": { + "$ref": "#/$defs/package", + "title": "Software Package" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "software_components": { + "items": { + "$ref": "#/$defs/software_component" + }, + "title": "Software Components", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "SBOM ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "package", + "software_components" + ], + "title": "Software Bill of Materials", + "type": "object" + }, + "malware_scan_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "end_time": { + "title": "End Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_files": { + "title": "Scanned Files", + "type": "integer" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "num_volumes": { + "title": "Number of Volumes", + "type": "integer" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Scan UID", + "type": "string" + }, + "unique_malware_count": { + "title": "Unique Malware Count", + "type": "integer" + } + }, + "required": [ + "type_id" + ], + "title": "Malware Scan Info", + "type": "object" + }, + "kill_chain_phase": { + "additionalProperties": false, + "properties": { + "phase": { + "title": "Kill Chain Phase", + "type": "string" + }, + "phase_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Kill Chain Phase ID", + "type": "integer" + } + }, + "required": [ + "phase_id" + ], + "title": "Kill Chain Phase", + "type": "object" + }, + "organization": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "ou_name": { + "title": "Org Unit Name", + "type": "string" + }, + "ou_uid": { + "title": "Org Unit ID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Organization", + "type": "object" + }, + "encryption_details": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Encryption Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Encryption Algorithm ID", + "type": "integer" + }, + "key_length": { + "title": "Encryption Key Length", + "type": "integer" + }, + "key_uid": { + "title": "Key UID", + "type": "string" + }, + "type": { + "title": "Encryption Type", + "type": "string" + } + }, + "title": "Encryption Details", + "type": "object" + }, + "logger": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "log_format": { + "title": "Log Format", + "type": "string" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Logger", + "type": "object" + }, + "feature": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Feature", + "type": "object" + }, + "timespan": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "duration" + ] + }, + { + "required": [ + "duration_days" + ] + }, + { + "required": [ + "duration_hours" + ] + }, + { + "required": [ + "duration_mins" + ] + }, + { + "required": [ + "duration_months" + ] + }, + { + "required": [ + "duration_secs" + ] + }, + { + "required": [ + "duration_weeks" + ] + }, + { + "required": [ + "duration_years" + ] + }, + { + "required": [ + "end_time" + ] + }, + { + "required": [ + "start_time" + ] + } + ], + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "duration_days": { + "title": "Duration Days", + "type": "integer" + }, + "duration_hours": { + "title": "Duration Hours", + "type": "integer" + }, + "duration_mins": { + "title": "Duration Minutes", + "type": "integer" + }, + "duration_months": { + "title": "Duration Months", + "type": "integer" + }, + "duration_secs": { + "title": "Duration Seconds", + "type": "integer" + }, + "duration_weeks": { + "title": "Duration Weeks", + "type": "integer" + }, + "duration_years": { + "title": "Duration Years", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Time Span Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Time Span Type ID", + "type": "integer" + } + }, + "title": "Time Span", + "type": "object" + }, + "rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Rule", + "type": "object" + }, + "software_component": { + "additionalProperties": false, + "properties": { + "author": { + "title": "Author", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "related_component": { + "title": "Related Component", + "type": "string" + }, + "relationship": { + "title": "Relationship", + "type": "string" + }, + "relationship_id": { + "enum": [ + 0, + 1, + 99 + ], + "title": "Relationship ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Component", + "type": "object" + }, + "agent": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Agent Name", + "type": "string" + }, + "policies": { + "items": { + "$ref": "#/$defs/policy" + }, + "title": "Agent Policies", + "type": "array" + }, + "type": { + "title": "Agent Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Agent ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate Agent ID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Agent Version", + "type": "string" + } + }, + "title": "Agent", + "type": "object" + }, + "whois": { + "additionalProperties": false, + "properties": { + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "created_time": { + "title": "Registered At", + "type": "integer" + }, + "dnssec_status": { + "title": "DNSSEC Status", + "type": "string" + }, + "dnssec_status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "DNSSEC Status ID", + "type": "integer" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "domain_contacts": { + "items": { + "$ref": "#/$defs/domain_contact" + }, + "title": "Domain Contacts", + "type": "array" + }, + "email_addr": { + "title": "Registrar Abuse Email Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "last_seen_time": { + "title": "Last Updated At", + "type": "integer" + }, + "name_servers": { + "items": { + "type": "string" + }, + "title": "Name Servers", + "type": "array" + }, + "phone_number": { + "title": "Registrar Abuse Phone Number", + "type": "string" + }, + "registrar": { + "title": "Domain Registrar", + "type": "string" + }, + "status": { + "title": "Domain Status", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Subdomains", + "type": "array" + }, + "subnet": { + "title": "Subnet Block", + "type": "string" + } + }, + "title": "WHOIS", + "type": "object" + }, + "key_value_object": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "value" + ] + }, + { + "required": [ + "values" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + }, + "values": { + "items": { + "type": "string" + }, + "title": "Values", + "type": "array" + } + }, + "required": [ + "name" + ], + "title": "Key:Value object", + "type": "object" + }, + "metadata": { + "additionalProperties": false, + "properties": { + "log_format": { + "title": "Log Source Format", + "type": "string" + }, + "transformation_info_list": { + "items": { + "$ref": "#/$defs/transformation_info" + }, + "title": "Transformation Info", + "type": "array" + }, + "uid": { + "title": "Event UID", + "type": "string" + }, + "original_event_uid": { + "title": "Original Event ID", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "extensions": { + "items": { + "$ref": "#/$defs/extension" + }, + "title": "Schema Extensions", + "type": "array" + }, + "source": { + "title": "Source", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "loggers": { + "items": { + "$ref": "#/$defs/logger" + }, + "title": "Loggers", + "type": "array" + }, + "processed_time": { + "title": "Processed Time", + "type": "integer" + }, + "log_source": { + "title": "Log Source", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "debug": { + "items": { + "type": "string" + }, + "title": "Debug Information", + "type": "array" + }, + "profiles": { + "items": { + "type": "string" + }, + "title": "Profiles", + "type": "array" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "event_code": { + "title": "Event Code", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "reporter": { + "$ref": "#/$defs/reporter", + "title": "Reporter" + }, + "correlation_uid": { + "title": "Correlation UID", + "type": "string" + }, + "extension": { + "$ref": "#/$defs/extension", + "title": "Schema Extension" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "sequence": { + "title": "Sequence Number", + "type": "integer" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "original_time": { + "title": "Original Time", + "type": "string" + }, + "total_queued_duration": { + "$ref": "#/$defs/timespan", + "title": "Total Queued Duration" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "product", + "version" + ], + "title": "Metadata", + "type": "object" + }, + "node": { + "additionalProperties": false, + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Node", + "type": "object" + }, + "product": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "feature": { + "$ref": "#/$defs/feature", + "title": "Feature" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Product", + "type": "object" + }, + "advisory": { + "additionalProperties": false, + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "Related CVEs", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Advisory ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Advisory", + "type": "object" + }, + "network_endpoint": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "title": "Network Endpoint", + "type": "object" + }, + "d3f_tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Tactic", + "type": "object" + }, + "cve": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "cvss": { + "items": { + "$ref": "#/$defs/cvss" + }, + "title": "CVSS Score", + "type": "array" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "cwe_uid": { + "title": "CWE UID", + "type": "string" + }, + "cwe_url": { + "title": "CWE URL", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "epss": { + "$ref": "#/$defs/epss", + "title": "EPSS" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "title": { + "title": "Title", + "type": "string" + }, + "type": { + "title": "Vulnerability Type", + "type": "string" + }, + "uid": { + "title": "CVE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CVE", + "type": "object" + }, + "domain_contact": { + "additionalProperties": false, + "properties": { + "email_addr": { + "title": "Contact Email", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Contact Location Information" + }, + "name": { + "title": "Name", + "type": "string" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "type": { + "title": "Domain Contact Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Domain Contact Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Domain Contact", + "type": "object" + }, + "policy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "type" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "is_applied": { + "title": "Applied", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Policy", + "type": "object" + }, + "span": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "end_time", + "start_time", + "uid" + ], + "title": "Span", + "type": "object" + } + }, + "$id": "https://schema.ocsf.io/schema/classes/api_activity", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "unmapped": { + "$ref": "#/$defs/object", + "title": "Unmapped Data" + }, + "message": { + "title": "Message", + "type": "string" + }, + "http_response": { + "$ref": "#/$defs/http_response", + "title": "HTTP Response" + }, + "category_uid": { + "const": 6, + "title": "Category ID", + "type": "integer" + }, + "api": { + "$ref": "#/$defs/api", + "title": "API Details" + }, + "message_context": { + "$ref": "#/$defs/message_context", + "title": "Message Context" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "status_detail": { + "title": "Status Detail", + "type": "string" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "type_name": { + "title": "Type Name", + "type": "string" + }, + "raw_data": { + "title": "Raw Data", + "type": "string" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "actor": { + "$ref": "#/$defs/actor", + "title": "Actor" + }, + "trace": { + "$ref": "#/$defs/trace", + "title": "Trace" + }, + "status": { + "title": "Status", + "type": "string" + }, + "ai_model": { + "$ref": "#/$defs/ai_model", + "title": "AI Model" + }, + "activity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Activity ID", + "type": "integer" + }, + "resources": { + "items": { + "$ref": "#/$defs/resource_details" + }, + "title": "Resources Array", + "type": "array" + }, + "observables": { + "items": { + "$ref": "#/$defs/observable" + }, + "title": "Observables", + "type": "array" + }, + "enrichments": { + "items": { + "$ref": "#/$defs/enrichment" + }, + "title": "Enrichments", + "type": "array" + }, + "http_request": { + "$ref": "#/$defs/http_request", + "title": "HTTP Request" + }, + "raw_data_hash": { + "$ref": "#/$defs/fingerprint", + "title": "Raw Data Hash" + }, + "class_uid": { + "const": 6003, + "title": "Class ID", + "type": "integer" + }, + "activity_name": { + "title": "Activity", + "type": "string" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "class_name": { + "title": "Class", + "type": "string" + }, + "timezone_offset": { + "title": "Timezone Offset", + "type": "integer" + }, + "raw_data_size": { + "title": "Raw Data Size", + "type": "integer" + }, + "dst_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Destination Endpoint" + }, + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "category_name": { + "title": "Category", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "count": { + "title": "Count", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "src_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Source Endpoint" + }, + "metadata": { + "$ref": "#/$defs/metadata", + "title": "Metadata" + } + }, + "required": [ + "activity_id", + "actor", + "api", + "category_uid", + "class_uid", + "metadata", + "severity_id", + "src_endpoint", + "time", + "type_uid" + ], + "title": "API Activity", + "type": "object" +} diff --git a/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+host+security_control+trace.json b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+host+security_control+trace.json new file mode 100644 index 0000000000..6c8cf83eb1 --- /dev/null +++ b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+host+security_control+trace.json @@ -0,0 +1,8241 @@ +{ + "$defs": { + "kb_article": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "src_url" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "KB Article", + "type": "object" + }, + "long_string": { + "additionalProperties": false, + "properties": { + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "value" + ], + "title": "Long String", + "type": "object" + }, + "enrichment": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "short_desc": { + "title": "Short Description", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "data", + "name", + "value" + ], + "title": "Enrichment", + "type": "object" + }, + "ldap_person": { + "additionalProperties": false, + "properties": { + "cost_center": { + "title": "Cost Center", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "deleted_time": { + "title": "Deleted Time", + "type": "integer" + }, + "department": { + "title": "Department", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "email_addrs": { + "items": { + "type": "string" + }, + "title": "Email Addresses", + "type": "array" + }, + "employee_uid": { + "title": "Employee ID", + "type": "string" + }, + "given_name": { + "title": "Given Name", + "type": "string" + }, + "hire_time": { + "title": "Hire Time", + "type": "integer" + }, + "job_title": { + "title": "Job Title", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "last_login_time": { + "title": "Last Login", + "type": "integer" + }, + "ldap_cn": { + "title": "LDAP Common Name", + "type": "string" + }, + "ldap_dn": { + "title": "LDAP Distinguished Name", + "type": "string" + }, + "leave_time": { + "title": "Leave Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "manager": { + "$ref": "#/$defs/user", + "title": "Manager" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "office_location": { + "title": "Office Location", + "type": "string" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "surname": { + "title": "Surname", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + } + }, + "title": "LDAP Person", + "type": "object" + }, + "email_auth": { + "additionalProperties": false, + "properties": { + "dkim": { + "title": "DKIM Status", + "type": "string" + }, + "dkim_domain": { + "title": "DKIM Domain", + "type": "string" + }, + "dkim_signature": { + "title": "DKIM Signature", + "type": "string" + }, + "dmarc": { + "title": "DMARC Status", + "type": "string" + }, + "dmarc_override": { + "title": "DMARC Override", + "type": "string" + }, + "dmarc_policy": { + "title": "DMARC Policy", + "type": "string" + }, + "spf": { + "title": "SPF Status", + "type": "string" + } + }, + "title": "Email Authentication", + "type": "object" + }, + "epss": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "percentile": { + "title": "EPSS Percentile", + "type": "number" + }, + "score": { + "title": "EPPS Score", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "score" + ], + "title": "EPSS", + "type": "object" + }, + "device_hw_info": { + "additionalProperties": false, + "properties": { + "bios_date": { + "title": "BIOS Date", + "type": "string" + }, + "bios_manufacturer": { + "title": "BIOS Manufacturer", + "type": "string" + }, + "bios_ver": { + "title": "BIOS Version", + "type": "string" + }, + "chassis": { + "title": "Chassis", + "type": "string" + }, + "cpu_architecture": { + "title": "CPU Architecture", + "type": "string" + }, + "cpu_architecture_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "CPU Architecture ID", + "type": "integer" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "cpu_cores": { + "title": "CPU Cores", + "type": "integer" + }, + "cpu_count": { + "title": "CPU Count", + "type": "integer" + }, + "cpu_speed": { + "title": "Processor Speed", + "type": "integer" + }, + "cpu_type": { + "title": "Processor Type", + "type": "string" + }, + "desktop_display": { + "$ref": "#/$defs/display", + "title": "Desktop Display" + }, + "gpu_count": { + "title": "GPU Count", + "type": "integer" + }, + "gpu_info_list": { + "items": { + "$ref": "#/$defs/gpu_info" + }, + "title": "GPU information", + "type": "array" + }, + "keyboard_info": { + "$ref": "#/$defs/keyboard_info", + "title": "Keyboard Information" + }, + "ram_size": { + "title": "RAM Size", + "type": "integer" + }, + "serial_number": { + "title": "Serial Number", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Device Hardware Info", + "type": "object" + }, + "network_proxy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Network Proxy Endpoint", + "type": "object" + }, + "data_classification": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "category_id" + ] + }, + { + "required": [ + "confidentiality_id" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "category_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Category ID", + "type": "integer" + }, + "classifier_details": { + "$ref": "#/$defs/classifier_details", + "title": "Classifier Details" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "discovery_details": { + "items": { + "$ref": "#/$defs/discovery_details" + }, + "title": "Discovery Details", + "type": "array" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "status_details": { + "items": { + "type": "string" + }, + "title": "Status Details", + "type": "array" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Data Classification", + "type": "object" + }, + "http_response": { + "additionalProperties": false, + "properties": { + "body_length": { + "title": "Response Body Length", + "type": "integer" + }, + "code": { + "title": "Response Code", + "type": "integer" + }, + "content_type": { + "title": "HTTP Content Type", + "type": "string" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "latency": { + "title": "Latency", + "type": "integer" + }, + "length": { + "title": "Response Length", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "code" + ], + "title": "HTTP Response", + "type": "object" + }, + "resource_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "criticality": { + "title": "Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Resource Relationship" + }, + "role": { + "title": "Role", + "type": "string" + }, + "role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Role ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Resource Details", + "type": "object" + }, + "display": { + "additionalProperties": false, + "properties": { + "color_depth": { + "title": "Color Depth", + "type": "integer" + }, + "physical_height": { + "title": "Physical Height", + "type": "integer" + }, + "physical_orientation": { + "title": "Physical Orientation", + "type": "integer" + }, + "physical_width": { + "title": "Physical Width", + "type": "integer" + }, + "scale_factor": { + "title": "Scale Factor", + "type": "integer" + } + }, + "title": "Display", + "type": "object" + }, + "scim": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "error_message": { + "title": "Last Error Message", + "type": "string" + }, + "is_group_provisioning_enabled": { + "title": "SCIM Group Provisioning Enabled", + "type": "boolean" + }, + "is_user_provisioning_enabled": { + "title": "SCIM User Provisioning Enabled", + "type": "boolean" + }, + "last_run_time": { + "title": "Last Sync Time", + "type": "integer" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "scim_group_schema": { + "title": "SCIM Group Schema" + }, + "scim_user_schema": { + "title": "SCIM User Schema" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "External ID", + "type": "string" + }, + "url_string": { + "title": "SCIM Endpoint URL", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + }, + "version": { + "title": "SCIM Version", + "type": "string" + } + }, + "title": "SCIM", + "type": "object" + }, + "programmatic_credential": { + "additionalProperties": false, + "properties": { + "last_used_time": { + "title": "Last Used Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Programmatic Credential", + "type": "object" + }, + "os": { + "additionalProperties": false, + "properties": { + "build": { + "title": "OS Build", + "type": "string" + }, + "country": { + "title": "Country", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "edition": { + "title": "OS Edition", + "type": "string" + }, + "kernel_release": { + "title": "Kernel Release", + "type": "string" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "sp_name": { + "title": "OS Service Pack", + "type": "string" + }, + "sp_ver": { + "title": "OS Service Pack Version", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 99, + 100, + 101, + 200, + 201, + 300, + 301, + 302, + 400, + 401, + 402 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "Operating System (OS)", + "type": "object" + }, + "tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Tactic", + "type": "object" + }, + "dns_answer": { + "additionalProperties": false, + "properties": { + "class": { + "title": "Resource Record Class", + "type": "string" + }, + "flag_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "type": "integer" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "packet_uid": { + "title": "Packet UID", + "type": "integer" + }, + "rdata": { + "title": "DNS RData", + "type": "string" + }, + "ttl": { + "title": "TTL", + "type": "integer" + }, + "type": { + "title": "Resource Record Type", + "type": "string" + } + }, + "required": [ + "rdata" + ], + "title": "DNS Answer", + "type": "object" + }, + "discovery_details": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "occurrence_details": { + "$ref": "#/$defs/occurrence_details", + "title": "Occurrence Details" + }, + "occurrences": { + "items": { + "$ref": "#/$defs/occurrence_details" + }, + "title": "Occurrences", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "title": "Discovery Details", + "type": "object" + }, + "sso": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "SAML Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "duration_mins": { + "title": "SSO Session Duration", + "type": "integer" + }, + "idle_timeout": { + "title": "SSO Idle Timeout", + "type": "integer" + }, + "login_endpoint": { + "title": "SSO Login Endpoint", + "type": "string" + }, + "logout_endpoint": { + "title": "SSO Logout Endpoint", + "type": "string" + }, + "metadata_endpoint": { + "title": "SSO Metadata Endpoint", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "title": "Scopes", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + } + }, + "title": "SSO", + "type": "object" + }, + "token": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_renewable": { + "title": "Renewable", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Token", + "type": "object" + }, + "api": { + "additionalProperties": false, + "properties": { + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "request": { + "$ref": "#/$defs/request", + "title": "API Request Details" + }, + "response": { + "$ref": "#/$defs/response", + "title": "API Response Details" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "token": { + "$ref": "#/$defs/token", + "title": "Token" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "operation" + ], + "title": "API", + "type": "object" + }, + "d3fend": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "d3f_tactic" + ] + }, + { + "required": [ + "d3f_technique" + ] + } + ], + "properties": { + "d3f_tactic": { + "$ref": "#/$defs/d3f_tactic", + "title": "MITRE D3FEND\u2122 Tactic" + }, + "d3f_technique": { + "$ref": "#/$defs/d3f_technique", + "title": "MITRE D3FEND\u2122 Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122", + "type": "object" + }, + "reputation": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Reputation Score", + "type": "number" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "score": { + "title": "Reputation Score", + "type": "string" + }, + "score_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "Reputation Score ID", + "type": "integer" + } + }, + "required": [ + "base_score", + "score_id" + ], + "title": "Reputation", + "type": "object" + }, + "mitigation": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "countermeasures": { + "items": { + "$ref": "#/$defs/d3fend" + }, + "title": "Countermeasures", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Mitigation", + "type": "object" + }, + "location": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "city" + ] + }, + { + "required": [ + "country" + ] + }, + { + "required": [ + "postal_code" + ] + }, + { + "required": [ + "region" + ] + } + ], + "properties": { + "aerial_height": { + "title": "Aerial Height", + "type": "string" + }, + "city": { + "title": "City", + "type": "string" + }, + "continent": { + "title": "Continent", + "type": "string" + }, + "coordinates": { + "items": { + "type": "number" + }, + "title": "Coordinates", + "type": "array" + }, + "country": { + "title": "Country", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "geodetic_altitude": { + "title": "Geodetic Altitude", + "type": "string" + }, + "geodetic_vertical_accuracy": { + "title": "Geodetic Vertical Accuracy", + "type": "string" + }, + "geohash": { + "title": "Geohash", + "type": "string" + }, + "horizontal_accuracy": { + "title": "Horizontal Accuracy", + "type": "string" + }, + "is_on_premises": { + "title": "On-Premises", + "type": "boolean" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "lat": { + "title": "Latitude", + "type": "number" + }, + "long": { + "title": "Longitude", + "type": "number" + }, + "postal_code": { + "title": "Postal Code", + "type": "string" + }, + "pressure_altitude": { + "title": "Pressure Altitude", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + } + }, + "title": "Geo Location", + "type": "object" + }, + "idp": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "auth_factors": { + "items": { + "$ref": "#/$defs/auth_factor" + }, + "title": "Authentication Factors", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprint": { + "$ref": "#/$defs/fingerprint", + "title": "Certificate Fingerprint" + }, + "has_mfa": { + "title": "MFA Enforced", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scim": { + "$ref": "#/$defs/scim", + "title": "SCIM" + }, + "sso": { + "$ref": "#/$defs/sso", + "title": "SSO" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "Configuration URL", + "type": "string" + } + }, + "title": "Identity Provider", + "type": "object" + }, + "message_context": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "application" + ] + }, + { + "required": [ + "service" + ] + } + ], + "properties": { + "ai_role": { + "title": "AI Role", + "type": "string" + }, + "ai_role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "AI Role ID", + "type": "integer" + }, + "application": { + "$ref": "#/$defs/application", + "title": "Application" + }, + "completion_tokens": { + "title": "Completion Tokens", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "prompt_tokens": { + "title": "Prompt Tokens", + "type": "integer" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "total_tokens": { + "title": "Total Tokens", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Message Context", + "type": "object" + }, + "process": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ancestry": { + "items": { + "$ref": "#/$defs/process_entity" + }, + "title": "Ancestry", + "type": "array" + }, + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "environment_variables": { + "items": { + "$ref": "#/$defs/environment_variable" + }, + "title": "Environment Variables", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hosted_services": { + "items": { + "$ref": "#/$defs/win_win_service" + }, + "title": "Hosted Services", + "type": "array" + }, + "integrity": { + "title": "Integrity", + "type": "string" + }, + "integrity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Integrity Level", + "type": "integer" + }, + "lineage": { + "items": { + "type": "string" + }, + "title": "Lineage", + "type": "array" + }, + "loaded_modules": { + "items": { + "type": "string" + }, + "title": "Loaded Modules", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_process": { + "$ref": "#/$defs/process", + "title": "Parent Process" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "ptid": { + "title": "Process Thread ID", + "type": "integer" + }, + "sandbox": { + "title": "Sandbox", + "type": "string" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "terminated_time": { + "title": "Terminated Time", + "type": "integer" + }, + "tid": { + "title": "Thread ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + }, + "working_directory": { + "title": "Working Directory", + "type": "string" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + } + }, + "title": "Process", + "type": "object" + }, + "vulnerability": { + "additionalProperties": false, + "oneOf": [ + { + "not": { + "required": [ + "cve", + "cwe" + ] + }, + "required": [ + "advisory" + ] + }, + { + "not": { + "required": [ + "advisory", + "cwe" + ] + }, + "required": [ + "cve" + ] + }, + { + "not": { + "required": [ + "advisory", + "cve" + ] + }, + "required": [ + "cwe" + ] + } + ], + "properties": { + "advisory": { + "$ref": "#/$defs/advisory", + "title": "Security Advisory" + }, + "affected_code": { + "items": { + "$ref": "#/$defs/affected_code" + }, + "title": "Affected Code", + "type": "array" + }, + "affected_packages": { + "items": { + "$ref": "#/$defs/affected_package" + }, + "title": "Affected Software Packages", + "type": "array" + }, + "category": { + "title": "Category", + "type": "string" + }, + "cve": { + "$ref": "#/$defs/cve", + "title": "CVE" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "dependency_chain": { + "title": "Dependency Chain", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "exploit_last_seen_time": { + "title": "Exploit Last Seen Time", + "type": "integer" + }, + "exploit_ref_url": { + "title": "Exploit URL", + "type": "string" + }, + "exploit_requirement": { + "title": "Exploit Requirement", + "type": "string" + }, + "exploit_type": { + "title": "Exploit Type", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "fix_coverage": { + "title": "Fix Coverage", + "type": "string" + }, + "fix_coverage_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Fix Coverage ID", + "type": "integer" + }, + "is_exploit_available": { + "title": "Exploit Availability", + "type": "boolean" + }, + "is_fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "packages": { + "items": { + "$ref": "#/$defs/package" + }, + "title": "Software Packages", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_vulnerabilities": { + "items": { + "type": "string" + }, + "title": "Related Vulnerability IDs", + "type": "array" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Vulnerability Details", + "type": "object" + }, + "affected_code": { + "additionalProperties": false, + "properties": { + "end_column": { + "title": "End Column", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "rule": { + "$ref": "#/$defs/rule", + "title": "Related Rule" + }, + "start_column": { + "title": "Start Column", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "required": [ + "file" + ], + "title": "Affected Code", + "type": "object" + }, + "keyboard_info": { + "additionalProperties": false, + "properties": { + "function_keys": { + "title": "Function Keys", + "type": "integer" + }, + "ime": { + "title": "IME", + "type": "string" + }, + "keyboard_layout": { + "title": "Keyboard Layout", + "type": "string" + }, + "keyboard_subtype": { + "title": "Keyboard Subtype", + "type": "integer" + }, + "keyboard_type": { + "title": "Keyboard Type", + "type": "string" + } + }, + "title": "Keyboard Information", + "type": "object" + }, + "certificate": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "is_self_signed": { + "title": "Certificate Self-Signed", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Distinguished Name", + "type": "string" + }, + "sans": { + "items": { + "$ref": "#/$defs/san" + }, + "title": "Subject Alternative Names", + "type": "array" + }, + "serial_number": { + "title": "Certificate Serial Number", + "type": "string" + }, + "subject": { + "title": "Subject Distinguished Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "issuer", + "serial_number" + ], + "title": "Digital Certificate", + "type": "object" + }, + "graph": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "edges": { + "items": { + "$ref": "#/$defs/edge" + }, + "title": "Edges", + "type": "array" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "nodes": { + "items": { + "$ref": "#/$defs/node" + }, + "title": "Nodes", + "type": "array" + }, + "query_language": { + "title": "Query Language", + "type": "string" + }, + "query_language_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Query Language ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "nodes" + ], + "title": "Graph", + "type": "object" + }, + "account": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "is_disabled": { + "title": "Disabled", + "type": "boolean" + }, + "is_locked": { + "title": "Locked", + "type": "boolean" + }, + "is_on_premises_sync_enabled": { + "title": "On-Premises Sync Enabled", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Account", + "type": "object" + }, + "authorization": { + "additionalProperties": false, + "properties": { + "decision": { + "title": "Authorization Decision/Outcome", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "title": "Authorization Result", + "type": "object" + }, + "autonomous_system": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "number" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "number": { + "title": "Number", + "type": "integer" + } + }, + "title": "Autonomous System", + "type": "object" + }, + "classifier_details": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "Classifier Details", + "type": "object" + }, + "session": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "expiration_reason": { + "title": "Expiration Reason", + "type": "string" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_mfa": { + "title": "Multi Factor Authentication", + "type": "boolean" + }, + "is_remote": { + "title": "Remote", + "type": "boolean" + }, + "is_vpn": { + "title": "VPN Session", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "terminal": { + "title": "Terminal", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + } + }, + "title": "Session", + "type": "object" + }, + "d3f_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Technique", + "type": "object" + }, + "image": { + "additionalProperties": false, + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Image", + "type": "object" + }, + "application": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "criticality": { + "title": "Business Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Application Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Application Name", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Application Relationship" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "sbom": { + "$ref": "#/$defs/sbom", + "title": "Related SBOM" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Application Type", + "type": "string" + }, + "uid": { + "title": "Application ID", + "type": "string" + }, + "uid_alt": { + "title": "Application Alternative ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "version": { + "title": "Application Version", + "type": "string" + } + }, + "title": "Application", + "type": "object" + }, + "script": { + "additionalProperties": false, + "properties": { + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "script_content": { + "$ref": "#/$defs/long_string", + "title": "Script Content" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "script_content", + "type_id" + ], + "title": "Script", + "type": "object" + }, + "affected_package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "fixed_in_version": { + "title": "Fixed In Version", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Affected Software Package", + "type": "object" + }, + "cis_control": { + "additionalProperties": false, + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "CIS Control", + "type": "object" + }, + "analytic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Analytic", + "type": "object" + }, + "edge": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "relation": { + "title": "Relation", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "target": { + "title": "Target", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "source", + "target" + ], + "title": "Edge", + "type": "object" + }, + "actor": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "app_name" + ] + }, + { + "required": [ + "app_uid" + ] + }, + { + "required": [ + "invoked_by" + ] + }, + { + "required": [ + "process" + ] + }, + { + "required": [ + "session" + ] + }, + { + "required": [ + "user" + ] + } + ], + "properties": { + "app_name": { + "title": "Application Name", + "type": "string" + }, + "app_uid": { + "title": "Application ID", + "type": "string" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "idp": { + "$ref": "#/$defs/idp", + "title": "Identity Provider" + }, + "invoked_by": { + "title": "Invoked by", + "type": "string" + }, + "process": { + "$ref": "#/$defs/process", + "title": "Process" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + } + }, + "title": "Actor", + "type": "object" + }, + "cloud": { + "additionalProperties": false, + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "cloud_partition": { + "title": "Cloud Partition", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "project_uid": { + "title": "Project ID", + "type": "string" + }, + "provider": { + "title": "Cloud Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + }, + "zone": { + "title": "Cloud Availability Zone", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "Cloud", + "type": "object" + }, + "trace": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "span": { + "$ref": "#/$defs/span", + "title": "Span" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Trace", + "type": "object" + }, + "ai_model": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ai_provider": { + "title": "AI Provider", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "ai_provider" + ], + "title": "AI Model", + "type": "object" + }, + "file": { + "additionalProperties": false, + "properties": { + "parent_folder": { + "title": "Parent Folder", + "type": "string" + }, + "is_system": { + "title": "System", + "type": "boolean" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "accessor": { + "$ref": "#/$defs/user", + "title": "Accessor" + }, + "is_readonly": { + "title": "Read-Only", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "ext": { + "title": "File Extension", + "type": "string" + }, + "company_name": { + "title": "Company Name", + "type": "string" + }, + "attributes": { + "title": "Attributes", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uri": { + "title": "File URI", + "type": "string" + }, + "is_deleted": { + "title": "Deleted", + "type": "boolean" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "drive_type": { + "title": "Drive Type", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "imported_symbols": { + "items": { + "type": "string" + }, + "title": "Imported Symbols", + "type": "array" + }, + "signature": { + "$ref": "#/$defs/digital_signature", + "title": "Digital Signature" + }, + "is_encrypted": { + "title": "Encrypted", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "accessed_time": { + "title": "Accessed Time", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "internal_name": { + "title": "Internal Name", + "type": "string" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "drive_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Drive Type ID", + "type": "integer" + }, + "security_descriptor": { + "title": "Security Descriptor", + "type": "string" + }, + "volume": { + "title": "Volume", + "type": "string" + }, + "modifier": { + "$ref": "#/$defs/user", + "title": "Modifier" + }, + "mime_type": { + "title": "MIME type", + "type": "string" + }, + "encryption_details": { + "$ref": "#/$defs/encryption_details", + "title": "Encryption Details" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Digital Signatures", + "type": "array" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "File", + "type": "object" + }, + "remediation": { + "additionalProperties": false, + "properties": { + "cis_controls": { + "items": { + "$ref": "#/$defs/cis_control" + }, + "title": "CIS Controls", + "type": "array" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + } + }, + "required": [ + "desc" + ], + "title": "Remediation", + "type": "object" + }, + "email": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "from" + ] + }, + { + "required": [ + "to" + ] + } + ], + "properties": { + "cc": { + "items": { + "type": "string" + }, + "title": "Cc", + "type": "array" + }, + "cc_mailboxes": { + "items": { + "type": "string" + }, + "title": "Cc Mailboxes", + "type": "array" + }, + "delivered_to": { + "title": "Delivered To", + "type": "string" + }, + "delivered_to_list": { + "items": { + "type": "string" + }, + "title": "Delivered To List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "from": { + "title": "From", + "type": "string" + }, + "from_list": { + "items": { + "type": "string" + }, + "title": "From List", + "type": "array" + }, + "from_mailbox": { + "title": "From Mailbox", + "type": "string" + }, + "from_mailboxes": { + "items": { + "type": "string" + }, + "title": "From Mailboxes", + "type": "array" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "is_read": { + "title": "Read", + "type": "boolean" + }, + "message_uid": { + "title": "Message UID", + "type": "string" + }, + "raw_header": { + "title": "Raw Header", + "type": "string" + }, + "reply_to": { + "title": "Reply To", + "type": "string" + }, + "reply_to_list": { + "items": { + "type": "string" + }, + "title": "Reply To List", + "type": "array" + }, + "reply_to_mailboxes": { + "items": { + "type": "string" + }, + "title": "Reply To Mailboxes", + "type": "array" + }, + "return_path": { + "title": "Return Path", + "type": "string" + }, + "sender": { + "title": "Sender", + "type": "string" + }, + "sender_mailbox": { + "title": "Sender Mailbox", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "smtp_from": { + "title": "SMTP From", + "type": "string" + }, + "smtp_to": { + "items": { + "type": "string" + }, + "title": "SMTP To", + "type": "array" + }, + "subject": { + "title": "Subject", + "type": "string" + }, + "to": { + "items": { + "type": "string" + }, + "title": "To", + "type": "array" + }, + "to_mailboxes": { + "items": { + "type": "string" + }, + "title": "To Mailboxes", + "type": "array" + }, + "uid": { + "title": "Email Thread UID", + "type": "string" + }, + "urls": { + "items": { + "$ref": "#/$defs/url" + }, + "title": "URLs", + "type": "array" + }, + "x_originating_ip": { + "items": { + "type": "string" + }, + "title": "X-Originating-IP", + "type": "array" + } + }, + "title": "Email", + "type": "object" + }, + "environment_variable": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Environment Variable", + "type": "object" + }, + "technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Technique", + "type": "object" + }, + "http_request": { + "additionalProperties": false, + "properties": { + "args": { + "title": "HTTP Arguments", + "type": "string" + }, + "body_length": { + "title": "Request Body Length", + "type": "integer" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "http_method": { + "enum": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "title": "HTTP Method", + "type": "string" + }, + "length": { + "title": "Request Length", + "type": "integer" + }, + "referrer": { + "title": "HTTP Referrer", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "user_agent": { + "title": "HTTP User-Agent", + "type": "string" + }, + "version": { + "title": "HTTP Version", + "type": "string" + }, + "x_forwarded_for": { + "items": { + "type": "string" + }, + "title": "X-Forwarded-For", + "type": "array" + } + }, + "title": "HTTP Request", + "type": "object" + }, + "cvss": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Base Score", + "type": "number" + }, + "depth": { + "enum": [ + "Base", + "Environmental", + "Temporal" + ], + "title": "CVSS Depth", + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/$defs/metric" + }, + "title": "Metrics", + "type": "array" + }, + "overall_score": { + "title": "Overall Score", + "type": "number" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "vector_string": { + "title": "Vector String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "base_score", + "version" + ], + "title": "CVSS Score", + "type": "object" + }, + "fingerprint": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "algorithm_id", + "value" + ], + "title": "Fingerprint", + "type": "object" + }, + "process_entity": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "path" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Process Entity", + "type": "object" + }, + "observable": { + "additionalProperties": false, + "properties": { + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Observable", + "type": "object" + }, + "http_header": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "HTTP Header", + "type": "object" + }, + "attack": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "sub_technique" + ] + }, + { + "required": [ + "tactic" + ] + }, + { + "required": [ + "technique" + ] + } + ], + "properties": { + "mitigation": { + "$ref": "#/$defs/mitigation", + "title": "MITRE Mitigation" + }, + "sub_technique": { + "$ref": "#/$defs/sub_technique", + "title": "MITRE Sub-technique" + }, + "tactic": { + "$ref": "#/$defs/tactic", + "title": "MITRE Tactic" + }, + "tactics": { + "items": { + "$ref": "#/$defs/tactic" + }, + "title": "Tactics", + "type": "array" + }, + "technique": { + "$ref": "#/$defs/technique", + "title": "MITRE Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "type": "object" + }, + "network_interface": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "mac" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "open_ports": { + "items": { + "$ref": "#/$defs/port_info" + }, + "title": "Open Ports", + "type": "array" + }, + "subnet_prefix": { + "title": "Subnet Prefix Length", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Network Interface", + "type": "object" + }, + "user": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "account" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "forward_addr": { + "title": "Forwarding Address", + "type": "string" + }, + "full_name": { + "title": "Full Name", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "has_mfa": { + "title": "MFA Assigned", + "type": "boolean" + }, + "ldap_person": { + "$ref": "#/$defs/ldap_person", + "title": "LDAP Person" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "programmatic_credentials": { + "items": { + "$ref": "#/$defs/programmatic_credential" + }, + "title": "Programmatic Credentials", + "type": "array" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "User", + "type": "object" + }, + "auth_factor": { + "additionalProperties": false, + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "factor_type": { + "title": "Factor Type", + "type": "string" + }, + "factor_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Factor Type ID", + "type": "integer" + }, + "is_hotp": { + "title": "HMAC-based One-time Password (HOTP)", + "type": "boolean" + }, + "is_totp": { + "title": "Time-based One-time Password (TOTP)", + "type": "boolean" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "security_questions": { + "items": { + "type": "string" + }, + "title": "Security Questions", + "type": "array" + } + }, + "required": [ + "factor_type_id" + ], + "title": "Authentication Factor", + "type": "object" + }, + "osint": { + "additionalProperties": false, + "properties": { + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "email_auth": { + "$ref": "#/$defs/email_auth", + "title": "Related Email Authentication" + }, + "external_uid": { + "title": "External ID", + "type": "string" + }, + "tlp": { + "enum": [ + "AMBER", + "AMBER STRICT", + "CLEAR", + "GREEN", + "RED", + "WHITE" + ], + "title": "Traffic Light Protocol", + "type": "string" + }, + "detection_pattern": { + "title": "Detection Pattern", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Related Subdomains", + "type": "array" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uploaded_time": { + "title": "Uploaded Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "comment": { + "title": "Analyst Comments", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "value": { + "title": "Indicator", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "intrusion_sets": { + "items": { + "type": "string" + }, + "title": "Intrusion Sets", + "type": "array" + }, + "script": { + "$ref": "#/$defs/script", + "title": "Related Script Data" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "Related File" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "email": { + "$ref": "#/$defs/email", + "title": "Related Email" + }, + "kill_chain": { + "items": { + "$ref": "#/$defs/kill_chain_phase" + }, + "title": "Kill Chain", + "type": "array" + }, + "detection_pattern_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Detection Pattern Type ID", + "type": "integer" + }, + "detection_pattern_type": { + "title": "Detection Pattern", + "type": "string" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "threat_actor": { + "$ref": "#/$defs/threat_actor", + "title": "Threat Actor" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 99 + ], + "title": "Indicator Type ID", + "type": "integer" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "answers": { + "items": { + "$ref": "#/$defs/dns_answer" + }, + "title": "Related DNS Answers", + "type": "array" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "campaign": { + "$ref": "#/$defs/campaign", + "title": "Campaign" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "subnet": { + "title": "Related Subnet", + "type": "string" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Related Digital Signatures", + "type": "array" + }, + "whois": { + "$ref": "#/$defs/whois", + "title": "WHOIS" + }, + "vulnerabilities": { + "items": { + "$ref": "#/$defs/vulnerability" + }, + "title": "Related Vulnerabilities", + "type": "array" + } + }, + "required": [ + "type_id", + "value" + ], + "title": "OSINT", + "type": "object" + }, + "san": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "title": "Subject Alternative Name", + "type": "object" + }, + "occurrence_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cell_name" + ] + }, + { + "required": [ + "column_name" + ] + }, + { + "required": [ + "column_number" + ] + }, + { + "required": [ + "end_line" + ] + }, + { + "required": [ + "json_path" + ] + }, + { + "required": [ + "page_number" + ] + }, + { + "required": [ + "record_index_in_array" + ] + }, + { + "required": [ + "row_number" + ] + }, + { + "required": [ + "start_line" + ] + } + ], + "properties": { + "cell_name": { + "title": "Cell Name", + "type": "string" + }, + "column_name": { + "title": "Column Name", + "type": "string" + }, + "column_number": { + "title": "Column Number", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "json_path": { + "title": "JSON Path", + "type": "string" + }, + "page_number": { + "title": "Page Number", + "type": "integer" + }, + "record_index_in_array": { + "title": "Record Index in Array", + "type": "integer" + }, + "row_number": { + "title": "Row Number", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "title": "Occurrence Details", + "type": "object" + }, + "firewall_rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "condition": { + "title": "Condition", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "match_details": { + "items": { + "type": "string" + }, + "title": "Match Details", + "type": "array" + }, + "match_location": { + "title": "Match Location", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "sensitivity": { + "title": "Sensitivity", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Firewall Rule", + "type": "object" + }, + "threat_actor": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Threat Actor Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Threat Actor Type ID", + "type": "integer" + } + }, + "required": [ + "name" + ], + "title": "Threat Actor", + "type": "object" + }, + "package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Package", + "type": "object" + }, + "transformation_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Transformation Info", + "type": "object" + }, + "response": { + "additionalProperties": false, + "properties": { + "code": { + "title": "Response Code", + "type": "integer" + }, + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "error": { + "title": "Error Code", + "type": "string" + }, + "error_message": { + "title": "Error Message", + "type": "string" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "message": { + "title": "Message", + "type": "string" + } + }, + "title": "Response Elements", + "type": "object" + }, + "reporter": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Reporter", + "type": "object" + }, + "url": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "path" + ] + }, + { + "required": [ + "url_string" + ] + } + ], + "properties": { + "categories": { + "items": { + "type": "string" + }, + "title": "Website Categorization", + "type": "array" + }, + "category_ids": { + "items": { + "enum": [ + 0, + 1, + 3, + 4, + 5, + 6, + 7, + 9, + 11, + 14, + 15, + 16, + 17, + 18, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 40, + 43, + 44, + 45, + 46, + 47, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 63, + 64, + 65, + 66, + 67, + 68, + 71, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 92, + 93, + 95, + 96, + 97, + 98, + 99, + 101, + 102, + 103, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 118, + 121 + ], + "type": "integer" + }, + "title": "Website Categorization IDs", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "query_string": { + "title": "HTTP Query String", + "type": "string" + }, + "resource_type": { + "title": "Resource Type", + "type": "string" + }, + "scheme": { + "title": "Scheme", + "type": "string" + }, + "subdomain": { + "title": "Subdomain", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Uniform Resource Locator", + "type": "object" + }, + "object": { + "additionalProperties": true, + "properties": {}, + "title": "Object", + "type": "object" + }, + "extension": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "version" + ], + "title": "Schema Extension", + "type": "object" + }, + "gpu_info": { + "additionalProperties": false, + "properties": { + "bus_type": { + "title": "Bus Type", + "type": "string" + }, + "bus_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Bus Type ID", + "type": "integer" + }, + "cores": { + "title": "Cores", + "type": "integer" + }, + "model": { + "title": "Model", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "vram_mode": { + "title": "VRAM Mode", + "type": "string" + }, + "vram_mode_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "VRAM Mode ID", + "type": "integer" + }, + "vram_size": { + "title": "VRAM Size", + "type": "integer" + } + }, + "title": "GPU Information", + "type": "object" + }, + "port_info": { + "additionalProperties": false, + "properties": { + "port": { + "title": "Port", + "type": "integer" + }, + "protocol_name": { + "title": "Protocol Name", + "type": "string" + }, + "protocol_num": { + "title": "Protocol Number", + "type": "integer" + } + }, + "required": [ + "port" + ], + "title": "Port Information", + "type": "object" + }, + "service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Service", + "type": "object" + }, + "metric": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Metric", + "type": "object" + }, + "request": { + "additionalProperties": false, + "properties": { + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Request Elements", + "type": "object" + }, + "sub_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Sub-technique", + "type": "object" + }, + "container": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_driver": { + "title": "Network Driver", + "type": "string" + }, + "orchestrator": { + "title": "Orchestrator", + "type": "string" + }, + "pod_uuid": { + "title": "Pod UUID", + "type": "string" + }, + "runtime": { + "title": "Runtime", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Container", + "type": "object" + }, + "malware": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "classification_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 99 + ], + "type": "integer" + }, + "title": "Classification IDs", + "type": "array" + }, + "classifications": { + "items": { + "type": "string" + }, + "title": "Classifications", + "type": "array" + }, + "cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "CVE List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "path": { + "title": "Path", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "classification_ids" + ], + "title": "Malware", + "type": "object" + }, + "win_win_service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "service_category_id" + ] + }, + { + "required": [ + "service_dependencies" + ] + }, + { + "required": [ + "service_error_control_id" + ] + }, + { + "required": [ + "service_start_name" + ] + }, + { + "required": [ + "service_start_type_id" + ] + }, + { + "required": [ + "service_type_id" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "hosting_process": { + "$ref": "#/$defs/process_entity", + "title": "Hosting Process" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "load_order_group": { + "title": "Load Order Group", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "service_category": { + "title": "Service Category", + "type": "string" + }, + "service_category_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Service Category ID", + "type": "integer" + }, + "service_dependencies": { + "items": { + "type": "string" + }, + "title": "Service Dependencies", + "type": "array" + }, + "service_dll_file": { + "$ref": "#/$defs/file", + "title": "Service DLL" + }, + "service_error_control": { + "title": "Service Error Control", + "type": "string" + }, + "service_error_control_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Error Control ID", + "type": "integer" + }, + "service_file": { + "$ref": "#/$defs/file", + "title": "Service File" + }, + "service_start_name": { + "title": "Service Start Name", + "type": "string" + }, + "service_start_type": { + "title": "Service Start Type", + "type": "string" + }, + "service_start_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Service Start Type ID", + "type": "integer" + }, + "service_type": { + "title": "Service Type", + "type": "string" + }, + "service_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Type ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Windows Service", + "type": "object" + }, + "group": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "privileges": { + "items": { + "type": "string" + }, + "title": "Privileges", + "type": "array" + }, + "type": { + "title": "Group Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "Group", + "type": "object" + }, + "device": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "region": { + "title": "Region", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "os_machine_uuid": { + "title": "OS Machine UUID", + "type": "string" + }, + "hypervisor": { + "title": "Hypervisor", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "imei_list": { + "items": { + "type": "string" + }, + "title": "IMEI List", + "type": "array" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "is_shared": { + "title": "Shared Device", + "type": "boolean" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "meid": { + "title": "MEID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "is_personal": { + "title": "Personal Device", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "iccid": { + "title": "ICCID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "udid": { + "title": "Unique Device Identifier", + "type": "string" + }, + "model": { + "title": "Model", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "boot_time": { + "title": "Boot Time", + "type": "integer" + }, + "is_supervised": { + "title": "Supervised Device", + "type": "boolean" + }, + "is_managed": { + "title": "Managed Device", + "type": "boolean" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "is_trusted": { + "title": "Trusted Device", + "type": "boolean" + }, + "boot_uid": { + "title": "Boot UID", + "type": "string" + }, + "network_interfaces": { + "items": { + "$ref": "#/$defs/network_interface" + }, + "title": "Network Interfaces", + "type": "array" + }, + "autoscale_uid": { + "title": "Autoscale UID", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "subnet": { + "title": "Subnet", + "type": "string" + }, + "eid": { + "title": "EID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "imei": { + "title": "IMEI", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "is_mobile_account_active": { + "title": "Mobile Account Active", + "type": "boolean" + }, + "is_compliant": { + "title": "Compliant Device", + "type": "boolean" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Device", + "type": "object" + }, + "campaign": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Campaign", + "type": "object" + }, + "cwe": { + "additionalProperties": false, + "properties": { + "caption": { + "title": "Caption", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "CWE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CWE", + "type": "object" + }, + "digital_signature": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "developer_uid": { + "title": "Developer UID", + "type": "string" + }, + "digest": { + "$ref": "#/$defs/fingerprint", + "title": "Message Digest" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "State ID", + "type": "integer" + } + }, + "required": [ + "algorithm_id" + ], + "title": "Digital Signature", + "type": "object" + }, + "sbom": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "package": { + "$ref": "#/$defs/package", + "title": "Software Package" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "software_components": { + "items": { + "$ref": "#/$defs/software_component" + }, + "title": "Software Components", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "SBOM ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "package", + "software_components" + ], + "title": "Software Bill of Materials", + "type": "object" + }, + "malware_scan_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "end_time": { + "title": "End Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_files": { + "title": "Scanned Files", + "type": "integer" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "num_volumes": { + "title": "Number of Volumes", + "type": "integer" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Scan UID", + "type": "string" + }, + "unique_malware_count": { + "title": "Unique Malware Count", + "type": "integer" + } + }, + "required": [ + "type_id" + ], + "title": "Malware Scan Info", + "type": "object" + }, + "kill_chain_phase": { + "additionalProperties": false, + "properties": { + "phase": { + "title": "Kill Chain Phase", + "type": "string" + }, + "phase_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Kill Chain Phase ID", + "type": "integer" + } + }, + "required": [ + "phase_id" + ], + "title": "Kill Chain Phase", + "type": "object" + }, + "organization": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "ou_name": { + "title": "Org Unit Name", + "type": "string" + }, + "ou_uid": { + "title": "Org Unit ID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Organization", + "type": "object" + }, + "encryption_details": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Encryption Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Encryption Algorithm ID", + "type": "integer" + }, + "key_length": { + "title": "Encryption Key Length", + "type": "integer" + }, + "key_uid": { + "title": "Key UID", + "type": "string" + }, + "type": { + "title": "Encryption Type", + "type": "string" + } + }, + "title": "Encryption Details", + "type": "object" + }, + "logger": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "log_format": { + "title": "Log Format", + "type": "string" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Logger", + "type": "object" + }, + "feature": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Feature", + "type": "object" + }, + "timespan": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "duration" + ] + }, + { + "required": [ + "duration_days" + ] + }, + { + "required": [ + "duration_hours" + ] + }, + { + "required": [ + "duration_mins" + ] + }, + { + "required": [ + "duration_months" + ] + }, + { + "required": [ + "duration_secs" + ] + }, + { + "required": [ + "duration_weeks" + ] + }, + { + "required": [ + "duration_years" + ] + }, + { + "required": [ + "end_time" + ] + }, + { + "required": [ + "start_time" + ] + } + ], + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "duration_days": { + "title": "Duration Days", + "type": "integer" + }, + "duration_hours": { + "title": "Duration Hours", + "type": "integer" + }, + "duration_mins": { + "title": "Duration Minutes", + "type": "integer" + }, + "duration_months": { + "title": "Duration Months", + "type": "integer" + }, + "duration_secs": { + "title": "Duration Seconds", + "type": "integer" + }, + "duration_weeks": { + "title": "Duration Weeks", + "type": "integer" + }, + "duration_years": { + "title": "Duration Years", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Time Span Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Time Span Type ID", + "type": "integer" + } + }, + "title": "Time Span", + "type": "object" + }, + "rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Rule", + "type": "object" + }, + "software_component": { + "additionalProperties": false, + "properties": { + "author": { + "title": "Author", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "related_component": { + "title": "Related Component", + "type": "string" + }, + "relationship": { + "title": "Relationship", + "type": "string" + }, + "relationship_id": { + "enum": [ + 0, + 1, + 99 + ], + "title": "Relationship ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Component", + "type": "object" + }, + "agent": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Agent Name", + "type": "string" + }, + "policies": { + "items": { + "$ref": "#/$defs/policy" + }, + "title": "Agent Policies", + "type": "array" + }, + "type": { + "title": "Agent Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Agent ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate Agent ID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Agent Version", + "type": "string" + } + }, + "title": "Agent", + "type": "object" + }, + "whois": { + "additionalProperties": false, + "properties": { + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "created_time": { + "title": "Registered At", + "type": "integer" + }, + "dnssec_status": { + "title": "DNSSEC Status", + "type": "string" + }, + "dnssec_status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "DNSSEC Status ID", + "type": "integer" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "domain_contacts": { + "items": { + "$ref": "#/$defs/domain_contact" + }, + "title": "Domain Contacts", + "type": "array" + }, + "email_addr": { + "title": "Registrar Abuse Email Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "last_seen_time": { + "title": "Last Updated At", + "type": "integer" + }, + "name_servers": { + "items": { + "type": "string" + }, + "title": "Name Servers", + "type": "array" + }, + "phone_number": { + "title": "Registrar Abuse Phone Number", + "type": "string" + }, + "registrar": { + "title": "Domain Registrar", + "type": "string" + }, + "status": { + "title": "Domain Status", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Subdomains", + "type": "array" + }, + "subnet": { + "title": "Subnet Block", + "type": "string" + } + }, + "title": "WHOIS", + "type": "object" + }, + "key_value_object": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "value" + ] + }, + { + "required": [ + "values" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + }, + "values": { + "items": { + "type": "string" + }, + "title": "Values", + "type": "array" + } + }, + "required": [ + "name" + ], + "title": "Key:Value object", + "type": "object" + }, + "metadata": { + "additionalProperties": false, + "properties": { + "log_format": { + "title": "Log Source Format", + "type": "string" + }, + "transformation_info_list": { + "items": { + "$ref": "#/$defs/transformation_info" + }, + "title": "Transformation Info", + "type": "array" + }, + "uid": { + "title": "Event UID", + "type": "string" + }, + "original_event_uid": { + "title": "Original Event ID", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "extensions": { + "items": { + "$ref": "#/$defs/extension" + }, + "title": "Schema Extensions", + "type": "array" + }, + "source": { + "title": "Source", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "loggers": { + "items": { + "$ref": "#/$defs/logger" + }, + "title": "Loggers", + "type": "array" + }, + "processed_time": { + "title": "Processed Time", + "type": "integer" + }, + "log_source": { + "title": "Log Source", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "debug": { + "items": { + "type": "string" + }, + "title": "Debug Information", + "type": "array" + }, + "profiles": { + "items": { + "type": "string" + }, + "title": "Profiles", + "type": "array" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "event_code": { + "title": "Event Code", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "reporter": { + "$ref": "#/$defs/reporter", + "title": "Reporter" + }, + "correlation_uid": { + "title": "Correlation UID", + "type": "string" + }, + "extension": { + "$ref": "#/$defs/extension", + "title": "Schema Extension" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "sequence": { + "title": "Sequence Number", + "type": "integer" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "original_time": { + "title": "Original Time", + "type": "string" + }, + "total_queued_duration": { + "$ref": "#/$defs/timespan", + "title": "Total Queued Duration" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "product", + "version" + ], + "title": "Metadata", + "type": "object" + }, + "node": { + "additionalProperties": false, + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Node", + "type": "object" + }, + "product": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "feature": { + "$ref": "#/$defs/feature", + "title": "Feature" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Product", + "type": "object" + }, + "advisory": { + "additionalProperties": false, + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "Related CVEs", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Advisory ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Advisory", + "type": "object" + }, + "network_endpoint": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Network Endpoint", + "type": "object" + }, + "d3f_tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Tactic", + "type": "object" + }, + "cve": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "cvss": { + "items": { + "$ref": "#/$defs/cvss" + }, + "title": "CVSS Score", + "type": "array" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "cwe_uid": { + "title": "CWE UID", + "type": "string" + }, + "cwe_url": { + "title": "CWE URL", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "epss": { + "$ref": "#/$defs/epss", + "title": "EPSS" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "title": { + "title": "Title", + "type": "string" + }, + "type": { + "title": "Vulnerability Type", + "type": "string" + }, + "uid": { + "title": "CVE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CVE", + "type": "object" + }, + "domain_contact": { + "additionalProperties": false, + "properties": { + "email_addr": { + "title": "Contact Email", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Contact Location Information" + }, + "name": { + "title": "Name", + "type": "string" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "type": { + "title": "Domain Contact Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Domain Contact Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Domain Contact", + "type": "object" + }, + "policy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "type" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "is_applied": { + "title": "Applied", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Policy", + "type": "object" + }, + "span": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "end_time", + "start_time", + "uid" + ], + "title": "Span", + "type": "object" + } + }, + "$id": "https://schema.ocsf.io/schema/classes/api_activity", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "unmapped": { + "$ref": "#/$defs/object", + "title": "Unmapped Data" + }, + "message": { + "title": "Message", + "type": "string" + }, + "http_response": { + "$ref": "#/$defs/http_response", + "title": "HTTP Response" + }, + "disposition": { + "title": "Disposition", + "type": "string" + }, + "category_uid": { + "const": 6, + "title": "Category ID", + "type": "integer" + }, + "action": { + "title": "Action", + "type": "string" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "api": { + "$ref": "#/$defs/api", + "title": "API Details" + }, + "message_context": { + "$ref": "#/$defs/message_context", + "title": "Message Context" + }, + "risk_details": { + "title": "Risk Details", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "status_detail": { + "title": "Status Detail", + "type": "string" + }, + "confidence_score": { + "title": "Confidence Score", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "disposition_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 99 + ], + "title": "Disposition ID", + "type": "integer" + }, + "type_name": { + "title": "Type Name", + "type": "string" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "raw_data": { + "title": "Raw Data", + "type": "string" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "actor": { + "$ref": "#/$defs/actor", + "title": "Actor" + }, + "trace": { + "$ref": "#/$defs/trace", + "title": "Trace" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "status": { + "title": "Status", + "type": "string" + }, + "ai_model": { + "$ref": "#/$defs/ai_model", + "title": "AI Model" + }, + "activity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Activity ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "resources": { + "items": { + "$ref": "#/$defs/resource_details" + }, + "title": "Resources Array", + "type": "array" + }, + "observables": { + "items": { + "$ref": "#/$defs/observable" + }, + "title": "Observables", + "type": "array" + }, + "enrichments": { + "items": { + "$ref": "#/$defs/enrichment" + }, + "title": "Enrichments", + "type": "array" + }, + "http_request": { + "$ref": "#/$defs/http_request", + "title": "HTTP Request" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "raw_data_hash": { + "$ref": "#/$defs/fingerprint", + "title": "Raw Data Hash" + }, + "action_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Action ID", + "type": "integer" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "firewall_rule": { + "$ref": "#/$defs/firewall_rule", + "title": "Firewall Rule" + }, + "class_uid": { + "const": 6003, + "title": "Class ID", + "type": "integer" + }, + "activity_name": { + "title": "Activity", + "type": "string" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "class_name": { + "title": "Class", + "type": "string" + }, + "timezone_offset": { + "title": "Timezone Offset", + "type": "integer" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "raw_data_size": { + "title": "Raw Data Size", + "type": "integer" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "dst_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Destination Endpoint" + }, + "is_alert": { + "title": "Alert", + "type": "boolean" + }, + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "category_name": { + "title": "Category", + "type": "string" + }, + "malware_scan_info": { + "$ref": "#/$defs/malware_scan_info", + "title": "Malware Scan Info" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "count": { + "title": "Count", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "src_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Source Endpoint" + }, + "metadata": { + "$ref": "#/$defs/metadata", + "title": "Metadata" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "required": [ + "activity_id", + "actor", + "api", + "category_uid", + "class_uid", + "metadata", + "severity_id", + "src_endpoint", + "time", + "type_uid" + ], + "title": "API Activity", + "type": "object" +} diff --git a/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+host+trace.json b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+host+trace.json new file mode 100644 index 0000000000..ee9a0d4e14 --- /dev/null +++ b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+host+trace.json @@ -0,0 +1,8106 @@ +{ + "$defs": { + "kb_article": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "src_url" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "KB Article", + "type": "object" + }, + "long_string": { + "additionalProperties": false, + "properties": { + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "value" + ], + "title": "Long String", + "type": "object" + }, + "enrichment": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "short_desc": { + "title": "Short Description", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "data", + "name", + "value" + ], + "title": "Enrichment", + "type": "object" + }, + "ldap_person": { + "additionalProperties": false, + "properties": { + "cost_center": { + "title": "Cost Center", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "deleted_time": { + "title": "Deleted Time", + "type": "integer" + }, + "department": { + "title": "Department", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "email_addrs": { + "items": { + "type": "string" + }, + "title": "Email Addresses", + "type": "array" + }, + "employee_uid": { + "title": "Employee ID", + "type": "string" + }, + "given_name": { + "title": "Given Name", + "type": "string" + }, + "hire_time": { + "title": "Hire Time", + "type": "integer" + }, + "job_title": { + "title": "Job Title", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "last_login_time": { + "title": "Last Login", + "type": "integer" + }, + "ldap_cn": { + "title": "LDAP Common Name", + "type": "string" + }, + "ldap_dn": { + "title": "LDAP Distinguished Name", + "type": "string" + }, + "leave_time": { + "title": "Leave Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "manager": { + "$ref": "#/$defs/user", + "title": "Manager" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "office_location": { + "title": "Office Location", + "type": "string" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "surname": { + "title": "Surname", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + } + }, + "title": "LDAP Person", + "type": "object" + }, + "email_auth": { + "additionalProperties": false, + "properties": { + "dkim": { + "title": "DKIM Status", + "type": "string" + }, + "dkim_domain": { + "title": "DKIM Domain", + "type": "string" + }, + "dkim_signature": { + "title": "DKIM Signature", + "type": "string" + }, + "dmarc": { + "title": "DMARC Status", + "type": "string" + }, + "dmarc_override": { + "title": "DMARC Override", + "type": "string" + }, + "dmarc_policy": { + "title": "DMARC Policy", + "type": "string" + }, + "spf": { + "title": "SPF Status", + "type": "string" + } + }, + "title": "Email Authentication", + "type": "object" + }, + "epss": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "percentile": { + "title": "EPSS Percentile", + "type": "number" + }, + "score": { + "title": "EPPS Score", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "score" + ], + "title": "EPSS", + "type": "object" + }, + "device_hw_info": { + "additionalProperties": false, + "properties": { + "bios_date": { + "title": "BIOS Date", + "type": "string" + }, + "bios_manufacturer": { + "title": "BIOS Manufacturer", + "type": "string" + }, + "bios_ver": { + "title": "BIOS Version", + "type": "string" + }, + "chassis": { + "title": "Chassis", + "type": "string" + }, + "cpu_architecture": { + "title": "CPU Architecture", + "type": "string" + }, + "cpu_architecture_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "CPU Architecture ID", + "type": "integer" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "cpu_cores": { + "title": "CPU Cores", + "type": "integer" + }, + "cpu_count": { + "title": "CPU Count", + "type": "integer" + }, + "cpu_speed": { + "title": "Processor Speed", + "type": "integer" + }, + "cpu_type": { + "title": "Processor Type", + "type": "string" + }, + "desktop_display": { + "$ref": "#/$defs/display", + "title": "Desktop Display" + }, + "gpu_count": { + "title": "GPU Count", + "type": "integer" + }, + "gpu_info_list": { + "items": { + "$ref": "#/$defs/gpu_info" + }, + "title": "GPU information", + "type": "array" + }, + "keyboard_info": { + "$ref": "#/$defs/keyboard_info", + "title": "Keyboard Information" + }, + "ram_size": { + "title": "RAM Size", + "type": "integer" + }, + "serial_number": { + "title": "Serial Number", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Device Hardware Info", + "type": "object" + }, + "network_proxy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Network Proxy Endpoint", + "type": "object" + }, + "data_classification": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "category_id" + ] + }, + { + "required": [ + "confidentiality_id" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "category_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Category ID", + "type": "integer" + }, + "classifier_details": { + "$ref": "#/$defs/classifier_details", + "title": "Classifier Details" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "discovery_details": { + "items": { + "$ref": "#/$defs/discovery_details" + }, + "title": "Discovery Details", + "type": "array" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "status_details": { + "items": { + "type": "string" + }, + "title": "Status Details", + "type": "array" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Data Classification", + "type": "object" + }, + "http_response": { + "additionalProperties": false, + "properties": { + "body_length": { + "title": "Response Body Length", + "type": "integer" + }, + "code": { + "title": "Response Code", + "type": "integer" + }, + "content_type": { + "title": "HTTP Content Type", + "type": "string" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "latency": { + "title": "Latency", + "type": "integer" + }, + "length": { + "title": "Response Length", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "code" + ], + "title": "HTTP Response", + "type": "object" + }, + "resource_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "criticality": { + "title": "Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Resource Relationship" + }, + "role": { + "title": "Role", + "type": "string" + }, + "role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Role ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Resource Details", + "type": "object" + }, + "display": { + "additionalProperties": false, + "properties": { + "color_depth": { + "title": "Color Depth", + "type": "integer" + }, + "physical_height": { + "title": "Physical Height", + "type": "integer" + }, + "physical_orientation": { + "title": "Physical Orientation", + "type": "integer" + }, + "physical_width": { + "title": "Physical Width", + "type": "integer" + }, + "scale_factor": { + "title": "Scale Factor", + "type": "integer" + } + }, + "title": "Display", + "type": "object" + }, + "scim": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "error_message": { + "title": "Last Error Message", + "type": "string" + }, + "is_group_provisioning_enabled": { + "title": "SCIM Group Provisioning Enabled", + "type": "boolean" + }, + "is_user_provisioning_enabled": { + "title": "SCIM User Provisioning Enabled", + "type": "boolean" + }, + "last_run_time": { + "title": "Last Sync Time", + "type": "integer" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "scim_group_schema": { + "title": "SCIM Group Schema" + }, + "scim_user_schema": { + "title": "SCIM User Schema" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "External ID", + "type": "string" + }, + "url_string": { + "title": "SCIM Endpoint URL", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + }, + "version": { + "title": "SCIM Version", + "type": "string" + } + }, + "title": "SCIM", + "type": "object" + }, + "programmatic_credential": { + "additionalProperties": false, + "properties": { + "last_used_time": { + "title": "Last Used Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Programmatic Credential", + "type": "object" + }, + "os": { + "additionalProperties": false, + "properties": { + "build": { + "title": "OS Build", + "type": "string" + }, + "country": { + "title": "Country", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "edition": { + "title": "OS Edition", + "type": "string" + }, + "kernel_release": { + "title": "Kernel Release", + "type": "string" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "sp_name": { + "title": "OS Service Pack", + "type": "string" + }, + "sp_ver": { + "title": "OS Service Pack Version", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 99, + 100, + 101, + 200, + 201, + 300, + 301, + 302, + 400, + 401, + 402 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "Operating System (OS)", + "type": "object" + }, + "tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Tactic", + "type": "object" + }, + "dns_answer": { + "additionalProperties": false, + "properties": { + "class": { + "title": "Resource Record Class", + "type": "string" + }, + "flag_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "type": "integer" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "packet_uid": { + "title": "Packet UID", + "type": "integer" + }, + "rdata": { + "title": "DNS RData", + "type": "string" + }, + "ttl": { + "title": "TTL", + "type": "integer" + }, + "type": { + "title": "Resource Record Type", + "type": "string" + } + }, + "required": [ + "rdata" + ], + "title": "DNS Answer", + "type": "object" + }, + "discovery_details": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "occurrence_details": { + "$ref": "#/$defs/occurrence_details", + "title": "Occurrence Details" + }, + "occurrences": { + "items": { + "$ref": "#/$defs/occurrence_details" + }, + "title": "Occurrences", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "title": "Discovery Details", + "type": "object" + }, + "sso": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "SAML Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "duration_mins": { + "title": "SSO Session Duration", + "type": "integer" + }, + "idle_timeout": { + "title": "SSO Idle Timeout", + "type": "integer" + }, + "login_endpoint": { + "title": "SSO Login Endpoint", + "type": "string" + }, + "logout_endpoint": { + "title": "SSO Logout Endpoint", + "type": "string" + }, + "metadata_endpoint": { + "title": "SSO Metadata Endpoint", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "title": "Scopes", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + } + }, + "title": "SSO", + "type": "object" + }, + "token": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_renewable": { + "title": "Renewable", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Token", + "type": "object" + }, + "api": { + "additionalProperties": false, + "properties": { + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "request": { + "$ref": "#/$defs/request", + "title": "API Request Details" + }, + "response": { + "$ref": "#/$defs/response", + "title": "API Response Details" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "token": { + "$ref": "#/$defs/token", + "title": "Token" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "operation" + ], + "title": "API", + "type": "object" + }, + "d3fend": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "d3f_tactic" + ] + }, + { + "required": [ + "d3f_technique" + ] + } + ], + "properties": { + "d3f_tactic": { + "$ref": "#/$defs/d3f_tactic", + "title": "MITRE D3FEND\u2122 Tactic" + }, + "d3f_technique": { + "$ref": "#/$defs/d3f_technique", + "title": "MITRE D3FEND\u2122 Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122", + "type": "object" + }, + "reputation": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Reputation Score", + "type": "number" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "score": { + "title": "Reputation Score", + "type": "string" + }, + "score_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "Reputation Score ID", + "type": "integer" + } + }, + "required": [ + "base_score", + "score_id" + ], + "title": "Reputation", + "type": "object" + }, + "mitigation": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "countermeasures": { + "items": { + "$ref": "#/$defs/d3fend" + }, + "title": "Countermeasures", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Mitigation", + "type": "object" + }, + "location": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "city" + ] + }, + { + "required": [ + "country" + ] + }, + { + "required": [ + "postal_code" + ] + }, + { + "required": [ + "region" + ] + } + ], + "properties": { + "aerial_height": { + "title": "Aerial Height", + "type": "string" + }, + "city": { + "title": "City", + "type": "string" + }, + "continent": { + "title": "Continent", + "type": "string" + }, + "coordinates": { + "items": { + "type": "number" + }, + "title": "Coordinates", + "type": "array" + }, + "country": { + "title": "Country", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "geodetic_altitude": { + "title": "Geodetic Altitude", + "type": "string" + }, + "geodetic_vertical_accuracy": { + "title": "Geodetic Vertical Accuracy", + "type": "string" + }, + "geohash": { + "title": "Geohash", + "type": "string" + }, + "horizontal_accuracy": { + "title": "Horizontal Accuracy", + "type": "string" + }, + "is_on_premises": { + "title": "On-Premises", + "type": "boolean" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "lat": { + "title": "Latitude", + "type": "number" + }, + "long": { + "title": "Longitude", + "type": "number" + }, + "postal_code": { + "title": "Postal Code", + "type": "string" + }, + "pressure_altitude": { + "title": "Pressure Altitude", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + } + }, + "title": "Geo Location", + "type": "object" + }, + "idp": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "auth_factors": { + "items": { + "$ref": "#/$defs/auth_factor" + }, + "title": "Authentication Factors", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprint": { + "$ref": "#/$defs/fingerprint", + "title": "Certificate Fingerprint" + }, + "has_mfa": { + "title": "MFA Enforced", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scim": { + "$ref": "#/$defs/scim", + "title": "SCIM" + }, + "sso": { + "$ref": "#/$defs/sso", + "title": "SSO" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "Configuration URL", + "type": "string" + } + }, + "title": "Identity Provider", + "type": "object" + }, + "message_context": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "application" + ] + }, + { + "required": [ + "service" + ] + } + ], + "properties": { + "ai_role": { + "title": "AI Role", + "type": "string" + }, + "ai_role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "AI Role ID", + "type": "integer" + }, + "application": { + "$ref": "#/$defs/application", + "title": "Application" + }, + "completion_tokens": { + "title": "Completion Tokens", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "prompt_tokens": { + "title": "Prompt Tokens", + "type": "integer" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "total_tokens": { + "title": "Total Tokens", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Message Context", + "type": "object" + }, + "process": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ancestry": { + "items": { + "$ref": "#/$defs/process_entity" + }, + "title": "Ancestry", + "type": "array" + }, + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "environment_variables": { + "items": { + "$ref": "#/$defs/environment_variable" + }, + "title": "Environment Variables", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hosted_services": { + "items": { + "$ref": "#/$defs/win_win_service" + }, + "title": "Hosted Services", + "type": "array" + }, + "integrity": { + "title": "Integrity", + "type": "string" + }, + "integrity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Integrity Level", + "type": "integer" + }, + "lineage": { + "items": { + "type": "string" + }, + "title": "Lineage", + "type": "array" + }, + "loaded_modules": { + "items": { + "type": "string" + }, + "title": "Loaded Modules", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_process": { + "$ref": "#/$defs/process", + "title": "Parent Process" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "ptid": { + "title": "Process Thread ID", + "type": "integer" + }, + "sandbox": { + "title": "Sandbox", + "type": "string" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "terminated_time": { + "title": "Terminated Time", + "type": "integer" + }, + "tid": { + "title": "Thread ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + }, + "working_directory": { + "title": "Working Directory", + "type": "string" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + } + }, + "title": "Process", + "type": "object" + }, + "vulnerability": { + "additionalProperties": false, + "oneOf": [ + { + "not": { + "required": [ + "cve", + "cwe" + ] + }, + "required": [ + "advisory" + ] + }, + { + "not": { + "required": [ + "advisory", + "cwe" + ] + }, + "required": [ + "cve" + ] + }, + { + "not": { + "required": [ + "advisory", + "cve" + ] + }, + "required": [ + "cwe" + ] + } + ], + "properties": { + "advisory": { + "$ref": "#/$defs/advisory", + "title": "Security Advisory" + }, + "affected_code": { + "items": { + "$ref": "#/$defs/affected_code" + }, + "title": "Affected Code", + "type": "array" + }, + "affected_packages": { + "items": { + "$ref": "#/$defs/affected_package" + }, + "title": "Affected Software Packages", + "type": "array" + }, + "category": { + "title": "Category", + "type": "string" + }, + "cve": { + "$ref": "#/$defs/cve", + "title": "CVE" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "dependency_chain": { + "title": "Dependency Chain", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "exploit_last_seen_time": { + "title": "Exploit Last Seen Time", + "type": "integer" + }, + "exploit_ref_url": { + "title": "Exploit URL", + "type": "string" + }, + "exploit_requirement": { + "title": "Exploit Requirement", + "type": "string" + }, + "exploit_type": { + "title": "Exploit Type", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "fix_coverage": { + "title": "Fix Coverage", + "type": "string" + }, + "fix_coverage_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Fix Coverage ID", + "type": "integer" + }, + "is_exploit_available": { + "title": "Exploit Availability", + "type": "boolean" + }, + "is_fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "packages": { + "items": { + "$ref": "#/$defs/package" + }, + "title": "Software Packages", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_vulnerabilities": { + "items": { + "type": "string" + }, + "title": "Related Vulnerability IDs", + "type": "array" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Vulnerability Details", + "type": "object" + }, + "affected_code": { + "additionalProperties": false, + "properties": { + "end_column": { + "title": "End Column", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "rule": { + "$ref": "#/$defs/rule", + "title": "Related Rule" + }, + "start_column": { + "title": "Start Column", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "required": [ + "file" + ], + "title": "Affected Code", + "type": "object" + }, + "keyboard_info": { + "additionalProperties": false, + "properties": { + "function_keys": { + "title": "Function Keys", + "type": "integer" + }, + "ime": { + "title": "IME", + "type": "string" + }, + "keyboard_layout": { + "title": "Keyboard Layout", + "type": "string" + }, + "keyboard_subtype": { + "title": "Keyboard Subtype", + "type": "integer" + }, + "keyboard_type": { + "title": "Keyboard Type", + "type": "string" + } + }, + "title": "Keyboard Information", + "type": "object" + }, + "certificate": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "is_self_signed": { + "title": "Certificate Self-Signed", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Distinguished Name", + "type": "string" + }, + "sans": { + "items": { + "$ref": "#/$defs/san" + }, + "title": "Subject Alternative Names", + "type": "array" + }, + "serial_number": { + "title": "Certificate Serial Number", + "type": "string" + }, + "subject": { + "title": "Subject Distinguished Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "issuer", + "serial_number" + ], + "title": "Digital Certificate", + "type": "object" + }, + "graph": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "edges": { + "items": { + "$ref": "#/$defs/edge" + }, + "title": "Edges", + "type": "array" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "nodes": { + "items": { + "$ref": "#/$defs/node" + }, + "title": "Nodes", + "type": "array" + }, + "query_language": { + "title": "Query Language", + "type": "string" + }, + "query_language_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Query Language ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "nodes" + ], + "title": "Graph", + "type": "object" + }, + "account": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "is_disabled": { + "title": "Disabled", + "type": "boolean" + }, + "is_locked": { + "title": "Locked", + "type": "boolean" + }, + "is_on_premises_sync_enabled": { + "title": "On-Premises Sync Enabled", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Account", + "type": "object" + }, + "authorization": { + "additionalProperties": false, + "properties": { + "decision": { + "title": "Authorization Decision/Outcome", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "title": "Authorization Result", + "type": "object" + }, + "autonomous_system": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "number" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "number": { + "title": "Number", + "type": "integer" + } + }, + "title": "Autonomous System", + "type": "object" + }, + "classifier_details": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "Classifier Details", + "type": "object" + }, + "session": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "expiration_reason": { + "title": "Expiration Reason", + "type": "string" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_mfa": { + "title": "Multi Factor Authentication", + "type": "boolean" + }, + "is_remote": { + "title": "Remote", + "type": "boolean" + }, + "is_vpn": { + "title": "VPN Session", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "terminal": { + "title": "Terminal", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + } + }, + "title": "Session", + "type": "object" + }, + "d3f_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Technique", + "type": "object" + }, + "image": { + "additionalProperties": false, + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Image", + "type": "object" + }, + "application": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "criticality": { + "title": "Business Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Application Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Application Name", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Application Relationship" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "sbom": { + "$ref": "#/$defs/sbom", + "title": "Related SBOM" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Application Type", + "type": "string" + }, + "uid": { + "title": "Application ID", + "type": "string" + }, + "uid_alt": { + "title": "Application Alternative ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "version": { + "title": "Application Version", + "type": "string" + } + }, + "title": "Application", + "type": "object" + }, + "script": { + "additionalProperties": false, + "properties": { + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "script_content": { + "$ref": "#/$defs/long_string", + "title": "Script Content" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "script_content", + "type_id" + ], + "title": "Script", + "type": "object" + }, + "affected_package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "fixed_in_version": { + "title": "Fixed In Version", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Affected Software Package", + "type": "object" + }, + "cis_control": { + "additionalProperties": false, + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "CIS Control", + "type": "object" + }, + "analytic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Analytic", + "type": "object" + }, + "edge": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "relation": { + "title": "Relation", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "target": { + "title": "Target", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "source", + "target" + ], + "title": "Edge", + "type": "object" + }, + "actor": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "app_name" + ] + }, + { + "required": [ + "app_uid" + ] + }, + { + "required": [ + "invoked_by" + ] + }, + { + "required": [ + "process" + ] + }, + { + "required": [ + "session" + ] + }, + { + "required": [ + "user" + ] + } + ], + "properties": { + "app_name": { + "title": "Application Name", + "type": "string" + }, + "app_uid": { + "title": "Application ID", + "type": "string" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "idp": { + "$ref": "#/$defs/idp", + "title": "Identity Provider" + }, + "invoked_by": { + "title": "Invoked by", + "type": "string" + }, + "process": { + "$ref": "#/$defs/process", + "title": "Process" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + } + }, + "title": "Actor", + "type": "object" + }, + "cloud": { + "additionalProperties": false, + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "cloud_partition": { + "title": "Cloud Partition", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "project_uid": { + "title": "Project ID", + "type": "string" + }, + "provider": { + "title": "Cloud Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + }, + "zone": { + "title": "Cloud Availability Zone", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "Cloud", + "type": "object" + }, + "trace": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "span": { + "$ref": "#/$defs/span", + "title": "Span" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Trace", + "type": "object" + }, + "ai_model": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ai_provider": { + "title": "AI Provider", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "ai_provider" + ], + "title": "AI Model", + "type": "object" + }, + "file": { + "additionalProperties": false, + "properties": { + "parent_folder": { + "title": "Parent Folder", + "type": "string" + }, + "is_system": { + "title": "System", + "type": "boolean" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "accessor": { + "$ref": "#/$defs/user", + "title": "Accessor" + }, + "is_readonly": { + "title": "Read-Only", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "ext": { + "title": "File Extension", + "type": "string" + }, + "company_name": { + "title": "Company Name", + "type": "string" + }, + "attributes": { + "title": "Attributes", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uri": { + "title": "File URI", + "type": "string" + }, + "is_deleted": { + "title": "Deleted", + "type": "boolean" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "drive_type": { + "title": "Drive Type", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "imported_symbols": { + "items": { + "type": "string" + }, + "title": "Imported Symbols", + "type": "array" + }, + "signature": { + "$ref": "#/$defs/digital_signature", + "title": "Digital Signature" + }, + "is_encrypted": { + "title": "Encrypted", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "accessed_time": { + "title": "Accessed Time", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "internal_name": { + "title": "Internal Name", + "type": "string" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "drive_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Drive Type ID", + "type": "integer" + }, + "security_descriptor": { + "title": "Security Descriptor", + "type": "string" + }, + "volume": { + "title": "Volume", + "type": "string" + }, + "modifier": { + "$ref": "#/$defs/user", + "title": "Modifier" + }, + "mime_type": { + "title": "MIME type", + "type": "string" + }, + "encryption_details": { + "$ref": "#/$defs/encryption_details", + "title": "Encryption Details" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Digital Signatures", + "type": "array" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "File", + "type": "object" + }, + "remediation": { + "additionalProperties": false, + "properties": { + "cis_controls": { + "items": { + "$ref": "#/$defs/cis_control" + }, + "title": "CIS Controls", + "type": "array" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + } + }, + "required": [ + "desc" + ], + "title": "Remediation", + "type": "object" + }, + "email": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "from" + ] + }, + { + "required": [ + "to" + ] + } + ], + "properties": { + "cc": { + "items": { + "type": "string" + }, + "title": "Cc", + "type": "array" + }, + "cc_mailboxes": { + "items": { + "type": "string" + }, + "title": "Cc Mailboxes", + "type": "array" + }, + "delivered_to": { + "title": "Delivered To", + "type": "string" + }, + "delivered_to_list": { + "items": { + "type": "string" + }, + "title": "Delivered To List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "from": { + "title": "From", + "type": "string" + }, + "from_list": { + "items": { + "type": "string" + }, + "title": "From List", + "type": "array" + }, + "from_mailbox": { + "title": "From Mailbox", + "type": "string" + }, + "from_mailboxes": { + "items": { + "type": "string" + }, + "title": "From Mailboxes", + "type": "array" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "is_read": { + "title": "Read", + "type": "boolean" + }, + "message_uid": { + "title": "Message UID", + "type": "string" + }, + "raw_header": { + "title": "Raw Header", + "type": "string" + }, + "reply_to": { + "title": "Reply To", + "type": "string" + }, + "reply_to_list": { + "items": { + "type": "string" + }, + "title": "Reply To List", + "type": "array" + }, + "reply_to_mailboxes": { + "items": { + "type": "string" + }, + "title": "Reply To Mailboxes", + "type": "array" + }, + "return_path": { + "title": "Return Path", + "type": "string" + }, + "sender": { + "title": "Sender", + "type": "string" + }, + "sender_mailbox": { + "title": "Sender Mailbox", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "smtp_from": { + "title": "SMTP From", + "type": "string" + }, + "smtp_to": { + "items": { + "type": "string" + }, + "title": "SMTP To", + "type": "array" + }, + "subject": { + "title": "Subject", + "type": "string" + }, + "to": { + "items": { + "type": "string" + }, + "title": "To", + "type": "array" + }, + "to_mailboxes": { + "items": { + "type": "string" + }, + "title": "To Mailboxes", + "type": "array" + }, + "uid": { + "title": "Email Thread UID", + "type": "string" + }, + "urls": { + "items": { + "$ref": "#/$defs/url" + }, + "title": "URLs", + "type": "array" + }, + "x_originating_ip": { + "items": { + "type": "string" + }, + "title": "X-Originating-IP", + "type": "array" + } + }, + "title": "Email", + "type": "object" + }, + "environment_variable": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Environment Variable", + "type": "object" + }, + "technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Technique", + "type": "object" + }, + "http_request": { + "additionalProperties": false, + "properties": { + "args": { + "title": "HTTP Arguments", + "type": "string" + }, + "body_length": { + "title": "Request Body Length", + "type": "integer" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "http_method": { + "enum": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "title": "HTTP Method", + "type": "string" + }, + "length": { + "title": "Request Length", + "type": "integer" + }, + "referrer": { + "title": "HTTP Referrer", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "user_agent": { + "title": "HTTP User-Agent", + "type": "string" + }, + "version": { + "title": "HTTP Version", + "type": "string" + }, + "x_forwarded_for": { + "items": { + "type": "string" + }, + "title": "X-Forwarded-For", + "type": "array" + } + }, + "title": "HTTP Request", + "type": "object" + }, + "cvss": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Base Score", + "type": "number" + }, + "depth": { + "enum": [ + "Base", + "Environmental", + "Temporal" + ], + "title": "CVSS Depth", + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/$defs/metric" + }, + "title": "Metrics", + "type": "array" + }, + "overall_score": { + "title": "Overall Score", + "type": "number" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "vector_string": { + "title": "Vector String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "base_score", + "version" + ], + "title": "CVSS Score", + "type": "object" + }, + "fingerprint": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "algorithm_id", + "value" + ], + "title": "Fingerprint", + "type": "object" + }, + "process_entity": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "path" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Process Entity", + "type": "object" + }, + "observable": { + "additionalProperties": false, + "properties": { + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Observable", + "type": "object" + }, + "http_header": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "HTTP Header", + "type": "object" + }, + "attack": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "sub_technique" + ] + }, + { + "required": [ + "tactic" + ] + }, + { + "required": [ + "technique" + ] + } + ], + "properties": { + "mitigation": { + "$ref": "#/$defs/mitigation", + "title": "MITRE Mitigation" + }, + "sub_technique": { + "$ref": "#/$defs/sub_technique", + "title": "MITRE Sub-technique" + }, + "tactic": { + "$ref": "#/$defs/tactic", + "title": "MITRE Tactic" + }, + "tactics": { + "items": { + "$ref": "#/$defs/tactic" + }, + "title": "Tactics", + "type": "array" + }, + "technique": { + "$ref": "#/$defs/technique", + "title": "MITRE Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "type": "object" + }, + "network_interface": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "mac" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "open_ports": { + "items": { + "$ref": "#/$defs/port_info" + }, + "title": "Open Ports", + "type": "array" + }, + "subnet_prefix": { + "title": "Subnet Prefix Length", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Network Interface", + "type": "object" + }, + "user": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "account" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "forward_addr": { + "title": "Forwarding Address", + "type": "string" + }, + "full_name": { + "title": "Full Name", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "has_mfa": { + "title": "MFA Assigned", + "type": "boolean" + }, + "ldap_person": { + "$ref": "#/$defs/ldap_person", + "title": "LDAP Person" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "programmatic_credentials": { + "items": { + "$ref": "#/$defs/programmatic_credential" + }, + "title": "Programmatic Credentials", + "type": "array" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "User", + "type": "object" + }, + "auth_factor": { + "additionalProperties": false, + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "factor_type": { + "title": "Factor Type", + "type": "string" + }, + "factor_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Factor Type ID", + "type": "integer" + }, + "is_hotp": { + "title": "HMAC-based One-time Password (HOTP)", + "type": "boolean" + }, + "is_totp": { + "title": "Time-based One-time Password (TOTP)", + "type": "boolean" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "security_questions": { + "items": { + "type": "string" + }, + "title": "Security Questions", + "type": "array" + } + }, + "required": [ + "factor_type_id" + ], + "title": "Authentication Factor", + "type": "object" + }, + "osint": { + "additionalProperties": false, + "properties": { + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "email_auth": { + "$ref": "#/$defs/email_auth", + "title": "Related Email Authentication" + }, + "external_uid": { + "title": "External ID", + "type": "string" + }, + "tlp": { + "enum": [ + "AMBER", + "AMBER STRICT", + "CLEAR", + "GREEN", + "RED", + "WHITE" + ], + "title": "Traffic Light Protocol", + "type": "string" + }, + "detection_pattern": { + "title": "Detection Pattern", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Related Subdomains", + "type": "array" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uploaded_time": { + "title": "Uploaded Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "comment": { + "title": "Analyst Comments", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "value": { + "title": "Indicator", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "intrusion_sets": { + "items": { + "type": "string" + }, + "title": "Intrusion Sets", + "type": "array" + }, + "script": { + "$ref": "#/$defs/script", + "title": "Related Script Data" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "Related File" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "email": { + "$ref": "#/$defs/email", + "title": "Related Email" + }, + "kill_chain": { + "items": { + "$ref": "#/$defs/kill_chain_phase" + }, + "title": "Kill Chain", + "type": "array" + }, + "detection_pattern_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Detection Pattern Type ID", + "type": "integer" + }, + "detection_pattern_type": { + "title": "Detection Pattern", + "type": "string" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "threat_actor": { + "$ref": "#/$defs/threat_actor", + "title": "Threat Actor" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 99 + ], + "title": "Indicator Type ID", + "type": "integer" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "answers": { + "items": { + "$ref": "#/$defs/dns_answer" + }, + "title": "Related DNS Answers", + "type": "array" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "campaign": { + "$ref": "#/$defs/campaign", + "title": "Campaign" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "subnet": { + "title": "Related Subnet", + "type": "string" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Related Digital Signatures", + "type": "array" + }, + "whois": { + "$ref": "#/$defs/whois", + "title": "WHOIS" + }, + "vulnerabilities": { + "items": { + "$ref": "#/$defs/vulnerability" + }, + "title": "Related Vulnerabilities", + "type": "array" + } + }, + "required": [ + "type_id", + "value" + ], + "title": "OSINT", + "type": "object" + }, + "san": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "title": "Subject Alternative Name", + "type": "object" + }, + "occurrence_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cell_name" + ] + }, + { + "required": [ + "column_name" + ] + }, + { + "required": [ + "column_number" + ] + }, + { + "required": [ + "end_line" + ] + }, + { + "required": [ + "json_path" + ] + }, + { + "required": [ + "page_number" + ] + }, + { + "required": [ + "record_index_in_array" + ] + }, + { + "required": [ + "row_number" + ] + }, + { + "required": [ + "start_line" + ] + } + ], + "properties": { + "cell_name": { + "title": "Cell Name", + "type": "string" + }, + "column_name": { + "title": "Column Name", + "type": "string" + }, + "column_number": { + "title": "Column Number", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "json_path": { + "title": "JSON Path", + "type": "string" + }, + "page_number": { + "title": "Page Number", + "type": "integer" + }, + "record_index_in_array": { + "title": "Record Index in Array", + "type": "integer" + }, + "row_number": { + "title": "Row Number", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "title": "Occurrence Details", + "type": "object" + }, + "firewall_rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "condition": { + "title": "Condition", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "match_details": { + "items": { + "type": "string" + }, + "title": "Match Details", + "type": "array" + }, + "match_location": { + "title": "Match Location", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "sensitivity": { + "title": "Sensitivity", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Firewall Rule", + "type": "object" + }, + "threat_actor": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Threat Actor Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Threat Actor Type ID", + "type": "integer" + } + }, + "required": [ + "name" + ], + "title": "Threat Actor", + "type": "object" + }, + "package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Package", + "type": "object" + }, + "transformation_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Transformation Info", + "type": "object" + }, + "response": { + "additionalProperties": false, + "properties": { + "code": { + "title": "Response Code", + "type": "integer" + }, + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "error": { + "title": "Error Code", + "type": "string" + }, + "error_message": { + "title": "Error Message", + "type": "string" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "message": { + "title": "Message", + "type": "string" + } + }, + "title": "Response Elements", + "type": "object" + }, + "reporter": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Reporter", + "type": "object" + }, + "url": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "path" + ] + }, + { + "required": [ + "url_string" + ] + } + ], + "properties": { + "categories": { + "items": { + "type": "string" + }, + "title": "Website Categorization", + "type": "array" + }, + "category_ids": { + "items": { + "enum": [ + 0, + 1, + 3, + 4, + 5, + 6, + 7, + 9, + 11, + 14, + 15, + 16, + 17, + 18, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 40, + 43, + 44, + 45, + 46, + 47, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 63, + 64, + 65, + 66, + 67, + 68, + 71, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 92, + 93, + 95, + 96, + 97, + 98, + 99, + 101, + 102, + 103, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 118, + 121 + ], + "type": "integer" + }, + "title": "Website Categorization IDs", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "query_string": { + "title": "HTTP Query String", + "type": "string" + }, + "resource_type": { + "title": "Resource Type", + "type": "string" + }, + "scheme": { + "title": "Scheme", + "type": "string" + }, + "subdomain": { + "title": "Subdomain", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Uniform Resource Locator", + "type": "object" + }, + "object": { + "additionalProperties": true, + "properties": {}, + "title": "Object", + "type": "object" + }, + "extension": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "version" + ], + "title": "Schema Extension", + "type": "object" + }, + "gpu_info": { + "additionalProperties": false, + "properties": { + "bus_type": { + "title": "Bus Type", + "type": "string" + }, + "bus_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Bus Type ID", + "type": "integer" + }, + "cores": { + "title": "Cores", + "type": "integer" + }, + "model": { + "title": "Model", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "vram_mode": { + "title": "VRAM Mode", + "type": "string" + }, + "vram_mode_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "VRAM Mode ID", + "type": "integer" + }, + "vram_size": { + "title": "VRAM Size", + "type": "integer" + } + }, + "title": "GPU Information", + "type": "object" + }, + "port_info": { + "additionalProperties": false, + "properties": { + "port": { + "title": "Port", + "type": "integer" + }, + "protocol_name": { + "title": "Protocol Name", + "type": "string" + }, + "protocol_num": { + "title": "Protocol Number", + "type": "integer" + } + }, + "required": [ + "port" + ], + "title": "Port Information", + "type": "object" + }, + "service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Service", + "type": "object" + }, + "metric": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Metric", + "type": "object" + }, + "request": { + "additionalProperties": false, + "properties": { + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Request Elements", + "type": "object" + }, + "sub_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Sub-technique", + "type": "object" + }, + "container": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_driver": { + "title": "Network Driver", + "type": "string" + }, + "orchestrator": { + "title": "Orchestrator", + "type": "string" + }, + "pod_uuid": { + "title": "Pod UUID", + "type": "string" + }, + "runtime": { + "title": "Runtime", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Container", + "type": "object" + }, + "malware": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "classification_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 99 + ], + "type": "integer" + }, + "title": "Classification IDs", + "type": "array" + }, + "classifications": { + "items": { + "type": "string" + }, + "title": "Classifications", + "type": "array" + }, + "cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "CVE List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "path": { + "title": "Path", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "classification_ids" + ], + "title": "Malware", + "type": "object" + }, + "win_win_service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "service_category_id" + ] + }, + { + "required": [ + "service_dependencies" + ] + }, + { + "required": [ + "service_error_control_id" + ] + }, + { + "required": [ + "service_start_name" + ] + }, + { + "required": [ + "service_start_type_id" + ] + }, + { + "required": [ + "service_type_id" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "hosting_process": { + "$ref": "#/$defs/process_entity", + "title": "Hosting Process" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "load_order_group": { + "title": "Load Order Group", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "service_category": { + "title": "Service Category", + "type": "string" + }, + "service_category_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Service Category ID", + "type": "integer" + }, + "service_dependencies": { + "items": { + "type": "string" + }, + "title": "Service Dependencies", + "type": "array" + }, + "service_dll_file": { + "$ref": "#/$defs/file", + "title": "Service DLL" + }, + "service_error_control": { + "title": "Service Error Control", + "type": "string" + }, + "service_error_control_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Error Control ID", + "type": "integer" + }, + "service_file": { + "$ref": "#/$defs/file", + "title": "Service File" + }, + "service_start_name": { + "title": "Service Start Name", + "type": "string" + }, + "service_start_type": { + "title": "Service Start Type", + "type": "string" + }, + "service_start_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Service Start Type ID", + "type": "integer" + }, + "service_type": { + "title": "Service Type", + "type": "string" + }, + "service_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Type ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Windows Service", + "type": "object" + }, + "group": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "privileges": { + "items": { + "type": "string" + }, + "title": "Privileges", + "type": "array" + }, + "type": { + "title": "Group Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "Group", + "type": "object" + }, + "device": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "region": { + "title": "Region", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "os_machine_uuid": { + "title": "OS Machine UUID", + "type": "string" + }, + "hypervisor": { + "title": "Hypervisor", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "imei_list": { + "items": { + "type": "string" + }, + "title": "IMEI List", + "type": "array" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "is_shared": { + "title": "Shared Device", + "type": "boolean" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "meid": { + "title": "MEID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "is_personal": { + "title": "Personal Device", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "iccid": { + "title": "ICCID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "udid": { + "title": "Unique Device Identifier", + "type": "string" + }, + "model": { + "title": "Model", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "boot_time": { + "title": "Boot Time", + "type": "integer" + }, + "is_supervised": { + "title": "Supervised Device", + "type": "boolean" + }, + "is_managed": { + "title": "Managed Device", + "type": "boolean" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "is_trusted": { + "title": "Trusted Device", + "type": "boolean" + }, + "boot_uid": { + "title": "Boot UID", + "type": "string" + }, + "network_interfaces": { + "items": { + "$ref": "#/$defs/network_interface" + }, + "title": "Network Interfaces", + "type": "array" + }, + "autoscale_uid": { + "title": "Autoscale UID", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "subnet": { + "title": "Subnet", + "type": "string" + }, + "eid": { + "title": "EID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "imei": { + "title": "IMEI", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "is_mobile_account_active": { + "title": "Mobile Account Active", + "type": "boolean" + }, + "is_compliant": { + "title": "Compliant Device", + "type": "boolean" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Device", + "type": "object" + }, + "campaign": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Campaign", + "type": "object" + }, + "cwe": { + "additionalProperties": false, + "properties": { + "caption": { + "title": "Caption", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "CWE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CWE", + "type": "object" + }, + "digital_signature": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "developer_uid": { + "title": "Developer UID", + "type": "string" + }, + "digest": { + "$ref": "#/$defs/fingerprint", + "title": "Message Digest" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "State ID", + "type": "integer" + } + }, + "required": [ + "algorithm_id" + ], + "title": "Digital Signature", + "type": "object" + }, + "sbom": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "package": { + "$ref": "#/$defs/package", + "title": "Software Package" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "software_components": { + "items": { + "$ref": "#/$defs/software_component" + }, + "title": "Software Components", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "SBOM ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "package", + "software_components" + ], + "title": "Software Bill of Materials", + "type": "object" + }, + "malware_scan_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "end_time": { + "title": "End Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_files": { + "title": "Scanned Files", + "type": "integer" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "num_volumes": { + "title": "Number of Volumes", + "type": "integer" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Scan UID", + "type": "string" + }, + "unique_malware_count": { + "title": "Unique Malware Count", + "type": "integer" + } + }, + "required": [ + "type_id" + ], + "title": "Malware Scan Info", + "type": "object" + }, + "kill_chain_phase": { + "additionalProperties": false, + "properties": { + "phase": { + "title": "Kill Chain Phase", + "type": "string" + }, + "phase_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Kill Chain Phase ID", + "type": "integer" + } + }, + "required": [ + "phase_id" + ], + "title": "Kill Chain Phase", + "type": "object" + }, + "organization": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "ou_name": { + "title": "Org Unit Name", + "type": "string" + }, + "ou_uid": { + "title": "Org Unit ID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Organization", + "type": "object" + }, + "encryption_details": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Encryption Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Encryption Algorithm ID", + "type": "integer" + }, + "key_length": { + "title": "Encryption Key Length", + "type": "integer" + }, + "key_uid": { + "title": "Key UID", + "type": "string" + }, + "type": { + "title": "Encryption Type", + "type": "string" + } + }, + "title": "Encryption Details", + "type": "object" + }, + "logger": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "log_format": { + "title": "Log Format", + "type": "string" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Logger", + "type": "object" + }, + "feature": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Feature", + "type": "object" + }, + "timespan": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "duration" + ] + }, + { + "required": [ + "duration_days" + ] + }, + { + "required": [ + "duration_hours" + ] + }, + { + "required": [ + "duration_mins" + ] + }, + { + "required": [ + "duration_months" + ] + }, + { + "required": [ + "duration_secs" + ] + }, + { + "required": [ + "duration_weeks" + ] + }, + { + "required": [ + "duration_years" + ] + }, + { + "required": [ + "end_time" + ] + }, + { + "required": [ + "start_time" + ] + } + ], + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "duration_days": { + "title": "Duration Days", + "type": "integer" + }, + "duration_hours": { + "title": "Duration Hours", + "type": "integer" + }, + "duration_mins": { + "title": "Duration Minutes", + "type": "integer" + }, + "duration_months": { + "title": "Duration Months", + "type": "integer" + }, + "duration_secs": { + "title": "Duration Seconds", + "type": "integer" + }, + "duration_weeks": { + "title": "Duration Weeks", + "type": "integer" + }, + "duration_years": { + "title": "Duration Years", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Time Span Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Time Span Type ID", + "type": "integer" + } + }, + "title": "Time Span", + "type": "object" + }, + "rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Rule", + "type": "object" + }, + "software_component": { + "additionalProperties": false, + "properties": { + "author": { + "title": "Author", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "related_component": { + "title": "Related Component", + "type": "string" + }, + "relationship": { + "title": "Relationship", + "type": "string" + }, + "relationship_id": { + "enum": [ + 0, + 1, + 99 + ], + "title": "Relationship ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Component", + "type": "object" + }, + "agent": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Agent Name", + "type": "string" + }, + "policies": { + "items": { + "$ref": "#/$defs/policy" + }, + "title": "Agent Policies", + "type": "array" + }, + "type": { + "title": "Agent Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Agent ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate Agent ID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Agent Version", + "type": "string" + } + }, + "title": "Agent", + "type": "object" + }, + "whois": { + "additionalProperties": false, + "properties": { + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "created_time": { + "title": "Registered At", + "type": "integer" + }, + "dnssec_status": { + "title": "DNSSEC Status", + "type": "string" + }, + "dnssec_status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "DNSSEC Status ID", + "type": "integer" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "domain_contacts": { + "items": { + "$ref": "#/$defs/domain_contact" + }, + "title": "Domain Contacts", + "type": "array" + }, + "email_addr": { + "title": "Registrar Abuse Email Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "last_seen_time": { + "title": "Last Updated At", + "type": "integer" + }, + "name_servers": { + "items": { + "type": "string" + }, + "title": "Name Servers", + "type": "array" + }, + "phone_number": { + "title": "Registrar Abuse Phone Number", + "type": "string" + }, + "registrar": { + "title": "Domain Registrar", + "type": "string" + }, + "status": { + "title": "Domain Status", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Subdomains", + "type": "array" + }, + "subnet": { + "title": "Subnet Block", + "type": "string" + } + }, + "title": "WHOIS", + "type": "object" + }, + "key_value_object": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "value" + ] + }, + { + "required": [ + "values" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + }, + "values": { + "items": { + "type": "string" + }, + "title": "Values", + "type": "array" + } + }, + "required": [ + "name" + ], + "title": "Key:Value object", + "type": "object" + }, + "metadata": { + "additionalProperties": false, + "properties": { + "log_format": { + "title": "Log Source Format", + "type": "string" + }, + "transformation_info_list": { + "items": { + "$ref": "#/$defs/transformation_info" + }, + "title": "Transformation Info", + "type": "array" + }, + "uid": { + "title": "Event UID", + "type": "string" + }, + "original_event_uid": { + "title": "Original Event ID", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "extensions": { + "items": { + "$ref": "#/$defs/extension" + }, + "title": "Schema Extensions", + "type": "array" + }, + "source": { + "title": "Source", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "loggers": { + "items": { + "$ref": "#/$defs/logger" + }, + "title": "Loggers", + "type": "array" + }, + "processed_time": { + "title": "Processed Time", + "type": "integer" + }, + "log_source": { + "title": "Log Source", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "debug": { + "items": { + "type": "string" + }, + "title": "Debug Information", + "type": "array" + }, + "profiles": { + "items": { + "type": "string" + }, + "title": "Profiles", + "type": "array" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "event_code": { + "title": "Event Code", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "reporter": { + "$ref": "#/$defs/reporter", + "title": "Reporter" + }, + "correlation_uid": { + "title": "Correlation UID", + "type": "string" + }, + "extension": { + "$ref": "#/$defs/extension", + "title": "Schema Extension" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "sequence": { + "title": "Sequence Number", + "type": "integer" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "original_time": { + "title": "Original Time", + "type": "string" + }, + "total_queued_duration": { + "$ref": "#/$defs/timespan", + "title": "Total Queued Duration" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "product", + "version" + ], + "title": "Metadata", + "type": "object" + }, + "node": { + "additionalProperties": false, + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Node", + "type": "object" + }, + "product": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "feature": { + "$ref": "#/$defs/feature", + "title": "Feature" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Product", + "type": "object" + }, + "advisory": { + "additionalProperties": false, + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "Related CVEs", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Advisory ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Advisory", + "type": "object" + }, + "network_endpoint": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Network Endpoint", + "type": "object" + }, + "d3f_tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Tactic", + "type": "object" + }, + "cve": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "cvss": { + "items": { + "$ref": "#/$defs/cvss" + }, + "title": "CVSS Score", + "type": "array" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "cwe_uid": { + "title": "CWE UID", + "type": "string" + }, + "cwe_url": { + "title": "CWE URL", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "epss": { + "$ref": "#/$defs/epss", + "title": "EPSS" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "title": { + "title": "Title", + "type": "string" + }, + "type": { + "title": "Vulnerability Type", + "type": "string" + }, + "uid": { + "title": "CVE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CVE", + "type": "object" + }, + "domain_contact": { + "additionalProperties": false, + "properties": { + "email_addr": { + "title": "Contact Email", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Contact Location Information" + }, + "name": { + "title": "Name", + "type": "string" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "type": { + "title": "Domain Contact Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Domain Contact Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Domain Contact", + "type": "object" + }, + "policy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "type" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "is_applied": { + "title": "Applied", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Policy", + "type": "object" + }, + "span": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "end_time", + "start_time", + "uid" + ], + "title": "Span", + "type": "object" + } + }, + "$id": "https://schema.ocsf.io/schema/classes/api_activity", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "unmapped": { + "$ref": "#/$defs/object", + "title": "Unmapped Data" + }, + "message": { + "title": "Message", + "type": "string" + }, + "http_response": { + "$ref": "#/$defs/http_response", + "title": "HTTP Response" + }, + "category_uid": { + "const": 6, + "title": "Category ID", + "type": "integer" + }, + "api": { + "$ref": "#/$defs/api", + "title": "API Details" + }, + "message_context": { + "$ref": "#/$defs/message_context", + "title": "Message Context" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "status_detail": { + "title": "Status Detail", + "type": "string" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "type_name": { + "title": "Type Name", + "type": "string" + }, + "raw_data": { + "title": "Raw Data", + "type": "string" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "actor": { + "$ref": "#/$defs/actor", + "title": "Actor" + }, + "trace": { + "$ref": "#/$defs/trace", + "title": "Trace" + }, + "status": { + "title": "Status", + "type": "string" + }, + "ai_model": { + "$ref": "#/$defs/ai_model", + "title": "AI Model" + }, + "activity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Activity ID", + "type": "integer" + }, + "resources": { + "items": { + "$ref": "#/$defs/resource_details" + }, + "title": "Resources Array", + "type": "array" + }, + "observables": { + "items": { + "$ref": "#/$defs/observable" + }, + "title": "Observables", + "type": "array" + }, + "enrichments": { + "items": { + "$ref": "#/$defs/enrichment" + }, + "title": "Enrichments", + "type": "array" + }, + "http_request": { + "$ref": "#/$defs/http_request", + "title": "HTTP Request" + }, + "raw_data_hash": { + "$ref": "#/$defs/fingerprint", + "title": "Raw Data Hash" + }, + "class_uid": { + "const": 6003, + "title": "Class ID", + "type": "integer" + }, + "activity_name": { + "title": "Activity", + "type": "string" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "class_name": { + "title": "Class", + "type": "string" + }, + "timezone_offset": { + "title": "Timezone Offset", + "type": "integer" + }, + "raw_data_size": { + "title": "Raw Data Size", + "type": "integer" + }, + "dst_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Destination Endpoint" + }, + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "category_name": { + "title": "Category", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "count": { + "title": "Count", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "src_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Source Endpoint" + }, + "metadata": { + "$ref": "#/$defs/metadata", + "title": "Metadata" + } + }, + "required": [ + "activity_id", + "actor", + "api", + "category_uid", + "class_uid", + "metadata", + "severity_id", + "src_endpoint", + "time", + "type_uid" + ], + "title": "API Activity", + "type": "object" +} diff --git a/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@container+host+security_control+trace.json b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@container+host+security_control+trace.json new file mode 100644 index 0000000000..f982941183 --- /dev/null +++ b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@container+host+security_control+trace.json @@ -0,0 +1,8265 @@ +{ + "$defs": { + "kb_article": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "src_url" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "KB Article", + "type": "object" + }, + "long_string": { + "additionalProperties": false, + "properties": { + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "value" + ], + "title": "Long String", + "type": "object" + }, + "enrichment": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "short_desc": { + "title": "Short Description", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "data", + "name", + "value" + ], + "title": "Enrichment", + "type": "object" + }, + "ldap_person": { + "additionalProperties": false, + "properties": { + "cost_center": { + "title": "Cost Center", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "deleted_time": { + "title": "Deleted Time", + "type": "integer" + }, + "department": { + "title": "Department", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "email_addrs": { + "items": { + "type": "string" + }, + "title": "Email Addresses", + "type": "array" + }, + "employee_uid": { + "title": "Employee ID", + "type": "string" + }, + "given_name": { + "title": "Given Name", + "type": "string" + }, + "hire_time": { + "title": "Hire Time", + "type": "integer" + }, + "job_title": { + "title": "Job Title", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "last_login_time": { + "title": "Last Login", + "type": "integer" + }, + "ldap_cn": { + "title": "LDAP Common Name", + "type": "string" + }, + "ldap_dn": { + "title": "LDAP Distinguished Name", + "type": "string" + }, + "leave_time": { + "title": "Leave Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "manager": { + "$ref": "#/$defs/user", + "title": "Manager" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "office_location": { + "title": "Office Location", + "type": "string" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "surname": { + "title": "Surname", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + } + }, + "title": "LDAP Person", + "type": "object" + }, + "email_auth": { + "additionalProperties": false, + "properties": { + "dkim": { + "title": "DKIM Status", + "type": "string" + }, + "dkim_domain": { + "title": "DKIM Domain", + "type": "string" + }, + "dkim_signature": { + "title": "DKIM Signature", + "type": "string" + }, + "dmarc": { + "title": "DMARC Status", + "type": "string" + }, + "dmarc_override": { + "title": "DMARC Override", + "type": "string" + }, + "dmarc_policy": { + "title": "DMARC Policy", + "type": "string" + }, + "spf": { + "title": "SPF Status", + "type": "string" + } + }, + "title": "Email Authentication", + "type": "object" + }, + "epss": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "percentile": { + "title": "EPSS Percentile", + "type": "number" + }, + "score": { + "title": "EPPS Score", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "score" + ], + "title": "EPSS", + "type": "object" + }, + "device_hw_info": { + "additionalProperties": false, + "properties": { + "bios_date": { + "title": "BIOS Date", + "type": "string" + }, + "bios_manufacturer": { + "title": "BIOS Manufacturer", + "type": "string" + }, + "bios_ver": { + "title": "BIOS Version", + "type": "string" + }, + "chassis": { + "title": "Chassis", + "type": "string" + }, + "cpu_architecture": { + "title": "CPU Architecture", + "type": "string" + }, + "cpu_architecture_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "CPU Architecture ID", + "type": "integer" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "cpu_cores": { + "title": "CPU Cores", + "type": "integer" + }, + "cpu_count": { + "title": "CPU Count", + "type": "integer" + }, + "cpu_speed": { + "title": "Processor Speed", + "type": "integer" + }, + "cpu_type": { + "title": "Processor Type", + "type": "string" + }, + "desktop_display": { + "$ref": "#/$defs/display", + "title": "Desktop Display" + }, + "gpu_count": { + "title": "GPU Count", + "type": "integer" + }, + "gpu_info_list": { + "items": { + "$ref": "#/$defs/gpu_info" + }, + "title": "GPU information", + "type": "array" + }, + "keyboard_info": { + "$ref": "#/$defs/keyboard_info", + "title": "Keyboard Information" + }, + "ram_size": { + "title": "RAM Size", + "type": "integer" + }, + "serial_number": { + "title": "Serial Number", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Device Hardware Info", + "type": "object" + }, + "network_proxy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "title": "Network Proxy Endpoint", + "type": "object" + }, + "data_classification": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "category_id" + ] + }, + { + "required": [ + "confidentiality_id" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "category_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Category ID", + "type": "integer" + }, + "classifier_details": { + "$ref": "#/$defs/classifier_details", + "title": "Classifier Details" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "discovery_details": { + "items": { + "$ref": "#/$defs/discovery_details" + }, + "title": "Discovery Details", + "type": "array" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "status_details": { + "items": { + "type": "string" + }, + "title": "Status Details", + "type": "array" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Data Classification", + "type": "object" + }, + "http_response": { + "additionalProperties": false, + "properties": { + "body_length": { + "title": "Response Body Length", + "type": "integer" + }, + "code": { + "title": "Response Code", + "type": "integer" + }, + "content_type": { + "title": "HTTP Content Type", + "type": "string" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "latency": { + "title": "Latency", + "type": "integer" + }, + "length": { + "title": "Response Length", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "code" + ], + "title": "HTTP Response", + "type": "object" + }, + "resource_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "criticality": { + "title": "Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Resource Relationship" + }, + "role": { + "title": "Role", + "type": "string" + }, + "role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Role ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Resource Details", + "type": "object" + }, + "display": { + "additionalProperties": false, + "properties": { + "color_depth": { + "title": "Color Depth", + "type": "integer" + }, + "physical_height": { + "title": "Physical Height", + "type": "integer" + }, + "physical_orientation": { + "title": "Physical Orientation", + "type": "integer" + }, + "physical_width": { + "title": "Physical Width", + "type": "integer" + }, + "scale_factor": { + "title": "Scale Factor", + "type": "integer" + } + }, + "title": "Display", + "type": "object" + }, + "scim": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "error_message": { + "title": "Last Error Message", + "type": "string" + }, + "is_group_provisioning_enabled": { + "title": "SCIM Group Provisioning Enabled", + "type": "boolean" + }, + "is_user_provisioning_enabled": { + "title": "SCIM User Provisioning Enabled", + "type": "boolean" + }, + "last_run_time": { + "title": "Last Sync Time", + "type": "integer" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "scim_group_schema": { + "title": "SCIM Group Schema" + }, + "scim_user_schema": { + "title": "SCIM User Schema" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "External ID", + "type": "string" + }, + "url_string": { + "title": "SCIM Endpoint URL", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + }, + "version": { + "title": "SCIM Version", + "type": "string" + } + }, + "title": "SCIM", + "type": "object" + }, + "programmatic_credential": { + "additionalProperties": false, + "properties": { + "last_used_time": { + "title": "Last Used Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Programmatic Credential", + "type": "object" + }, + "os": { + "additionalProperties": false, + "properties": { + "build": { + "title": "OS Build", + "type": "string" + }, + "country": { + "title": "Country", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "edition": { + "title": "OS Edition", + "type": "string" + }, + "kernel_release": { + "title": "Kernel Release", + "type": "string" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "sp_name": { + "title": "OS Service Pack", + "type": "string" + }, + "sp_ver": { + "title": "OS Service Pack Version", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 99, + 100, + 101, + 200, + 201, + 300, + 301, + 302, + 400, + 401, + 402 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "Operating System (OS)", + "type": "object" + }, + "tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Tactic", + "type": "object" + }, + "dns_answer": { + "additionalProperties": false, + "properties": { + "class": { + "title": "Resource Record Class", + "type": "string" + }, + "flag_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "type": "integer" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "packet_uid": { + "title": "Packet UID", + "type": "integer" + }, + "rdata": { + "title": "DNS RData", + "type": "string" + }, + "ttl": { + "title": "TTL", + "type": "integer" + }, + "type": { + "title": "Resource Record Type", + "type": "string" + } + }, + "required": [ + "rdata" + ], + "title": "DNS Answer", + "type": "object" + }, + "discovery_details": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "occurrence_details": { + "$ref": "#/$defs/occurrence_details", + "title": "Occurrence Details" + }, + "occurrences": { + "items": { + "$ref": "#/$defs/occurrence_details" + }, + "title": "Occurrences", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "title": "Discovery Details", + "type": "object" + }, + "sso": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "SAML Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "duration_mins": { + "title": "SSO Session Duration", + "type": "integer" + }, + "idle_timeout": { + "title": "SSO Idle Timeout", + "type": "integer" + }, + "login_endpoint": { + "title": "SSO Login Endpoint", + "type": "string" + }, + "logout_endpoint": { + "title": "SSO Logout Endpoint", + "type": "string" + }, + "metadata_endpoint": { + "title": "SSO Metadata Endpoint", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "title": "Scopes", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + } + }, + "title": "SSO", + "type": "object" + }, + "token": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_renewable": { + "title": "Renewable", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Token", + "type": "object" + }, + "api": { + "additionalProperties": false, + "properties": { + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "request": { + "$ref": "#/$defs/request", + "title": "API Request Details" + }, + "response": { + "$ref": "#/$defs/response", + "title": "API Response Details" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "token": { + "$ref": "#/$defs/token", + "title": "Token" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "operation" + ], + "title": "API", + "type": "object" + }, + "d3fend": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "d3f_tactic" + ] + }, + { + "required": [ + "d3f_technique" + ] + } + ], + "properties": { + "d3f_tactic": { + "$ref": "#/$defs/d3f_tactic", + "title": "MITRE D3FEND\u2122 Tactic" + }, + "d3f_technique": { + "$ref": "#/$defs/d3f_technique", + "title": "MITRE D3FEND\u2122 Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122", + "type": "object" + }, + "reputation": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Reputation Score", + "type": "number" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "score": { + "title": "Reputation Score", + "type": "string" + }, + "score_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "Reputation Score ID", + "type": "integer" + } + }, + "required": [ + "base_score", + "score_id" + ], + "title": "Reputation", + "type": "object" + }, + "mitigation": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "countermeasures": { + "items": { + "$ref": "#/$defs/d3fend" + }, + "title": "Countermeasures", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Mitigation", + "type": "object" + }, + "location": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "city" + ] + }, + { + "required": [ + "country" + ] + }, + { + "required": [ + "postal_code" + ] + }, + { + "required": [ + "region" + ] + } + ], + "properties": { + "aerial_height": { + "title": "Aerial Height", + "type": "string" + }, + "city": { + "title": "City", + "type": "string" + }, + "continent": { + "title": "Continent", + "type": "string" + }, + "coordinates": { + "items": { + "type": "number" + }, + "title": "Coordinates", + "type": "array" + }, + "country": { + "title": "Country", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "geodetic_altitude": { + "title": "Geodetic Altitude", + "type": "string" + }, + "geodetic_vertical_accuracy": { + "title": "Geodetic Vertical Accuracy", + "type": "string" + }, + "geohash": { + "title": "Geohash", + "type": "string" + }, + "horizontal_accuracy": { + "title": "Horizontal Accuracy", + "type": "string" + }, + "is_on_premises": { + "title": "On-Premises", + "type": "boolean" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "lat": { + "title": "Latitude", + "type": "number" + }, + "long": { + "title": "Longitude", + "type": "number" + }, + "postal_code": { + "title": "Postal Code", + "type": "string" + }, + "pressure_altitude": { + "title": "Pressure Altitude", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + } + }, + "title": "Geo Location", + "type": "object" + }, + "idp": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "auth_factors": { + "items": { + "$ref": "#/$defs/auth_factor" + }, + "title": "Authentication Factors", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprint": { + "$ref": "#/$defs/fingerprint", + "title": "Certificate Fingerprint" + }, + "has_mfa": { + "title": "MFA Enforced", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scim": { + "$ref": "#/$defs/scim", + "title": "SCIM" + }, + "sso": { + "$ref": "#/$defs/sso", + "title": "SSO" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "Configuration URL", + "type": "string" + } + }, + "title": "Identity Provider", + "type": "object" + }, + "message_context": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "application" + ] + }, + { + "required": [ + "service" + ] + } + ], + "properties": { + "ai_role": { + "title": "AI Role", + "type": "string" + }, + "ai_role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "AI Role ID", + "type": "integer" + }, + "application": { + "$ref": "#/$defs/application", + "title": "Application" + }, + "completion_tokens": { + "title": "Completion Tokens", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "prompt_tokens": { + "title": "Prompt Tokens", + "type": "integer" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "total_tokens": { + "title": "Total Tokens", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Message Context", + "type": "object" + }, + "process": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ancestry": { + "items": { + "$ref": "#/$defs/process_entity" + }, + "title": "Ancestry", + "type": "array" + }, + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "environment_variables": { + "items": { + "$ref": "#/$defs/environment_variable" + }, + "title": "Environment Variables", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hosted_services": { + "items": { + "$ref": "#/$defs/win_win_service" + }, + "title": "Hosted Services", + "type": "array" + }, + "integrity": { + "title": "Integrity", + "type": "string" + }, + "integrity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Integrity Level", + "type": "integer" + }, + "lineage": { + "items": { + "type": "string" + }, + "title": "Lineage", + "type": "array" + }, + "loaded_modules": { + "items": { + "type": "string" + }, + "title": "Loaded Modules", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "parent_process": { + "$ref": "#/$defs/process", + "title": "Parent Process" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "ptid": { + "title": "Process Thread ID", + "type": "integer" + }, + "sandbox": { + "title": "Sandbox", + "type": "string" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "terminated_time": { + "title": "Terminated Time", + "type": "integer" + }, + "tid": { + "title": "Thread ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + }, + "working_directory": { + "title": "Working Directory", + "type": "string" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + } + }, + "title": "Process", + "type": "object" + }, + "vulnerability": { + "additionalProperties": false, + "oneOf": [ + { + "not": { + "required": [ + "cve", + "cwe" + ] + }, + "required": [ + "advisory" + ] + }, + { + "not": { + "required": [ + "advisory", + "cwe" + ] + }, + "required": [ + "cve" + ] + }, + { + "not": { + "required": [ + "advisory", + "cve" + ] + }, + "required": [ + "cwe" + ] + } + ], + "properties": { + "advisory": { + "$ref": "#/$defs/advisory", + "title": "Security Advisory" + }, + "affected_code": { + "items": { + "$ref": "#/$defs/affected_code" + }, + "title": "Affected Code", + "type": "array" + }, + "affected_packages": { + "items": { + "$ref": "#/$defs/affected_package" + }, + "title": "Affected Software Packages", + "type": "array" + }, + "category": { + "title": "Category", + "type": "string" + }, + "cve": { + "$ref": "#/$defs/cve", + "title": "CVE" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "dependency_chain": { + "title": "Dependency Chain", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "exploit_last_seen_time": { + "title": "Exploit Last Seen Time", + "type": "integer" + }, + "exploit_ref_url": { + "title": "Exploit URL", + "type": "string" + }, + "exploit_requirement": { + "title": "Exploit Requirement", + "type": "string" + }, + "exploit_type": { + "title": "Exploit Type", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "fix_coverage": { + "title": "Fix Coverage", + "type": "string" + }, + "fix_coverage_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Fix Coverage ID", + "type": "integer" + }, + "is_exploit_available": { + "title": "Exploit Availability", + "type": "boolean" + }, + "is_fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "packages": { + "items": { + "$ref": "#/$defs/package" + }, + "title": "Software Packages", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_vulnerabilities": { + "items": { + "type": "string" + }, + "title": "Related Vulnerability IDs", + "type": "array" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Vulnerability Details", + "type": "object" + }, + "affected_code": { + "additionalProperties": false, + "properties": { + "end_column": { + "title": "End Column", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "rule": { + "$ref": "#/$defs/rule", + "title": "Related Rule" + }, + "start_column": { + "title": "Start Column", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "required": [ + "file" + ], + "title": "Affected Code", + "type": "object" + }, + "keyboard_info": { + "additionalProperties": false, + "properties": { + "function_keys": { + "title": "Function Keys", + "type": "integer" + }, + "ime": { + "title": "IME", + "type": "string" + }, + "keyboard_layout": { + "title": "Keyboard Layout", + "type": "string" + }, + "keyboard_subtype": { + "title": "Keyboard Subtype", + "type": "integer" + }, + "keyboard_type": { + "title": "Keyboard Type", + "type": "string" + } + }, + "title": "Keyboard Information", + "type": "object" + }, + "certificate": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "is_self_signed": { + "title": "Certificate Self-Signed", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Distinguished Name", + "type": "string" + }, + "sans": { + "items": { + "$ref": "#/$defs/san" + }, + "title": "Subject Alternative Names", + "type": "array" + }, + "serial_number": { + "title": "Certificate Serial Number", + "type": "string" + }, + "subject": { + "title": "Subject Distinguished Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "issuer", + "serial_number" + ], + "title": "Digital Certificate", + "type": "object" + }, + "graph": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "edges": { + "items": { + "$ref": "#/$defs/edge" + }, + "title": "Edges", + "type": "array" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "nodes": { + "items": { + "$ref": "#/$defs/node" + }, + "title": "Nodes", + "type": "array" + }, + "query_language": { + "title": "Query Language", + "type": "string" + }, + "query_language_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Query Language ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "nodes" + ], + "title": "Graph", + "type": "object" + }, + "account": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "is_disabled": { + "title": "Disabled", + "type": "boolean" + }, + "is_locked": { + "title": "Locked", + "type": "boolean" + }, + "is_on_premises_sync_enabled": { + "title": "On-Premises Sync Enabled", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Account", + "type": "object" + }, + "authorization": { + "additionalProperties": false, + "properties": { + "decision": { + "title": "Authorization Decision/Outcome", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "title": "Authorization Result", + "type": "object" + }, + "autonomous_system": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "number" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "number": { + "title": "Number", + "type": "integer" + } + }, + "title": "Autonomous System", + "type": "object" + }, + "classifier_details": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "Classifier Details", + "type": "object" + }, + "session": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "expiration_reason": { + "title": "Expiration Reason", + "type": "string" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_mfa": { + "title": "Multi Factor Authentication", + "type": "boolean" + }, + "is_remote": { + "title": "Remote", + "type": "boolean" + }, + "is_vpn": { + "title": "VPN Session", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "terminal": { + "title": "Terminal", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + } + }, + "title": "Session", + "type": "object" + }, + "d3f_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Technique", + "type": "object" + }, + "image": { + "additionalProperties": false, + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Image", + "type": "object" + }, + "application": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "criticality": { + "title": "Business Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Application Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Application Name", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Application Relationship" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "sbom": { + "$ref": "#/$defs/sbom", + "title": "Related SBOM" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Application Type", + "type": "string" + }, + "uid": { + "title": "Application ID", + "type": "string" + }, + "uid_alt": { + "title": "Application Alternative ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "version": { + "title": "Application Version", + "type": "string" + } + }, + "title": "Application", + "type": "object" + }, + "script": { + "additionalProperties": false, + "properties": { + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "script_content": { + "$ref": "#/$defs/long_string", + "title": "Script Content" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "script_content", + "type_id" + ], + "title": "Script", + "type": "object" + }, + "affected_package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "fixed_in_version": { + "title": "Fixed In Version", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Affected Software Package", + "type": "object" + }, + "cis_control": { + "additionalProperties": false, + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "CIS Control", + "type": "object" + }, + "analytic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Analytic", + "type": "object" + }, + "edge": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "relation": { + "title": "Relation", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "target": { + "title": "Target", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "source", + "target" + ], + "title": "Edge", + "type": "object" + }, + "actor": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "app_name" + ] + }, + { + "required": [ + "app_uid" + ] + }, + { + "required": [ + "invoked_by" + ] + }, + { + "required": [ + "process" + ] + }, + { + "required": [ + "session" + ] + }, + { + "required": [ + "user" + ] + } + ], + "properties": { + "app_name": { + "title": "Application Name", + "type": "string" + }, + "app_uid": { + "title": "Application ID", + "type": "string" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "idp": { + "$ref": "#/$defs/idp", + "title": "Identity Provider" + }, + "invoked_by": { + "title": "Invoked by", + "type": "string" + }, + "process": { + "$ref": "#/$defs/process", + "title": "Process" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + } + }, + "title": "Actor", + "type": "object" + }, + "cloud": { + "additionalProperties": false, + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "cloud_partition": { + "title": "Cloud Partition", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "project_uid": { + "title": "Project ID", + "type": "string" + }, + "provider": { + "title": "Cloud Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + }, + "zone": { + "title": "Cloud Availability Zone", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "Cloud", + "type": "object" + }, + "trace": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "span": { + "$ref": "#/$defs/span", + "title": "Span" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Trace", + "type": "object" + }, + "ai_model": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ai_provider": { + "title": "AI Provider", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "ai_provider" + ], + "title": "AI Model", + "type": "object" + }, + "file": { + "additionalProperties": false, + "properties": { + "parent_folder": { + "title": "Parent Folder", + "type": "string" + }, + "is_system": { + "title": "System", + "type": "boolean" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "accessor": { + "$ref": "#/$defs/user", + "title": "Accessor" + }, + "is_readonly": { + "title": "Read-Only", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "ext": { + "title": "File Extension", + "type": "string" + }, + "company_name": { + "title": "Company Name", + "type": "string" + }, + "attributes": { + "title": "Attributes", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uri": { + "title": "File URI", + "type": "string" + }, + "is_deleted": { + "title": "Deleted", + "type": "boolean" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "drive_type": { + "title": "Drive Type", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "imported_symbols": { + "items": { + "type": "string" + }, + "title": "Imported Symbols", + "type": "array" + }, + "signature": { + "$ref": "#/$defs/digital_signature", + "title": "Digital Signature" + }, + "is_encrypted": { + "title": "Encrypted", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "accessed_time": { + "title": "Accessed Time", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "internal_name": { + "title": "Internal Name", + "type": "string" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "drive_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Drive Type ID", + "type": "integer" + }, + "security_descriptor": { + "title": "Security Descriptor", + "type": "string" + }, + "volume": { + "title": "Volume", + "type": "string" + }, + "modifier": { + "$ref": "#/$defs/user", + "title": "Modifier" + }, + "mime_type": { + "title": "MIME type", + "type": "string" + }, + "encryption_details": { + "$ref": "#/$defs/encryption_details", + "title": "Encryption Details" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Digital Signatures", + "type": "array" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "File", + "type": "object" + }, + "remediation": { + "additionalProperties": false, + "properties": { + "cis_controls": { + "items": { + "$ref": "#/$defs/cis_control" + }, + "title": "CIS Controls", + "type": "array" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + } + }, + "required": [ + "desc" + ], + "title": "Remediation", + "type": "object" + }, + "email": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "from" + ] + }, + { + "required": [ + "to" + ] + } + ], + "properties": { + "cc": { + "items": { + "type": "string" + }, + "title": "Cc", + "type": "array" + }, + "cc_mailboxes": { + "items": { + "type": "string" + }, + "title": "Cc Mailboxes", + "type": "array" + }, + "delivered_to": { + "title": "Delivered To", + "type": "string" + }, + "delivered_to_list": { + "items": { + "type": "string" + }, + "title": "Delivered To List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "from": { + "title": "From", + "type": "string" + }, + "from_list": { + "items": { + "type": "string" + }, + "title": "From List", + "type": "array" + }, + "from_mailbox": { + "title": "From Mailbox", + "type": "string" + }, + "from_mailboxes": { + "items": { + "type": "string" + }, + "title": "From Mailboxes", + "type": "array" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "is_read": { + "title": "Read", + "type": "boolean" + }, + "message_uid": { + "title": "Message UID", + "type": "string" + }, + "raw_header": { + "title": "Raw Header", + "type": "string" + }, + "reply_to": { + "title": "Reply To", + "type": "string" + }, + "reply_to_list": { + "items": { + "type": "string" + }, + "title": "Reply To List", + "type": "array" + }, + "reply_to_mailboxes": { + "items": { + "type": "string" + }, + "title": "Reply To Mailboxes", + "type": "array" + }, + "return_path": { + "title": "Return Path", + "type": "string" + }, + "sender": { + "title": "Sender", + "type": "string" + }, + "sender_mailbox": { + "title": "Sender Mailbox", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "smtp_from": { + "title": "SMTP From", + "type": "string" + }, + "smtp_to": { + "items": { + "type": "string" + }, + "title": "SMTP To", + "type": "array" + }, + "subject": { + "title": "Subject", + "type": "string" + }, + "to": { + "items": { + "type": "string" + }, + "title": "To", + "type": "array" + }, + "to_mailboxes": { + "items": { + "type": "string" + }, + "title": "To Mailboxes", + "type": "array" + }, + "uid": { + "title": "Email Thread UID", + "type": "string" + }, + "urls": { + "items": { + "$ref": "#/$defs/url" + }, + "title": "URLs", + "type": "array" + }, + "x_originating_ip": { + "items": { + "type": "string" + }, + "title": "X-Originating-IP", + "type": "array" + } + }, + "title": "Email", + "type": "object" + }, + "environment_variable": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Environment Variable", + "type": "object" + }, + "technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Technique", + "type": "object" + }, + "http_request": { + "additionalProperties": false, + "properties": { + "args": { + "title": "HTTP Arguments", + "type": "string" + }, + "body_length": { + "title": "Request Body Length", + "type": "integer" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "http_method": { + "enum": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "title": "HTTP Method", + "type": "string" + }, + "length": { + "title": "Request Length", + "type": "integer" + }, + "referrer": { + "title": "HTTP Referrer", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "user_agent": { + "title": "HTTP User-Agent", + "type": "string" + }, + "version": { + "title": "HTTP Version", + "type": "string" + }, + "x_forwarded_for": { + "items": { + "type": "string" + }, + "title": "X-Forwarded-For", + "type": "array" + } + }, + "title": "HTTP Request", + "type": "object" + }, + "cvss": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Base Score", + "type": "number" + }, + "depth": { + "enum": [ + "Base", + "Environmental", + "Temporal" + ], + "title": "CVSS Depth", + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/$defs/metric" + }, + "title": "Metrics", + "type": "array" + }, + "overall_score": { + "title": "Overall Score", + "type": "number" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "vector_string": { + "title": "Vector String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "base_score", + "version" + ], + "title": "CVSS Score", + "type": "object" + }, + "fingerprint": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "algorithm_id", + "value" + ], + "title": "Fingerprint", + "type": "object" + }, + "process_entity": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "path" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Process Entity", + "type": "object" + }, + "observable": { + "additionalProperties": false, + "properties": { + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Observable", + "type": "object" + }, + "http_header": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "HTTP Header", + "type": "object" + }, + "attack": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "sub_technique" + ] + }, + { + "required": [ + "tactic" + ] + }, + { + "required": [ + "technique" + ] + } + ], + "properties": { + "mitigation": { + "$ref": "#/$defs/mitigation", + "title": "MITRE Mitigation" + }, + "sub_technique": { + "$ref": "#/$defs/sub_technique", + "title": "MITRE Sub-technique" + }, + "tactic": { + "$ref": "#/$defs/tactic", + "title": "MITRE Tactic" + }, + "tactics": { + "items": { + "$ref": "#/$defs/tactic" + }, + "title": "Tactics", + "type": "array" + }, + "technique": { + "$ref": "#/$defs/technique", + "title": "MITRE Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "type": "object" + }, + "network_interface": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "mac" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "open_ports": { + "items": { + "$ref": "#/$defs/port_info" + }, + "title": "Open Ports", + "type": "array" + }, + "subnet_prefix": { + "title": "Subnet Prefix Length", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Network Interface", + "type": "object" + }, + "user": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "account" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "forward_addr": { + "title": "Forwarding Address", + "type": "string" + }, + "full_name": { + "title": "Full Name", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "has_mfa": { + "title": "MFA Assigned", + "type": "boolean" + }, + "ldap_person": { + "$ref": "#/$defs/ldap_person", + "title": "LDAP Person" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "programmatic_credentials": { + "items": { + "$ref": "#/$defs/programmatic_credential" + }, + "title": "Programmatic Credentials", + "type": "array" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "User", + "type": "object" + }, + "auth_factor": { + "additionalProperties": false, + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "factor_type": { + "title": "Factor Type", + "type": "string" + }, + "factor_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Factor Type ID", + "type": "integer" + }, + "is_hotp": { + "title": "HMAC-based One-time Password (HOTP)", + "type": "boolean" + }, + "is_totp": { + "title": "Time-based One-time Password (TOTP)", + "type": "boolean" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "security_questions": { + "items": { + "type": "string" + }, + "title": "Security Questions", + "type": "array" + } + }, + "required": [ + "factor_type_id" + ], + "title": "Authentication Factor", + "type": "object" + }, + "osint": { + "additionalProperties": false, + "properties": { + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "email_auth": { + "$ref": "#/$defs/email_auth", + "title": "Related Email Authentication" + }, + "external_uid": { + "title": "External ID", + "type": "string" + }, + "tlp": { + "enum": [ + "AMBER", + "AMBER STRICT", + "CLEAR", + "GREEN", + "RED", + "WHITE" + ], + "title": "Traffic Light Protocol", + "type": "string" + }, + "detection_pattern": { + "title": "Detection Pattern", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Related Subdomains", + "type": "array" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uploaded_time": { + "title": "Uploaded Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "comment": { + "title": "Analyst Comments", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "value": { + "title": "Indicator", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "intrusion_sets": { + "items": { + "type": "string" + }, + "title": "Intrusion Sets", + "type": "array" + }, + "script": { + "$ref": "#/$defs/script", + "title": "Related Script Data" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "Related File" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "email": { + "$ref": "#/$defs/email", + "title": "Related Email" + }, + "kill_chain": { + "items": { + "$ref": "#/$defs/kill_chain_phase" + }, + "title": "Kill Chain", + "type": "array" + }, + "detection_pattern_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Detection Pattern Type ID", + "type": "integer" + }, + "detection_pattern_type": { + "title": "Detection Pattern", + "type": "string" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "threat_actor": { + "$ref": "#/$defs/threat_actor", + "title": "Threat Actor" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 99 + ], + "title": "Indicator Type ID", + "type": "integer" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "answers": { + "items": { + "$ref": "#/$defs/dns_answer" + }, + "title": "Related DNS Answers", + "type": "array" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "campaign": { + "$ref": "#/$defs/campaign", + "title": "Campaign" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "subnet": { + "title": "Related Subnet", + "type": "string" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Related Digital Signatures", + "type": "array" + }, + "whois": { + "$ref": "#/$defs/whois", + "title": "WHOIS" + }, + "vulnerabilities": { + "items": { + "$ref": "#/$defs/vulnerability" + }, + "title": "Related Vulnerabilities", + "type": "array" + } + }, + "required": [ + "type_id", + "value" + ], + "title": "OSINT", + "type": "object" + }, + "san": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "title": "Subject Alternative Name", + "type": "object" + }, + "occurrence_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cell_name" + ] + }, + { + "required": [ + "column_name" + ] + }, + { + "required": [ + "column_number" + ] + }, + { + "required": [ + "end_line" + ] + }, + { + "required": [ + "json_path" + ] + }, + { + "required": [ + "page_number" + ] + }, + { + "required": [ + "record_index_in_array" + ] + }, + { + "required": [ + "row_number" + ] + }, + { + "required": [ + "start_line" + ] + } + ], + "properties": { + "cell_name": { + "title": "Cell Name", + "type": "string" + }, + "column_name": { + "title": "Column Name", + "type": "string" + }, + "column_number": { + "title": "Column Number", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "json_path": { + "title": "JSON Path", + "type": "string" + }, + "page_number": { + "title": "Page Number", + "type": "integer" + }, + "record_index_in_array": { + "title": "Record Index in Array", + "type": "integer" + }, + "row_number": { + "title": "Row Number", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "title": "Occurrence Details", + "type": "object" + }, + "firewall_rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "condition": { + "title": "Condition", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "match_details": { + "items": { + "type": "string" + }, + "title": "Match Details", + "type": "array" + }, + "match_location": { + "title": "Match Location", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "sensitivity": { + "title": "Sensitivity", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Firewall Rule", + "type": "object" + }, + "threat_actor": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Threat Actor Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Threat Actor Type ID", + "type": "integer" + } + }, + "required": [ + "name" + ], + "title": "Threat Actor", + "type": "object" + }, + "package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Package", + "type": "object" + }, + "transformation_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Transformation Info", + "type": "object" + }, + "response": { + "additionalProperties": false, + "properties": { + "code": { + "title": "Response Code", + "type": "integer" + }, + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "error": { + "title": "Error Code", + "type": "string" + }, + "error_message": { + "title": "Error Message", + "type": "string" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "message": { + "title": "Message", + "type": "string" + } + }, + "title": "Response Elements", + "type": "object" + }, + "reporter": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Reporter", + "type": "object" + }, + "url": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "path" + ] + }, + { + "required": [ + "url_string" + ] + } + ], + "properties": { + "categories": { + "items": { + "type": "string" + }, + "title": "Website Categorization", + "type": "array" + }, + "category_ids": { + "items": { + "enum": [ + 0, + 1, + 3, + 4, + 5, + 6, + 7, + 9, + 11, + 14, + 15, + 16, + 17, + 18, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 40, + 43, + 44, + 45, + 46, + 47, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 63, + 64, + 65, + 66, + 67, + 68, + 71, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 92, + 93, + 95, + 96, + 97, + 98, + 99, + 101, + 102, + 103, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 118, + 121 + ], + "type": "integer" + }, + "title": "Website Categorization IDs", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "query_string": { + "title": "HTTP Query String", + "type": "string" + }, + "resource_type": { + "title": "Resource Type", + "type": "string" + }, + "scheme": { + "title": "Scheme", + "type": "string" + }, + "subdomain": { + "title": "Subdomain", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Uniform Resource Locator", + "type": "object" + }, + "object": { + "additionalProperties": true, + "properties": {}, + "title": "Object", + "type": "object" + }, + "extension": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "version" + ], + "title": "Schema Extension", + "type": "object" + }, + "gpu_info": { + "additionalProperties": false, + "properties": { + "bus_type": { + "title": "Bus Type", + "type": "string" + }, + "bus_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Bus Type ID", + "type": "integer" + }, + "cores": { + "title": "Cores", + "type": "integer" + }, + "model": { + "title": "Model", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "vram_mode": { + "title": "VRAM Mode", + "type": "string" + }, + "vram_mode_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "VRAM Mode ID", + "type": "integer" + }, + "vram_size": { + "title": "VRAM Size", + "type": "integer" + } + }, + "title": "GPU Information", + "type": "object" + }, + "port_info": { + "additionalProperties": false, + "properties": { + "port": { + "title": "Port", + "type": "integer" + }, + "protocol_name": { + "title": "Protocol Name", + "type": "string" + }, + "protocol_num": { + "title": "Protocol Number", + "type": "integer" + } + }, + "required": [ + "port" + ], + "title": "Port Information", + "type": "object" + }, + "service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Service", + "type": "object" + }, + "metric": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Metric", + "type": "object" + }, + "request": { + "additionalProperties": false, + "properties": { + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Request Elements", + "type": "object" + }, + "sub_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Sub-technique", + "type": "object" + }, + "container": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_driver": { + "title": "Network Driver", + "type": "string" + }, + "orchestrator": { + "title": "Orchestrator", + "type": "string" + }, + "pod_uuid": { + "title": "Pod UUID", + "type": "string" + }, + "runtime": { + "title": "Runtime", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Container", + "type": "object" + }, + "malware": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "classification_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 99 + ], + "type": "integer" + }, + "title": "Classification IDs", + "type": "array" + }, + "classifications": { + "items": { + "type": "string" + }, + "title": "Classifications", + "type": "array" + }, + "cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "CVE List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "path": { + "title": "Path", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "classification_ids" + ], + "title": "Malware", + "type": "object" + }, + "win_win_service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "service_category_id" + ] + }, + { + "required": [ + "service_dependencies" + ] + }, + { + "required": [ + "service_error_control_id" + ] + }, + { + "required": [ + "service_start_name" + ] + }, + { + "required": [ + "service_start_type_id" + ] + }, + { + "required": [ + "service_type_id" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "hosting_process": { + "$ref": "#/$defs/process_entity", + "title": "Hosting Process" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "load_order_group": { + "title": "Load Order Group", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "service_category": { + "title": "Service Category", + "type": "string" + }, + "service_category_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Service Category ID", + "type": "integer" + }, + "service_dependencies": { + "items": { + "type": "string" + }, + "title": "Service Dependencies", + "type": "array" + }, + "service_dll_file": { + "$ref": "#/$defs/file", + "title": "Service DLL" + }, + "service_error_control": { + "title": "Service Error Control", + "type": "string" + }, + "service_error_control_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Error Control ID", + "type": "integer" + }, + "service_file": { + "$ref": "#/$defs/file", + "title": "Service File" + }, + "service_start_name": { + "title": "Service Start Name", + "type": "string" + }, + "service_start_type": { + "title": "Service Start Type", + "type": "string" + }, + "service_start_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Service Start Type ID", + "type": "integer" + }, + "service_type": { + "title": "Service Type", + "type": "string" + }, + "service_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Type ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Windows Service", + "type": "object" + }, + "group": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "privileges": { + "items": { + "type": "string" + }, + "title": "Privileges", + "type": "array" + }, + "type": { + "title": "Group Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "Group", + "type": "object" + }, + "device": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "region": { + "title": "Region", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "os_machine_uuid": { + "title": "OS Machine UUID", + "type": "string" + }, + "hypervisor": { + "title": "Hypervisor", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "imei_list": { + "items": { + "type": "string" + }, + "title": "IMEI List", + "type": "array" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "is_shared": { + "title": "Shared Device", + "type": "boolean" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "meid": { + "title": "MEID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "is_personal": { + "title": "Personal Device", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "iccid": { + "title": "ICCID", + "type": "string" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "udid": { + "title": "Unique Device Identifier", + "type": "string" + }, + "model": { + "title": "Model", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "boot_time": { + "title": "Boot Time", + "type": "integer" + }, + "is_supervised": { + "title": "Supervised Device", + "type": "boolean" + }, + "is_managed": { + "title": "Managed Device", + "type": "boolean" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "is_trusted": { + "title": "Trusted Device", + "type": "boolean" + }, + "boot_uid": { + "title": "Boot UID", + "type": "string" + }, + "network_interfaces": { + "items": { + "$ref": "#/$defs/network_interface" + }, + "title": "Network Interfaces", + "type": "array" + }, + "autoscale_uid": { + "title": "Autoscale UID", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "subnet": { + "title": "Subnet", + "type": "string" + }, + "eid": { + "title": "EID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "imei": { + "title": "IMEI", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "is_mobile_account_active": { + "title": "Mobile Account Active", + "type": "boolean" + }, + "is_compliant": { + "title": "Compliant Device", + "type": "boolean" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Device", + "type": "object" + }, + "campaign": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Campaign", + "type": "object" + }, + "cwe": { + "additionalProperties": false, + "properties": { + "caption": { + "title": "Caption", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "CWE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CWE", + "type": "object" + }, + "digital_signature": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "developer_uid": { + "title": "Developer UID", + "type": "string" + }, + "digest": { + "$ref": "#/$defs/fingerprint", + "title": "Message Digest" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "State ID", + "type": "integer" + } + }, + "required": [ + "algorithm_id" + ], + "title": "Digital Signature", + "type": "object" + }, + "sbom": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "package": { + "$ref": "#/$defs/package", + "title": "Software Package" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "software_components": { + "items": { + "$ref": "#/$defs/software_component" + }, + "title": "Software Components", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "SBOM ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "package", + "software_components" + ], + "title": "Software Bill of Materials", + "type": "object" + }, + "malware_scan_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "end_time": { + "title": "End Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_files": { + "title": "Scanned Files", + "type": "integer" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "num_volumes": { + "title": "Number of Volumes", + "type": "integer" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Scan UID", + "type": "string" + }, + "unique_malware_count": { + "title": "Unique Malware Count", + "type": "integer" + } + }, + "required": [ + "type_id" + ], + "title": "Malware Scan Info", + "type": "object" + }, + "kill_chain_phase": { + "additionalProperties": false, + "properties": { + "phase": { + "title": "Kill Chain Phase", + "type": "string" + }, + "phase_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Kill Chain Phase ID", + "type": "integer" + } + }, + "required": [ + "phase_id" + ], + "title": "Kill Chain Phase", + "type": "object" + }, + "organization": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "ou_name": { + "title": "Org Unit Name", + "type": "string" + }, + "ou_uid": { + "title": "Org Unit ID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Organization", + "type": "object" + }, + "encryption_details": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Encryption Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Encryption Algorithm ID", + "type": "integer" + }, + "key_length": { + "title": "Encryption Key Length", + "type": "integer" + }, + "key_uid": { + "title": "Key UID", + "type": "string" + }, + "type": { + "title": "Encryption Type", + "type": "string" + } + }, + "title": "Encryption Details", + "type": "object" + }, + "logger": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "log_format": { + "title": "Log Format", + "type": "string" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Logger", + "type": "object" + }, + "feature": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Feature", + "type": "object" + }, + "timespan": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "duration" + ] + }, + { + "required": [ + "duration_days" + ] + }, + { + "required": [ + "duration_hours" + ] + }, + { + "required": [ + "duration_mins" + ] + }, + { + "required": [ + "duration_months" + ] + }, + { + "required": [ + "duration_secs" + ] + }, + { + "required": [ + "duration_weeks" + ] + }, + { + "required": [ + "duration_years" + ] + }, + { + "required": [ + "end_time" + ] + }, + { + "required": [ + "start_time" + ] + } + ], + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "duration_days": { + "title": "Duration Days", + "type": "integer" + }, + "duration_hours": { + "title": "Duration Hours", + "type": "integer" + }, + "duration_mins": { + "title": "Duration Minutes", + "type": "integer" + }, + "duration_months": { + "title": "Duration Months", + "type": "integer" + }, + "duration_secs": { + "title": "Duration Seconds", + "type": "integer" + }, + "duration_weeks": { + "title": "Duration Weeks", + "type": "integer" + }, + "duration_years": { + "title": "Duration Years", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Time Span Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Time Span Type ID", + "type": "integer" + } + }, + "title": "Time Span", + "type": "object" + }, + "rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Rule", + "type": "object" + }, + "software_component": { + "additionalProperties": false, + "properties": { + "author": { + "title": "Author", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "related_component": { + "title": "Related Component", + "type": "string" + }, + "relationship": { + "title": "Relationship", + "type": "string" + }, + "relationship_id": { + "enum": [ + 0, + 1, + 99 + ], + "title": "Relationship ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Component", + "type": "object" + }, + "agent": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Agent Name", + "type": "string" + }, + "policies": { + "items": { + "$ref": "#/$defs/policy" + }, + "title": "Agent Policies", + "type": "array" + }, + "type": { + "title": "Agent Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Agent ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate Agent ID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Agent Version", + "type": "string" + } + }, + "title": "Agent", + "type": "object" + }, + "whois": { + "additionalProperties": false, + "properties": { + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "created_time": { + "title": "Registered At", + "type": "integer" + }, + "dnssec_status": { + "title": "DNSSEC Status", + "type": "string" + }, + "dnssec_status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "DNSSEC Status ID", + "type": "integer" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "domain_contacts": { + "items": { + "$ref": "#/$defs/domain_contact" + }, + "title": "Domain Contacts", + "type": "array" + }, + "email_addr": { + "title": "Registrar Abuse Email Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "last_seen_time": { + "title": "Last Updated At", + "type": "integer" + }, + "name_servers": { + "items": { + "type": "string" + }, + "title": "Name Servers", + "type": "array" + }, + "phone_number": { + "title": "Registrar Abuse Phone Number", + "type": "string" + }, + "registrar": { + "title": "Domain Registrar", + "type": "string" + }, + "status": { + "title": "Domain Status", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Subdomains", + "type": "array" + }, + "subnet": { + "title": "Subnet Block", + "type": "string" + } + }, + "title": "WHOIS", + "type": "object" + }, + "key_value_object": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "value" + ] + }, + { + "required": [ + "values" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + }, + "values": { + "items": { + "type": "string" + }, + "title": "Values", + "type": "array" + } + }, + "required": [ + "name" + ], + "title": "Key:Value object", + "type": "object" + }, + "metadata": { + "additionalProperties": false, + "properties": { + "log_format": { + "title": "Log Source Format", + "type": "string" + }, + "transformation_info_list": { + "items": { + "$ref": "#/$defs/transformation_info" + }, + "title": "Transformation Info", + "type": "array" + }, + "uid": { + "title": "Event UID", + "type": "string" + }, + "original_event_uid": { + "title": "Original Event ID", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "extensions": { + "items": { + "$ref": "#/$defs/extension" + }, + "title": "Schema Extensions", + "type": "array" + }, + "source": { + "title": "Source", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "loggers": { + "items": { + "$ref": "#/$defs/logger" + }, + "title": "Loggers", + "type": "array" + }, + "processed_time": { + "title": "Processed Time", + "type": "integer" + }, + "log_source": { + "title": "Log Source", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "debug": { + "items": { + "type": "string" + }, + "title": "Debug Information", + "type": "array" + }, + "profiles": { + "items": { + "type": "string" + }, + "title": "Profiles", + "type": "array" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "event_code": { + "title": "Event Code", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "reporter": { + "$ref": "#/$defs/reporter", + "title": "Reporter" + }, + "correlation_uid": { + "title": "Correlation UID", + "type": "string" + }, + "extension": { + "$ref": "#/$defs/extension", + "title": "Schema Extension" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "sequence": { + "title": "Sequence Number", + "type": "integer" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "original_time": { + "title": "Original Time", + "type": "string" + }, + "total_queued_duration": { + "$ref": "#/$defs/timespan", + "title": "Total Queued Duration" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "product", + "version" + ], + "title": "Metadata", + "type": "object" + }, + "node": { + "additionalProperties": false, + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Node", + "type": "object" + }, + "product": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "feature": { + "$ref": "#/$defs/feature", + "title": "Feature" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Product", + "type": "object" + }, + "advisory": { + "additionalProperties": false, + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "Related CVEs", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Advisory ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Advisory", + "type": "object" + }, + "network_endpoint": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "title": "Network Endpoint", + "type": "object" + }, + "d3f_tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Tactic", + "type": "object" + }, + "cve": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "cvss": { + "items": { + "$ref": "#/$defs/cvss" + }, + "title": "CVSS Score", + "type": "array" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "cwe_uid": { + "title": "CWE UID", + "type": "string" + }, + "cwe_url": { + "title": "CWE URL", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "epss": { + "$ref": "#/$defs/epss", + "title": "EPSS" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "title": { + "title": "Title", + "type": "string" + }, + "type": { + "title": "Vulnerability Type", + "type": "string" + }, + "uid": { + "title": "CVE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CVE", + "type": "object" + }, + "domain_contact": { + "additionalProperties": false, + "properties": { + "email_addr": { + "title": "Contact Email", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Contact Location Information" + }, + "name": { + "title": "Name", + "type": "string" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "type": { + "title": "Domain Contact Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Domain Contact Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Domain Contact", + "type": "object" + }, + "policy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "type" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "is_applied": { + "title": "Applied", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Policy", + "type": "object" + }, + "span": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "end_time", + "start_time", + "uid" + ], + "title": "Span", + "type": "object" + } + }, + "$id": "https://schema.ocsf.io/schema/classes/api_activity", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "unmapped": { + "$ref": "#/$defs/object", + "title": "Unmapped Data" + }, + "message": { + "title": "Message", + "type": "string" + }, + "http_response": { + "$ref": "#/$defs/http_response", + "title": "HTTP Response" + }, + "disposition": { + "title": "Disposition", + "type": "string" + }, + "category_uid": { + "const": 6, + "title": "Category ID", + "type": "integer" + }, + "action": { + "title": "Action", + "type": "string" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "api": { + "$ref": "#/$defs/api", + "title": "API Details" + }, + "risk_details": { + "title": "Risk Details", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "status_detail": { + "title": "Status Detail", + "type": "string" + }, + "confidence_score": { + "title": "Confidence Score", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "disposition_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 99 + ], + "title": "Disposition ID", + "type": "integer" + }, + "type_name": { + "title": "Type Name", + "type": "string" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "raw_data": { + "title": "Raw Data", + "type": "string" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "actor": { + "$ref": "#/$defs/actor", + "title": "Actor" + }, + "trace": { + "$ref": "#/$defs/trace", + "title": "Trace" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "status": { + "title": "Status", + "type": "string" + }, + "activity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Activity ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "resources": { + "items": { + "$ref": "#/$defs/resource_details" + }, + "title": "Resources Array", + "type": "array" + }, + "observables": { + "items": { + "$ref": "#/$defs/observable" + }, + "title": "Observables", + "type": "array" + }, + "enrichments": { + "items": { + "$ref": "#/$defs/enrichment" + }, + "title": "Enrichments", + "type": "array" + }, + "http_request": { + "$ref": "#/$defs/http_request", + "title": "HTTP Request" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "raw_data_hash": { + "$ref": "#/$defs/fingerprint", + "title": "Raw Data Hash" + }, + "action_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Action ID", + "type": "integer" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "firewall_rule": { + "$ref": "#/$defs/firewall_rule", + "title": "Firewall Rule" + }, + "class_uid": { + "const": 6003, + "title": "Class ID", + "type": "integer" + }, + "activity_name": { + "title": "Activity", + "type": "string" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "class_name": { + "title": "Class", + "type": "string" + }, + "timezone_offset": { + "title": "Timezone Offset", + "type": "integer" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "raw_data_size": { + "title": "Raw Data Size", + "type": "integer" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "dst_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Destination Endpoint" + }, + "is_alert": { + "title": "Alert", + "type": "boolean" + }, + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "category_name": { + "title": "Category", + "type": "string" + }, + "malware_scan_info": { + "$ref": "#/$defs/malware_scan_info", + "title": "Malware Scan Info" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "count": { + "title": "Count", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "src_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Source Endpoint" + }, + "metadata": { + "$ref": "#/$defs/metadata", + "title": "Metadata" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "required": [ + "activity_id", + "actor", + "api", + "category_uid", + "class_uid", + "metadata", + "severity_id", + "src_endpoint", + "time", + "type_uid" + ], + "title": "API Activity", + "type": "object" +} diff --git a/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@container+host+trace.json b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@container+host+trace.json new file mode 100644 index 0000000000..7c1523afd3 --- /dev/null +++ b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@container+host+trace.json @@ -0,0 +1,8130 @@ +{ + "$defs": { + "kb_article": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "src_url" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "KB Article", + "type": "object" + }, + "long_string": { + "additionalProperties": false, + "properties": { + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "value" + ], + "title": "Long String", + "type": "object" + }, + "enrichment": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "short_desc": { + "title": "Short Description", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "data", + "name", + "value" + ], + "title": "Enrichment", + "type": "object" + }, + "ldap_person": { + "additionalProperties": false, + "properties": { + "cost_center": { + "title": "Cost Center", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "deleted_time": { + "title": "Deleted Time", + "type": "integer" + }, + "department": { + "title": "Department", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "email_addrs": { + "items": { + "type": "string" + }, + "title": "Email Addresses", + "type": "array" + }, + "employee_uid": { + "title": "Employee ID", + "type": "string" + }, + "given_name": { + "title": "Given Name", + "type": "string" + }, + "hire_time": { + "title": "Hire Time", + "type": "integer" + }, + "job_title": { + "title": "Job Title", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "last_login_time": { + "title": "Last Login", + "type": "integer" + }, + "ldap_cn": { + "title": "LDAP Common Name", + "type": "string" + }, + "ldap_dn": { + "title": "LDAP Distinguished Name", + "type": "string" + }, + "leave_time": { + "title": "Leave Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "manager": { + "$ref": "#/$defs/user", + "title": "Manager" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "office_location": { + "title": "Office Location", + "type": "string" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "surname": { + "title": "Surname", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + } + }, + "title": "LDAP Person", + "type": "object" + }, + "email_auth": { + "additionalProperties": false, + "properties": { + "dkim": { + "title": "DKIM Status", + "type": "string" + }, + "dkim_domain": { + "title": "DKIM Domain", + "type": "string" + }, + "dkim_signature": { + "title": "DKIM Signature", + "type": "string" + }, + "dmarc": { + "title": "DMARC Status", + "type": "string" + }, + "dmarc_override": { + "title": "DMARC Override", + "type": "string" + }, + "dmarc_policy": { + "title": "DMARC Policy", + "type": "string" + }, + "spf": { + "title": "SPF Status", + "type": "string" + } + }, + "title": "Email Authentication", + "type": "object" + }, + "epss": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "percentile": { + "title": "EPSS Percentile", + "type": "number" + }, + "score": { + "title": "EPPS Score", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "score" + ], + "title": "EPSS", + "type": "object" + }, + "device_hw_info": { + "additionalProperties": false, + "properties": { + "bios_date": { + "title": "BIOS Date", + "type": "string" + }, + "bios_manufacturer": { + "title": "BIOS Manufacturer", + "type": "string" + }, + "bios_ver": { + "title": "BIOS Version", + "type": "string" + }, + "chassis": { + "title": "Chassis", + "type": "string" + }, + "cpu_architecture": { + "title": "CPU Architecture", + "type": "string" + }, + "cpu_architecture_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "CPU Architecture ID", + "type": "integer" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "cpu_cores": { + "title": "CPU Cores", + "type": "integer" + }, + "cpu_count": { + "title": "CPU Count", + "type": "integer" + }, + "cpu_speed": { + "title": "Processor Speed", + "type": "integer" + }, + "cpu_type": { + "title": "Processor Type", + "type": "string" + }, + "desktop_display": { + "$ref": "#/$defs/display", + "title": "Desktop Display" + }, + "gpu_count": { + "title": "GPU Count", + "type": "integer" + }, + "gpu_info_list": { + "items": { + "$ref": "#/$defs/gpu_info" + }, + "title": "GPU information", + "type": "array" + }, + "keyboard_info": { + "$ref": "#/$defs/keyboard_info", + "title": "Keyboard Information" + }, + "ram_size": { + "title": "RAM Size", + "type": "integer" + }, + "serial_number": { + "title": "Serial Number", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Device Hardware Info", + "type": "object" + }, + "network_proxy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "title": "Network Proxy Endpoint", + "type": "object" + }, + "data_classification": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "category_id" + ] + }, + { + "required": [ + "confidentiality_id" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "category_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Category ID", + "type": "integer" + }, + "classifier_details": { + "$ref": "#/$defs/classifier_details", + "title": "Classifier Details" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "discovery_details": { + "items": { + "$ref": "#/$defs/discovery_details" + }, + "title": "Discovery Details", + "type": "array" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "status_details": { + "items": { + "type": "string" + }, + "title": "Status Details", + "type": "array" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Data Classification", + "type": "object" + }, + "http_response": { + "additionalProperties": false, + "properties": { + "body_length": { + "title": "Response Body Length", + "type": "integer" + }, + "code": { + "title": "Response Code", + "type": "integer" + }, + "content_type": { + "title": "HTTP Content Type", + "type": "string" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "latency": { + "title": "Latency", + "type": "integer" + }, + "length": { + "title": "Response Length", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "code" + ], + "title": "HTTP Response", + "type": "object" + }, + "resource_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "criticality": { + "title": "Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Resource Relationship" + }, + "role": { + "title": "Role", + "type": "string" + }, + "role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Role ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Resource Details", + "type": "object" + }, + "display": { + "additionalProperties": false, + "properties": { + "color_depth": { + "title": "Color Depth", + "type": "integer" + }, + "physical_height": { + "title": "Physical Height", + "type": "integer" + }, + "physical_orientation": { + "title": "Physical Orientation", + "type": "integer" + }, + "physical_width": { + "title": "Physical Width", + "type": "integer" + }, + "scale_factor": { + "title": "Scale Factor", + "type": "integer" + } + }, + "title": "Display", + "type": "object" + }, + "scim": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "error_message": { + "title": "Last Error Message", + "type": "string" + }, + "is_group_provisioning_enabled": { + "title": "SCIM Group Provisioning Enabled", + "type": "boolean" + }, + "is_user_provisioning_enabled": { + "title": "SCIM User Provisioning Enabled", + "type": "boolean" + }, + "last_run_time": { + "title": "Last Sync Time", + "type": "integer" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "scim_group_schema": { + "title": "SCIM Group Schema" + }, + "scim_user_schema": { + "title": "SCIM User Schema" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "External ID", + "type": "string" + }, + "url_string": { + "title": "SCIM Endpoint URL", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + }, + "version": { + "title": "SCIM Version", + "type": "string" + } + }, + "title": "SCIM", + "type": "object" + }, + "programmatic_credential": { + "additionalProperties": false, + "properties": { + "last_used_time": { + "title": "Last Used Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Programmatic Credential", + "type": "object" + }, + "os": { + "additionalProperties": false, + "properties": { + "build": { + "title": "OS Build", + "type": "string" + }, + "country": { + "title": "Country", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "edition": { + "title": "OS Edition", + "type": "string" + }, + "kernel_release": { + "title": "Kernel Release", + "type": "string" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "sp_name": { + "title": "OS Service Pack", + "type": "string" + }, + "sp_ver": { + "title": "OS Service Pack Version", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 99, + 100, + 101, + 200, + 201, + 300, + 301, + 302, + 400, + 401, + 402 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "Operating System (OS)", + "type": "object" + }, + "tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Tactic", + "type": "object" + }, + "dns_answer": { + "additionalProperties": false, + "properties": { + "class": { + "title": "Resource Record Class", + "type": "string" + }, + "flag_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "type": "integer" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "packet_uid": { + "title": "Packet UID", + "type": "integer" + }, + "rdata": { + "title": "DNS RData", + "type": "string" + }, + "ttl": { + "title": "TTL", + "type": "integer" + }, + "type": { + "title": "Resource Record Type", + "type": "string" + } + }, + "required": [ + "rdata" + ], + "title": "DNS Answer", + "type": "object" + }, + "discovery_details": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "occurrence_details": { + "$ref": "#/$defs/occurrence_details", + "title": "Occurrence Details" + }, + "occurrences": { + "items": { + "$ref": "#/$defs/occurrence_details" + }, + "title": "Occurrences", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "title": "Discovery Details", + "type": "object" + }, + "sso": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "SAML Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "duration_mins": { + "title": "SSO Session Duration", + "type": "integer" + }, + "idle_timeout": { + "title": "SSO Idle Timeout", + "type": "integer" + }, + "login_endpoint": { + "title": "SSO Login Endpoint", + "type": "string" + }, + "logout_endpoint": { + "title": "SSO Logout Endpoint", + "type": "string" + }, + "metadata_endpoint": { + "title": "SSO Metadata Endpoint", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "title": "Scopes", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + } + }, + "title": "SSO", + "type": "object" + }, + "token": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_renewable": { + "title": "Renewable", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Token", + "type": "object" + }, + "api": { + "additionalProperties": false, + "properties": { + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "request": { + "$ref": "#/$defs/request", + "title": "API Request Details" + }, + "response": { + "$ref": "#/$defs/response", + "title": "API Response Details" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "token": { + "$ref": "#/$defs/token", + "title": "Token" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "operation" + ], + "title": "API", + "type": "object" + }, + "d3fend": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "d3f_tactic" + ] + }, + { + "required": [ + "d3f_technique" + ] + } + ], + "properties": { + "d3f_tactic": { + "$ref": "#/$defs/d3f_tactic", + "title": "MITRE D3FEND\u2122 Tactic" + }, + "d3f_technique": { + "$ref": "#/$defs/d3f_technique", + "title": "MITRE D3FEND\u2122 Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122", + "type": "object" + }, + "reputation": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Reputation Score", + "type": "number" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "score": { + "title": "Reputation Score", + "type": "string" + }, + "score_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "Reputation Score ID", + "type": "integer" + } + }, + "required": [ + "base_score", + "score_id" + ], + "title": "Reputation", + "type": "object" + }, + "mitigation": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "countermeasures": { + "items": { + "$ref": "#/$defs/d3fend" + }, + "title": "Countermeasures", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Mitigation", + "type": "object" + }, + "location": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "city" + ] + }, + { + "required": [ + "country" + ] + }, + { + "required": [ + "postal_code" + ] + }, + { + "required": [ + "region" + ] + } + ], + "properties": { + "aerial_height": { + "title": "Aerial Height", + "type": "string" + }, + "city": { + "title": "City", + "type": "string" + }, + "continent": { + "title": "Continent", + "type": "string" + }, + "coordinates": { + "items": { + "type": "number" + }, + "title": "Coordinates", + "type": "array" + }, + "country": { + "title": "Country", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "geodetic_altitude": { + "title": "Geodetic Altitude", + "type": "string" + }, + "geodetic_vertical_accuracy": { + "title": "Geodetic Vertical Accuracy", + "type": "string" + }, + "geohash": { + "title": "Geohash", + "type": "string" + }, + "horizontal_accuracy": { + "title": "Horizontal Accuracy", + "type": "string" + }, + "is_on_premises": { + "title": "On-Premises", + "type": "boolean" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "lat": { + "title": "Latitude", + "type": "number" + }, + "long": { + "title": "Longitude", + "type": "number" + }, + "postal_code": { + "title": "Postal Code", + "type": "string" + }, + "pressure_altitude": { + "title": "Pressure Altitude", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + } + }, + "title": "Geo Location", + "type": "object" + }, + "idp": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "auth_factors": { + "items": { + "$ref": "#/$defs/auth_factor" + }, + "title": "Authentication Factors", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprint": { + "$ref": "#/$defs/fingerprint", + "title": "Certificate Fingerprint" + }, + "has_mfa": { + "title": "MFA Enforced", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scim": { + "$ref": "#/$defs/scim", + "title": "SCIM" + }, + "sso": { + "$ref": "#/$defs/sso", + "title": "SSO" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "Configuration URL", + "type": "string" + } + }, + "title": "Identity Provider", + "type": "object" + }, + "message_context": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "application" + ] + }, + { + "required": [ + "service" + ] + } + ], + "properties": { + "ai_role": { + "title": "AI Role", + "type": "string" + }, + "ai_role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "AI Role ID", + "type": "integer" + }, + "application": { + "$ref": "#/$defs/application", + "title": "Application" + }, + "completion_tokens": { + "title": "Completion Tokens", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "prompt_tokens": { + "title": "Prompt Tokens", + "type": "integer" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "total_tokens": { + "title": "Total Tokens", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Message Context", + "type": "object" + }, + "process": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ancestry": { + "items": { + "$ref": "#/$defs/process_entity" + }, + "title": "Ancestry", + "type": "array" + }, + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "environment_variables": { + "items": { + "$ref": "#/$defs/environment_variable" + }, + "title": "Environment Variables", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hosted_services": { + "items": { + "$ref": "#/$defs/win_win_service" + }, + "title": "Hosted Services", + "type": "array" + }, + "integrity": { + "title": "Integrity", + "type": "string" + }, + "integrity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Integrity Level", + "type": "integer" + }, + "lineage": { + "items": { + "type": "string" + }, + "title": "Lineage", + "type": "array" + }, + "loaded_modules": { + "items": { + "type": "string" + }, + "title": "Loaded Modules", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "parent_process": { + "$ref": "#/$defs/process", + "title": "Parent Process" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "ptid": { + "title": "Process Thread ID", + "type": "integer" + }, + "sandbox": { + "title": "Sandbox", + "type": "string" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "terminated_time": { + "title": "Terminated Time", + "type": "integer" + }, + "tid": { + "title": "Thread ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + }, + "working_directory": { + "title": "Working Directory", + "type": "string" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + } + }, + "title": "Process", + "type": "object" + }, + "vulnerability": { + "additionalProperties": false, + "oneOf": [ + { + "not": { + "required": [ + "cve", + "cwe" + ] + }, + "required": [ + "advisory" + ] + }, + { + "not": { + "required": [ + "advisory", + "cwe" + ] + }, + "required": [ + "cve" + ] + }, + { + "not": { + "required": [ + "advisory", + "cve" + ] + }, + "required": [ + "cwe" + ] + } + ], + "properties": { + "advisory": { + "$ref": "#/$defs/advisory", + "title": "Security Advisory" + }, + "affected_code": { + "items": { + "$ref": "#/$defs/affected_code" + }, + "title": "Affected Code", + "type": "array" + }, + "affected_packages": { + "items": { + "$ref": "#/$defs/affected_package" + }, + "title": "Affected Software Packages", + "type": "array" + }, + "category": { + "title": "Category", + "type": "string" + }, + "cve": { + "$ref": "#/$defs/cve", + "title": "CVE" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "dependency_chain": { + "title": "Dependency Chain", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "exploit_last_seen_time": { + "title": "Exploit Last Seen Time", + "type": "integer" + }, + "exploit_ref_url": { + "title": "Exploit URL", + "type": "string" + }, + "exploit_requirement": { + "title": "Exploit Requirement", + "type": "string" + }, + "exploit_type": { + "title": "Exploit Type", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "fix_coverage": { + "title": "Fix Coverage", + "type": "string" + }, + "fix_coverage_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Fix Coverage ID", + "type": "integer" + }, + "is_exploit_available": { + "title": "Exploit Availability", + "type": "boolean" + }, + "is_fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "packages": { + "items": { + "$ref": "#/$defs/package" + }, + "title": "Software Packages", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_vulnerabilities": { + "items": { + "type": "string" + }, + "title": "Related Vulnerability IDs", + "type": "array" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Vulnerability Details", + "type": "object" + }, + "affected_code": { + "additionalProperties": false, + "properties": { + "end_column": { + "title": "End Column", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "rule": { + "$ref": "#/$defs/rule", + "title": "Related Rule" + }, + "start_column": { + "title": "Start Column", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "required": [ + "file" + ], + "title": "Affected Code", + "type": "object" + }, + "keyboard_info": { + "additionalProperties": false, + "properties": { + "function_keys": { + "title": "Function Keys", + "type": "integer" + }, + "ime": { + "title": "IME", + "type": "string" + }, + "keyboard_layout": { + "title": "Keyboard Layout", + "type": "string" + }, + "keyboard_subtype": { + "title": "Keyboard Subtype", + "type": "integer" + }, + "keyboard_type": { + "title": "Keyboard Type", + "type": "string" + } + }, + "title": "Keyboard Information", + "type": "object" + }, + "certificate": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "is_self_signed": { + "title": "Certificate Self-Signed", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Distinguished Name", + "type": "string" + }, + "sans": { + "items": { + "$ref": "#/$defs/san" + }, + "title": "Subject Alternative Names", + "type": "array" + }, + "serial_number": { + "title": "Certificate Serial Number", + "type": "string" + }, + "subject": { + "title": "Subject Distinguished Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "issuer", + "serial_number" + ], + "title": "Digital Certificate", + "type": "object" + }, + "graph": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "edges": { + "items": { + "$ref": "#/$defs/edge" + }, + "title": "Edges", + "type": "array" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "nodes": { + "items": { + "$ref": "#/$defs/node" + }, + "title": "Nodes", + "type": "array" + }, + "query_language": { + "title": "Query Language", + "type": "string" + }, + "query_language_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Query Language ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "nodes" + ], + "title": "Graph", + "type": "object" + }, + "account": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "is_disabled": { + "title": "Disabled", + "type": "boolean" + }, + "is_locked": { + "title": "Locked", + "type": "boolean" + }, + "is_on_premises_sync_enabled": { + "title": "On-Premises Sync Enabled", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Account", + "type": "object" + }, + "authorization": { + "additionalProperties": false, + "properties": { + "decision": { + "title": "Authorization Decision/Outcome", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "title": "Authorization Result", + "type": "object" + }, + "autonomous_system": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "number" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "number": { + "title": "Number", + "type": "integer" + } + }, + "title": "Autonomous System", + "type": "object" + }, + "classifier_details": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "Classifier Details", + "type": "object" + }, + "session": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "expiration_reason": { + "title": "Expiration Reason", + "type": "string" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_mfa": { + "title": "Multi Factor Authentication", + "type": "boolean" + }, + "is_remote": { + "title": "Remote", + "type": "boolean" + }, + "is_vpn": { + "title": "VPN Session", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "terminal": { + "title": "Terminal", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + } + }, + "title": "Session", + "type": "object" + }, + "d3f_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Technique", + "type": "object" + }, + "image": { + "additionalProperties": false, + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Image", + "type": "object" + }, + "application": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "criticality": { + "title": "Business Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Application Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Application Name", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Application Relationship" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "sbom": { + "$ref": "#/$defs/sbom", + "title": "Related SBOM" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Application Type", + "type": "string" + }, + "uid": { + "title": "Application ID", + "type": "string" + }, + "uid_alt": { + "title": "Application Alternative ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "version": { + "title": "Application Version", + "type": "string" + } + }, + "title": "Application", + "type": "object" + }, + "script": { + "additionalProperties": false, + "properties": { + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "script_content": { + "$ref": "#/$defs/long_string", + "title": "Script Content" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "script_content", + "type_id" + ], + "title": "Script", + "type": "object" + }, + "affected_package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "fixed_in_version": { + "title": "Fixed In Version", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Affected Software Package", + "type": "object" + }, + "cis_control": { + "additionalProperties": false, + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "CIS Control", + "type": "object" + }, + "analytic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Analytic", + "type": "object" + }, + "edge": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "relation": { + "title": "Relation", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "target": { + "title": "Target", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "source", + "target" + ], + "title": "Edge", + "type": "object" + }, + "actor": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "app_name" + ] + }, + { + "required": [ + "app_uid" + ] + }, + { + "required": [ + "invoked_by" + ] + }, + { + "required": [ + "process" + ] + }, + { + "required": [ + "session" + ] + }, + { + "required": [ + "user" + ] + } + ], + "properties": { + "app_name": { + "title": "Application Name", + "type": "string" + }, + "app_uid": { + "title": "Application ID", + "type": "string" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "idp": { + "$ref": "#/$defs/idp", + "title": "Identity Provider" + }, + "invoked_by": { + "title": "Invoked by", + "type": "string" + }, + "process": { + "$ref": "#/$defs/process", + "title": "Process" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + } + }, + "title": "Actor", + "type": "object" + }, + "cloud": { + "additionalProperties": false, + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "cloud_partition": { + "title": "Cloud Partition", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "project_uid": { + "title": "Project ID", + "type": "string" + }, + "provider": { + "title": "Cloud Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + }, + "zone": { + "title": "Cloud Availability Zone", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "Cloud", + "type": "object" + }, + "trace": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "span": { + "$ref": "#/$defs/span", + "title": "Span" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Trace", + "type": "object" + }, + "ai_model": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ai_provider": { + "title": "AI Provider", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "ai_provider" + ], + "title": "AI Model", + "type": "object" + }, + "file": { + "additionalProperties": false, + "properties": { + "parent_folder": { + "title": "Parent Folder", + "type": "string" + }, + "is_system": { + "title": "System", + "type": "boolean" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "accessor": { + "$ref": "#/$defs/user", + "title": "Accessor" + }, + "is_readonly": { + "title": "Read-Only", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "ext": { + "title": "File Extension", + "type": "string" + }, + "company_name": { + "title": "Company Name", + "type": "string" + }, + "attributes": { + "title": "Attributes", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uri": { + "title": "File URI", + "type": "string" + }, + "is_deleted": { + "title": "Deleted", + "type": "boolean" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "drive_type": { + "title": "Drive Type", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "imported_symbols": { + "items": { + "type": "string" + }, + "title": "Imported Symbols", + "type": "array" + }, + "signature": { + "$ref": "#/$defs/digital_signature", + "title": "Digital Signature" + }, + "is_encrypted": { + "title": "Encrypted", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "accessed_time": { + "title": "Accessed Time", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "internal_name": { + "title": "Internal Name", + "type": "string" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "drive_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Drive Type ID", + "type": "integer" + }, + "security_descriptor": { + "title": "Security Descriptor", + "type": "string" + }, + "volume": { + "title": "Volume", + "type": "string" + }, + "modifier": { + "$ref": "#/$defs/user", + "title": "Modifier" + }, + "mime_type": { + "title": "MIME type", + "type": "string" + }, + "encryption_details": { + "$ref": "#/$defs/encryption_details", + "title": "Encryption Details" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Digital Signatures", + "type": "array" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "File", + "type": "object" + }, + "remediation": { + "additionalProperties": false, + "properties": { + "cis_controls": { + "items": { + "$ref": "#/$defs/cis_control" + }, + "title": "CIS Controls", + "type": "array" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + } + }, + "required": [ + "desc" + ], + "title": "Remediation", + "type": "object" + }, + "email": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "from" + ] + }, + { + "required": [ + "to" + ] + } + ], + "properties": { + "cc": { + "items": { + "type": "string" + }, + "title": "Cc", + "type": "array" + }, + "cc_mailboxes": { + "items": { + "type": "string" + }, + "title": "Cc Mailboxes", + "type": "array" + }, + "delivered_to": { + "title": "Delivered To", + "type": "string" + }, + "delivered_to_list": { + "items": { + "type": "string" + }, + "title": "Delivered To List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "from": { + "title": "From", + "type": "string" + }, + "from_list": { + "items": { + "type": "string" + }, + "title": "From List", + "type": "array" + }, + "from_mailbox": { + "title": "From Mailbox", + "type": "string" + }, + "from_mailboxes": { + "items": { + "type": "string" + }, + "title": "From Mailboxes", + "type": "array" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "is_read": { + "title": "Read", + "type": "boolean" + }, + "message_uid": { + "title": "Message UID", + "type": "string" + }, + "raw_header": { + "title": "Raw Header", + "type": "string" + }, + "reply_to": { + "title": "Reply To", + "type": "string" + }, + "reply_to_list": { + "items": { + "type": "string" + }, + "title": "Reply To List", + "type": "array" + }, + "reply_to_mailboxes": { + "items": { + "type": "string" + }, + "title": "Reply To Mailboxes", + "type": "array" + }, + "return_path": { + "title": "Return Path", + "type": "string" + }, + "sender": { + "title": "Sender", + "type": "string" + }, + "sender_mailbox": { + "title": "Sender Mailbox", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "smtp_from": { + "title": "SMTP From", + "type": "string" + }, + "smtp_to": { + "items": { + "type": "string" + }, + "title": "SMTP To", + "type": "array" + }, + "subject": { + "title": "Subject", + "type": "string" + }, + "to": { + "items": { + "type": "string" + }, + "title": "To", + "type": "array" + }, + "to_mailboxes": { + "items": { + "type": "string" + }, + "title": "To Mailboxes", + "type": "array" + }, + "uid": { + "title": "Email Thread UID", + "type": "string" + }, + "urls": { + "items": { + "$ref": "#/$defs/url" + }, + "title": "URLs", + "type": "array" + }, + "x_originating_ip": { + "items": { + "type": "string" + }, + "title": "X-Originating-IP", + "type": "array" + } + }, + "title": "Email", + "type": "object" + }, + "environment_variable": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Environment Variable", + "type": "object" + }, + "technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Technique", + "type": "object" + }, + "http_request": { + "additionalProperties": false, + "properties": { + "args": { + "title": "HTTP Arguments", + "type": "string" + }, + "body_length": { + "title": "Request Body Length", + "type": "integer" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "http_method": { + "enum": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "title": "HTTP Method", + "type": "string" + }, + "length": { + "title": "Request Length", + "type": "integer" + }, + "referrer": { + "title": "HTTP Referrer", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "user_agent": { + "title": "HTTP User-Agent", + "type": "string" + }, + "version": { + "title": "HTTP Version", + "type": "string" + }, + "x_forwarded_for": { + "items": { + "type": "string" + }, + "title": "X-Forwarded-For", + "type": "array" + } + }, + "title": "HTTP Request", + "type": "object" + }, + "cvss": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Base Score", + "type": "number" + }, + "depth": { + "enum": [ + "Base", + "Environmental", + "Temporal" + ], + "title": "CVSS Depth", + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/$defs/metric" + }, + "title": "Metrics", + "type": "array" + }, + "overall_score": { + "title": "Overall Score", + "type": "number" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "vector_string": { + "title": "Vector String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "base_score", + "version" + ], + "title": "CVSS Score", + "type": "object" + }, + "fingerprint": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "algorithm_id", + "value" + ], + "title": "Fingerprint", + "type": "object" + }, + "process_entity": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "path" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Process Entity", + "type": "object" + }, + "observable": { + "additionalProperties": false, + "properties": { + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Observable", + "type": "object" + }, + "http_header": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "HTTP Header", + "type": "object" + }, + "attack": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "sub_technique" + ] + }, + { + "required": [ + "tactic" + ] + }, + { + "required": [ + "technique" + ] + } + ], + "properties": { + "mitigation": { + "$ref": "#/$defs/mitigation", + "title": "MITRE Mitigation" + }, + "sub_technique": { + "$ref": "#/$defs/sub_technique", + "title": "MITRE Sub-technique" + }, + "tactic": { + "$ref": "#/$defs/tactic", + "title": "MITRE Tactic" + }, + "tactics": { + "items": { + "$ref": "#/$defs/tactic" + }, + "title": "Tactics", + "type": "array" + }, + "technique": { + "$ref": "#/$defs/technique", + "title": "MITRE Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "type": "object" + }, + "network_interface": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "mac" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "open_ports": { + "items": { + "$ref": "#/$defs/port_info" + }, + "title": "Open Ports", + "type": "array" + }, + "subnet_prefix": { + "title": "Subnet Prefix Length", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Network Interface", + "type": "object" + }, + "user": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "account" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "forward_addr": { + "title": "Forwarding Address", + "type": "string" + }, + "full_name": { + "title": "Full Name", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "has_mfa": { + "title": "MFA Assigned", + "type": "boolean" + }, + "ldap_person": { + "$ref": "#/$defs/ldap_person", + "title": "LDAP Person" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "programmatic_credentials": { + "items": { + "$ref": "#/$defs/programmatic_credential" + }, + "title": "Programmatic Credentials", + "type": "array" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "User", + "type": "object" + }, + "auth_factor": { + "additionalProperties": false, + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "factor_type": { + "title": "Factor Type", + "type": "string" + }, + "factor_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Factor Type ID", + "type": "integer" + }, + "is_hotp": { + "title": "HMAC-based One-time Password (HOTP)", + "type": "boolean" + }, + "is_totp": { + "title": "Time-based One-time Password (TOTP)", + "type": "boolean" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "security_questions": { + "items": { + "type": "string" + }, + "title": "Security Questions", + "type": "array" + } + }, + "required": [ + "factor_type_id" + ], + "title": "Authentication Factor", + "type": "object" + }, + "osint": { + "additionalProperties": false, + "properties": { + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "email_auth": { + "$ref": "#/$defs/email_auth", + "title": "Related Email Authentication" + }, + "external_uid": { + "title": "External ID", + "type": "string" + }, + "tlp": { + "enum": [ + "AMBER", + "AMBER STRICT", + "CLEAR", + "GREEN", + "RED", + "WHITE" + ], + "title": "Traffic Light Protocol", + "type": "string" + }, + "detection_pattern": { + "title": "Detection Pattern", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Related Subdomains", + "type": "array" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uploaded_time": { + "title": "Uploaded Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "comment": { + "title": "Analyst Comments", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "value": { + "title": "Indicator", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "intrusion_sets": { + "items": { + "type": "string" + }, + "title": "Intrusion Sets", + "type": "array" + }, + "script": { + "$ref": "#/$defs/script", + "title": "Related Script Data" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "Related File" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "email": { + "$ref": "#/$defs/email", + "title": "Related Email" + }, + "kill_chain": { + "items": { + "$ref": "#/$defs/kill_chain_phase" + }, + "title": "Kill Chain", + "type": "array" + }, + "detection_pattern_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Detection Pattern Type ID", + "type": "integer" + }, + "detection_pattern_type": { + "title": "Detection Pattern", + "type": "string" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "threat_actor": { + "$ref": "#/$defs/threat_actor", + "title": "Threat Actor" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 99 + ], + "title": "Indicator Type ID", + "type": "integer" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "answers": { + "items": { + "$ref": "#/$defs/dns_answer" + }, + "title": "Related DNS Answers", + "type": "array" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "campaign": { + "$ref": "#/$defs/campaign", + "title": "Campaign" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "subnet": { + "title": "Related Subnet", + "type": "string" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Related Digital Signatures", + "type": "array" + }, + "whois": { + "$ref": "#/$defs/whois", + "title": "WHOIS" + }, + "vulnerabilities": { + "items": { + "$ref": "#/$defs/vulnerability" + }, + "title": "Related Vulnerabilities", + "type": "array" + } + }, + "required": [ + "type_id", + "value" + ], + "title": "OSINT", + "type": "object" + }, + "san": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "title": "Subject Alternative Name", + "type": "object" + }, + "occurrence_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cell_name" + ] + }, + { + "required": [ + "column_name" + ] + }, + { + "required": [ + "column_number" + ] + }, + { + "required": [ + "end_line" + ] + }, + { + "required": [ + "json_path" + ] + }, + { + "required": [ + "page_number" + ] + }, + { + "required": [ + "record_index_in_array" + ] + }, + { + "required": [ + "row_number" + ] + }, + { + "required": [ + "start_line" + ] + } + ], + "properties": { + "cell_name": { + "title": "Cell Name", + "type": "string" + }, + "column_name": { + "title": "Column Name", + "type": "string" + }, + "column_number": { + "title": "Column Number", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "json_path": { + "title": "JSON Path", + "type": "string" + }, + "page_number": { + "title": "Page Number", + "type": "integer" + }, + "record_index_in_array": { + "title": "Record Index in Array", + "type": "integer" + }, + "row_number": { + "title": "Row Number", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "title": "Occurrence Details", + "type": "object" + }, + "firewall_rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "condition": { + "title": "Condition", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "match_details": { + "items": { + "type": "string" + }, + "title": "Match Details", + "type": "array" + }, + "match_location": { + "title": "Match Location", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "sensitivity": { + "title": "Sensitivity", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Firewall Rule", + "type": "object" + }, + "threat_actor": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Threat Actor Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Threat Actor Type ID", + "type": "integer" + } + }, + "required": [ + "name" + ], + "title": "Threat Actor", + "type": "object" + }, + "package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Package", + "type": "object" + }, + "transformation_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Transformation Info", + "type": "object" + }, + "response": { + "additionalProperties": false, + "properties": { + "code": { + "title": "Response Code", + "type": "integer" + }, + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "error": { + "title": "Error Code", + "type": "string" + }, + "error_message": { + "title": "Error Message", + "type": "string" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "message": { + "title": "Message", + "type": "string" + } + }, + "title": "Response Elements", + "type": "object" + }, + "reporter": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Reporter", + "type": "object" + }, + "url": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "path" + ] + }, + { + "required": [ + "url_string" + ] + } + ], + "properties": { + "categories": { + "items": { + "type": "string" + }, + "title": "Website Categorization", + "type": "array" + }, + "category_ids": { + "items": { + "enum": [ + 0, + 1, + 3, + 4, + 5, + 6, + 7, + 9, + 11, + 14, + 15, + 16, + 17, + 18, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 40, + 43, + 44, + 45, + 46, + 47, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 63, + 64, + 65, + 66, + 67, + 68, + 71, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 92, + 93, + 95, + 96, + 97, + 98, + 99, + 101, + 102, + 103, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 118, + 121 + ], + "type": "integer" + }, + "title": "Website Categorization IDs", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "query_string": { + "title": "HTTP Query String", + "type": "string" + }, + "resource_type": { + "title": "Resource Type", + "type": "string" + }, + "scheme": { + "title": "Scheme", + "type": "string" + }, + "subdomain": { + "title": "Subdomain", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Uniform Resource Locator", + "type": "object" + }, + "object": { + "additionalProperties": true, + "properties": {}, + "title": "Object", + "type": "object" + }, + "extension": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "version" + ], + "title": "Schema Extension", + "type": "object" + }, + "gpu_info": { + "additionalProperties": false, + "properties": { + "bus_type": { + "title": "Bus Type", + "type": "string" + }, + "bus_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Bus Type ID", + "type": "integer" + }, + "cores": { + "title": "Cores", + "type": "integer" + }, + "model": { + "title": "Model", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "vram_mode": { + "title": "VRAM Mode", + "type": "string" + }, + "vram_mode_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "VRAM Mode ID", + "type": "integer" + }, + "vram_size": { + "title": "VRAM Size", + "type": "integer" + } + }, + "title": "GPU Information", + "type": "object" + }, + "port_info": { + "additionalProperties": false, + "properties": { + "port": { + "title": "Port", + "type": "integer" + }, + "protocol_name": { + "title": "Protocol Name", + "type": "string" + }, + "protocol_num": { + "title": "Protocol Number", + "type": "integer" + } + }, + "required": [ + "port" + ], + "title": "Port Information", + "type": "object" + }, + "service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Service", + "type": "object" + }, + "metric": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Metric", + "type": "object" + }, + "request": { + "additionalProperties": false, + "properties": { + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Request Elements", + "type": "object" + }, + "sub_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Sub-technique", + "type": "object" + }, + "container": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_driver": { + "title": "Network Driver", + "type": "string" + }, + "orchestrator": { + "title": "Orchestrator", + "type": "string" + }, + "pod_uuid": { + "title": "Pod UUID", + "type": "string" + }, + "runtime": { + "title": "Runtime", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Container", + "type": "object" + }, + "malware": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "classification_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 99 + ], + "type": "integer" + }, + "title": "Classification IDs", + "type": "array" + }, + "classifications": { + "items": { + "type": "string" + }, + "title": "Classifications", + "type": "array" + }, + "cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "CVE List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "path": { + "title": "Path", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "classification_ids" + ], + "title": "Malware", + "type": "object" + }, + "win_win_service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "service_category_id" + ] + }, + { + "required": [ + "service_dependencies" + ] + }, + { + "required": [ + "service_error_control_id" + ] + }, + { + "required": [ + "service_start_name" + ] + }, + { + "required": [ + "service_start_type_id" + ] + }, + { + "required": [ + "service_type_id" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "hosting_process": { + "$ref": "#/$defs/process_entity", + "title": "Hosting Process" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "load_order_group": { + "title": "Load Order Group", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "service_category": { + "title": "Service Category", + "type": "string" + }, + "service_category_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Service Category ID", + "type": "integer" + }, + "service_dependencies": { + "items": { + "type": "string" + }, + "title": "Service Dependencies", + "type": "array" + }, + "service_dll_file": { + "$ref": "#/$defs/file", + "title": "Service DLL" + }, + "service_error_control": { + "title": "Service Error Control", + "type": "string" + }, + "service_error_control_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Error Control ID", + "type": "integer" + }, + "service_file": { + "$ref": "#/$defs/file", + "title": "Service File" + }, + "service_start_name": { + "title": "Service Start Name", + "type": "string" + }, + "service_start_type": { + "title": "Service Start Type", + "type": "string" + }, + "service_start_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Service Start Type ID", + "type": "integer" + }, + "service_type": { + "title": "Service Type", + "type": "string" + }, + "service_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Type ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Windows Service", + "type": "object" + }, + "group": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "privileges": { + "items": { + "type": "string" + }, + "title": "Privileges", + "type": "array" + }, + "type": { + "title": "Group Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "Group", + "type": "object" + }, + "device": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "region": { + "title": "Region", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "os_machine_uuid": { + "title": "OS Machine UUID", + "type": "string" + }, + "hypervisor": { + "title": "Hypervisor", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "imei_list": { + "items": { + "type": "string" + }, + "title": "IMEI List", + "type": "array" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "is_shared": { + "title": "Shared Device", + "type": "boolean" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "meid": { + "title": "MEID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "is_personal": { + "title": "Personal Device", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "iccid": { + "title": "ICCID", + "type": "string" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "udid": { + "title": "Unique Device Identifier", + "type": "string" + }, + "model": { + "title": "Model", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "boot_time": { + "title": "Boot Time", + "type": "integer" + }, + "is_supervised": { + "title": "Supervised Device", + "type": "boolean" + }, + "is_managed": { + "title": "Managed Device", + "type": "boolean" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "is_trusted": { + "title": "Trusted Device", + "type": "boolean" + }, + "boot_uid": { + "title": "Boot UID", + "type": "string" + }, + "network_interfaces": { + "items": { + "$ref": "#/$defs/network_interface" + }, + "title": "Network Interfaces", + "type": "array" + }, + "autoscale_uid": { + "title": "Autoscale UID", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "subnet": { + "title": "Subnet", + "type": "string" + }, + "eid": { + "title": "EID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "imei": { + "title": "IMEI", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "is_mobile_account_active": { + "title": "Mobile Account Active", + "type": "boolean" + }, + "is_compliant": { + "title": "Compliant Device", + "type": "boolean" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Device", + "type": "object" + }, + "campaign": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Campaign", + "type": "object" + }, + "cwe": { + "additionalProperties": false, + "properties": { + "caption": { + "title": "Caption", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "CWE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CWE", + "type": "object" + }, + "digital_signature": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "developer_uid": { + "title": "Developer UID", + "type": "string" + }, + "digest": { + "$ref": "#/$defs/fingerprint", + "title": "Message Digest" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "State ID", + "type": "integer" + } + }, + "required": [ + "algorithm_id" + ], + "title": "Digital Signature", + "type": "object" + }, + "sbom": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "package": { + "$ref": "#/$defs/package", + "title": "Software Package" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "software_components": { + "items": { + "$ref": "#/$defs/software_component" + }, + "title": "Software Components", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "SBOM ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "package", + "software_components" + ], + "title": "Software Bill of Materials", + "type": "object" + }, + "malware_scan_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "end_time": { + "title": "End Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_files": { + "title": "Scanned Files", + "type": "integer" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "num_volumes": { + "title": "Number of Volumes", + "type": "integer" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Scan UID", + "type": "string" + }, + "unique_malware_count": { + "title": "Unique Malware Count", + "type": "integer" + } + }, + "required": [ + "type_id" + ], + "title": "Malware Scan Info", + "type": "object" + }, + "kill_chain_phase": { + "additionalProperties": false, + "properties": { + "phase": { + "title": "Kill Chain Phase", + "type": "string" + }, + "phase_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Kill Chain Phase ID", + "type": "integer" + } + }, + "required": [ + "phase_id" + ], + "title": "Kill Chain Phase", + "type": "object" + }, + "organization": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "ou_name": { + "title": "Org Unit Name", + "type": "string" + }, + "ou_uid": { + "title": "Org Unit ID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Organization", + "type": "object" + }, + "encryption_details": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Encryption Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Encryption Algorithm ID", + "type": "integer" + }, + "key_length": { + "title": "Encryption Key Length", + "type": "integer" + }, + "key_uid": { + "title": "Key UID", + "type": "string" + }, + "type": { + "title": "Encryption Type", + "type": "string" + } + }, + "title": "Encryption Details", + "type": "object" + }, + "logger": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "log_format": { + "title": "Log Format", + "type": "string" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Logger", + "type": "object" + }, + "feature": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Feature", + "type": "object" + }, + "timespan": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "duration" + ] + }, + { + "required": [ + "duration_days" + ] + }, + { + "required": [ + "duration_hours" + ] + }, + { + "required": [ + "duration_mins" + ] + }, + { + "required": [ + "duration_months" + ] + }, + { + "required": [ + "duration_secs" + ] + }, + { + "required": [ + "duration_weeks" + ] + }, + { + "required": [ + "duration_years" + ] + }, + { + "required": [ + "end_time" + ] + }, + { + "required": [ + "start_time" + ] + } + ], + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "duration_days": { + "title": "Duration Days", + "type": "integer" + }, + "duration_hours": { + "title": "Duration Hours", + "type": "integer" + }, + "duration_mins": { + "title": "Duration Minutes", + "type": "integer" + }, + "duration_months": { + "title": "Duration Months", + "type": "integer" + }, + "duration_secs": { + "title": "Duration Seconds", + "type": "integer" + }, + "duration_weeks": { + "title": "Duration Weeks", + "type": "integer" + }, + "duration_years": { + "title": "Duration Years", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Time Span Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Time Span Type ID", + "type": "integer" + } + }, + "title": "Time Span", + "type": "object" + }, + "rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Rule", + "type": "object" + }, + "software_component": { + "additionalProperties": false, + "properties": { + "author": { + "title": "Author", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "related_component": { + "title": "Related Component", + "type": "string" + }, + "relationship": { + "title": "Relationship", + "type": "string" + }, + "relationship_id": { + "enum": [ + 0, + 1, + 99 + ], + "title": "Relationship ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Component", + "type": "object" + }, + "agent": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Agent Name", + "type": "string" + }, + "policies": { + "items": { + "$ref": "#/$defs/policy" + }, + "title": "Agent Policies", + "type": "array" + }, + "type": { + "title": "Agent Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Agent ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate Agent ID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Agent Version", + "type": "string" + } + }, + "title": "Agent", + "type": "object" + }, + "whois": { + "additionalProperties": false, + "properties": { + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "created_time": { + "title": "Registered At", + "type": "integer" + }, + "dnssec_status": { + "title": "DNSSEC Status", + "type": "string" + }, + "dnssec_status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "DNSSEC Status ID", + "type": "integer" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "domain_contacts": { + "items": { + "$ref": "#/$defs/domain_contact" + }, + "title": "Domain Contacts", + "type": "array" + }, + "email_addr": { + "title": "Registrar Abuse Email Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "last_seen_time": { + "title": "Last Updated At", + "type": "integer" + }, + "name_servers": { + "items": { + "type": "string" + }, + "title": "Name Servers", + "type": "array" + }, + "phone_number": { + "title": "Registrar Abuse Phone Number", + "type": "string" + }, + "registrar": { + "title": "Domain Registrar", + "type": "string" + }, + "status": { + "title": "Domain Status", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Subdomains", + "type": "array" + }, + "subnet": { + "title": "Subnet Block", + "type": "string" + } + }, + "title": "WHOIS", + "type": "object" + }, + "key_value_object": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "value" + ] + }, + { + "required": [ + "values" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + }, + "values": { + "items": { + "type": "string" + }, + "title": "Values", + "type": "array" + } + }, + "required": [ + "name" + ], + "title": "Key:Value object", + "type": "object" + }, + "metadata": { + "additionalProperties": false, + "properties": { + "log_format": { + "title": "Log Source Format", + "type": "string" + }, + "transformation_info_list": { + "items": { + "$ref": "#/$defs/transformation_info" + }, + "title": "Transformation Info", + "type": "array" + }, + "uid": { + "title": "Event UID", + "type": "string" + }, + "original_event_uid": { + "title": "Original Event ID", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "extensions": { + "items": { + "$ref": "#/$defs/extension" + }, + "title": "Schema Extensions", + "type": "array" + }, + "source": { + "title": "Source", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "loggers": { + "items": { + "$ref": "#/$defs/logger" + }, + "title": "Loggers", + "type": "array" + }, + "processed_time": { + "title": "Processed Time", + "type": "integer" + }, + "log_source": { + "title": "Log Source", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "debug": { + "items": { + "type": "string" + }, + "title": "Debug Information", + "type": "array" + }, + "profiles": { + "items": { + "type": "string" + }, + "title": "Profiles", + "type": "array" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "event_code": { + "title": "Event Code", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "reporter": { + "$ref": "#/$defs/reporter", + "title": "Reporter" + }, + "correlation_uid": { + "title": "Correlation UID", + "type": "string" + }, + "extension": { + "$ref": "#/$defs/extension", + "title": "Schema Extension" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "sequence": { + "title": "Sequence Number", + "type": "integer" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "original_time": { + "title": "Original Time", + "type": "string" + }, + "total_queued_duration": { + "$ref": "#/$defs/timespan", + "title": "Total Queued Duration" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "product", + "version" + ], + "title": "Metadata", + "type": "object" + }, + "node": { + "additionalProperties": false, + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Node", + "type": "object" + }, + "product": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "feature": { + "$ref": "#/$defs/feature", + "title": "Feature" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Product", + "type": "object" + }, + "advisory": { + "additionalProperties": false, + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "Related CVEs", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Advisory ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Advisory", + "type": "object" + }, + "network_endpoint": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "title": "Network Endpoint", + "type": "object" + }, + "d3f_tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Tactic", + "type": "object" + }, + "cve": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "cvss": { + "items": { + "$ref": "#/$defs/cvss" + }, + "title": "CVSS Score", + "type": "array" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "cwe_uid": { + "title": "CWE UID", + "type": "string" + }, + "cwe_url": { + "title": "CWE URL", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "epss": { + "$ref": "#/$defs/epss", + "title": "EPSS" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "title": { + "title": "Title", + "type": "string" + }, + "type": { + "title": "Vulnerability Type", + "type": "string" + }, + "uid": { + "title": "CVE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CVE", + "type": "object" + }, + "domain_contact": { + "additionalProperties": false, + "properties": { + "email_addr": { + "title": "Contact Email", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Contact Location Information" + }, + "name": { + "title": "Name", + "type": "string" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "type": { + "title": "Domain Contact Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Domain Contact Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Domain Contact", + "type": "object" + }, + "policy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "type" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "is_applied": { + "title": "Applied", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Policy", + "type": "object" + }, + "span": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "end_time", + "start_time", + "uid" + ], + "title": "Span", + "type": "object" + } + }, + "$id": "https://schema.ocsf.io/schema/classes/api_activity", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "unmapped": { + "$ref": "#/$defs/object", + "title": "Unmapped Data" + }, + "message": { + "title": "Message", + "type": "string" + }, + "http_response": { + "$ref": "#/$defs/http_response", + "title": "HTTP Response" + }, + "category_uid": { + "const": 6, + "title": "Category ID", + "type": "integer" + }, + "api": { + "$ref": "#/$defs/api", + "title": "API Details" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "status_detail": { + "title": "Status Detail", + "type": "string" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "type_name": { + "title": "Type Name", + "type": "string" + }, + "raw_data": { + "title": "Raw Data", + "type": "string" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "actor": { + "$ref": "#/$defs/actor", + "title": "Actor" + }, + "trace": { + "$ref": "#/$defs/trace", + "title": "Trace" + }, + "status": { + "title": "Status", + "type": "string" + }, + "activity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Activity ID", + "type": "integer" + }, + "resources": { + "items": { + "$ref": "#/$defs/resource_details" + }, + "title": "Resources Array", + "type": "array" + }, + "observables": { + "items": { + "$ref": "#/$defs/observable" + }, + "title": "Observables", + "type": "array" + }, + "enrichments": { + "items": { + "$ref": "#/$defs/enrichment" + }, + "title": "Enrichments", + "type": "array" + }, + "http_request": { + "$ref": "#/$defs/http_request", + "title": "HTTP Request" + }, + "raw_data_hash": { + "$ref": "#/$defs/fingerprint", + "title": "Raw Data Hash" + }, + "class_uid": { + "const": 6003, + "title": "Class ID", + "type": "integer" + }, + "activity_name": { + "title": "Activity", + "type": "string" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "class_name": { + "title": "Class", + "type": "string" + }, + "timezone_offset": { + "title": "Timezone Offset", + "type": "integer" + }, + "raw_data_size": { + "title": "Raw Data Size", + "type": "integer" + }, + "dst_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Destination Endpoint" + }, + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "category_name": { + "title": "Category", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "count": { + "title": "Count", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "src_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Source Endpoint" + }, + "metadata": { + "$ref": "#/$defs/metadata", + "title": "Metadata" + } + }, + "required": [ + "activity_id", + "actor", + "api", + "category_uid", + "class_uid", + "metadata", + "severity_id", + "src_endpoint", + "time", + "type_uid" + ], + "title": "API Activity", + "type": "object" +} diff --git a/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@host+security_control+trace.json b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@host+security_control+trace.json new file mode 100644 index 0000000000..1c2e700d12 --- /dev/null +++ b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@host+security_control+trace.json @@ -0,0 +1,8233 @@ +{ + "$defs": { + "kb_article": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "src_url" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "KB Article", + "type": "object" + }, + "long_string": { + "additionalProperties": false, + "properties": { + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "value" + ], + "title": "Long String", + "type": "object" + }, + "enrichment": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "short_desc": { + "title": "Short Description", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "data", + "name", + "value" + ], + "title": "Enrichment", + "type": "object" + }, + "ldap_person": { + "additionalProperties": false, + "properties": { + "cost_center": { + "title": "Cost Center", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "deleted_time": { + "title": "Deleted Time", + "type": "integer" + }, + "department": { + "title": "Department", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "email_addrs": { + "items": { + "type": "string" + }, + "title": "Email Addresses", + "type": "array" + }, + "employee_uid": { + "title": "Employee ID", + "type": "string" + }, + "given_name": { + "title": "Given Name", + "type": "string" + }, + "hire_time": { + "title": "Hire Time", + "type": "integer" + }, + "job_title": { + "title": "Job Title", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "last_login_time": { + "title": "Last Login", + "type": "integer" + }, + "ldap_cn": { + "title": "LDAP Common Name", + "type": "string" + }, + "ldap_dn": { + "title": "LDAP Distinguished Name", + "type": "string" + }, + "leave_time": { + "title": "Leave Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "manager": { + "$ref": "#/$defs/user", + "title": "Manager" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "office_location": { + "title": "Office Location", + "type": "string" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "surname": { + "title": "Surname", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + } + }, + "title": "LDAP Person", + "type": "object" + }, + "email_auth": { + "additionalProperties": false, + "properties": { + "dkim": { + "title": "DKIM Status", + "type": "string" + }, + "dkim_domain": { + "title": "DKIM Domain", + "type": "string" + }, + "dkim_signature": { + "title": "DKIM Signature", + "type": "string" + }, + "dmarc": { + "title": "DMARC Status", + "type": "string" + }, + "dmarc_override": { + "title": "DMARC Override", + "type": "string" + }, + "dmarc_policy": { + "title": "DMARC Policy", + "type": "string" + }, + "spf": { + "title": "SPF Status", + "type": "string" + } + }, + "title": "Email Authentication", + "type": "object" + }, + "epss": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "percentile": { + "title": "EPSS Percentile", + "type": "number" + }, + "score": { + "title": "EPPS Score", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "score" + ], + "title": "EPSS", + "type": "object" + }, + "device_hw_info": { + "additionalProperties": false, + "properties": { + "bios_date": { + "title": "BIOS Date", + "type": "string" + }, + "bios_manufacturer": { + "title": "BIOS Manufacturer", + "type": "string" + }, + "bios_ver": { + "title": "BIOS Version", + "type": "string" + }, + "chassis": { + "title": "Chassis", + "type": "string" + }, + "cpu_architecture": { + "title": "CPU Architecture", + "type": "string" + }, + "cpu_architecture_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "CPU Architecture ID", + "type": "integer" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "cpu_cores": { + "title": "CPU Cores", + "type": "integer" + }, + "cpu_count": { + "title": "CPU Count", + "type": "integer" + }, + "cpu_speed": { + "title": "Processor Speed", + "type": "integer" + }, + "cpu_type": { + "title": "Processor Type", + "type": "string" + }, + "desktop_display": { + "$ref": "#/$defs/display", + "title": "Desktop Display" + }, + "gpu_count": { + "title": "GPU Count", + "type": "integer" + }, + "gpu_info_list": { + "items": { + "$ref": "#/$defs/gpu_info" + }, + "title": "GPU information", + "type": "array" + }, + "keyboard_info": { + "$ref": "#/$defs/keyboard_info", + "title": "Keyboard Information" + }, + "ram_size": { + "title": "RAM Size", + "type": "integer" + }, + "serial_number": { + "title": "Serial Number", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Device Hardware Info", + "type": "object" + }, + "network_proxy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Network Proxy Endpoint", + "type": "object" + }, + "data_classification": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "category_id" + ] + }, + { + "required": [ + "confidentiality_id" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "category_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Category ID", + "type": "integer" + }, + "classifier_details": { + "$ref": "#/$defs/classifier_details", + "title": "Classifier Details" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "discovery_details": { + "items": { + "$ref": "#/$defs/discovery_details" + }, + "title": "Discovery Details", + "type": "array" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "status_details": { + "items": { + "type": "string" + }, + "title": "Status Details", + "type": "array" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Data Classification", + "type": "object" + }, + "http_response": { + "additionalProperties": false, + "properties": { + "body_length": { + "title": "Response Body Length", + "type": "integer" + }, + "code": { + "title": "Response Code", + "type": "integer" + }, + "content_type": { + "title": "HTTP Content Type", + "type": "string" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "latency": { + "title": "Latency", + "type": "integer" + }, + "length": { + "title": "Response Length", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "code" + ], + "title": "HTTP Response", + "type": "object" + }, + "resource_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "criticality": { + "title": "Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Resource Relationship" + }, + "role": { + "title": "Role", + "type": "string" + }, + "role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Role ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Resource Details", + "type": "object" + }, + "display": { + "additionalProperties": false, + "properties": { + "color_depth": { + "title": "Color Depth", + "type": "integer" + }, + "physical_height": { + "title": "Physical Height", + "type": "integer" + }, + "physical_orientation": { + "title": "Physical Orientation", + "type": "integer" + }, + "physical_width": { + "title": "Physical Width", + "type": "integer" + }, + "scale_factor": { + "title": "Scale Factor", + "type": "integer" + } + }, + "title": "Display", + "type": "object" + }, + "scim": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "error_message": { + "title": "Last Error Message", + "type": "string" + }, + "is_group_provisioning_enabled": { + "title": "SCIM Group Provisioning Enabled", + "type": "boolean" + }, + "is_user_provisioning_enabled": { + "title": "SCIM User Provisioning Enabled", + "type": "boolean" + }, + "last_run_time": { + "title": "Last Sync Time", + "type": "integer" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "scim_group_schema": { + "title": "SCIM Group Schema" + }, + "scim_user_schema": { + "title": "SCIM User Schema" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "External ID", + "type": "string" + }, + "url_string": { + "title": "SCIM Endpoint URL", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + }, + "version": { + "title": "SCIM Version", + "type": "string" + } + }, + "title": "SCIM", + "type": "object" + }, + "programmatic_credential": { + "additionalProperties": false, + "properties": { + "last_used_time": { + "title": "Last Used Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Programmatic Credential", + "type": "object" + }, + "os": { + "additionalProperties": false, + "properties": { + "build": { + "title": "OS Build", + "type": "string" + }, + "country": { + "title": "Country", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "edition": { + "title": "OS Edition", + "type": "string" + }, + "kernel_release": { + "title": "Kernel Release", + "type": "string" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "sp_name": { + "title": "OS Service Pack", + "type": "string" + }, + "sp_ver": { + "title": "OS Service Pack Version", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 99, + 100, + 101, + 200, + 201, + 300, + 301, + 302, + 400, + 401, + 402 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "Operating System (OS)", + "type": "object" + }, + "tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Tactic", + "type": "object" + }, + "dns_answer": { + "additionalProperties": false, + "properties": { + "class": { + "title": "Resource Record Class", + "type": "string" + }, + "flag_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "type": "integer" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "packet_uid": { + "title": "Packet UID", + "type": "integer" + }, + "rdata": { + "title": "DNS RData", + "type": "string" + }, + "ttl": { + "title": "TTL", + "type": "integer" + }, + "type": { + "title": "Resource Record Type", + "type": "string" + } + }, + "required": [ + "rdata" + ], + "title": "DNS Answer", + "type": "object" + }, + "discovery_details": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "occurrence_details": { + "$ref": "#/$defs/occurrence_details", + "title": "Occurrence Details" + }, + "occurrences": { + "items": { + "$ref": "#/$defs/occurrence_details" + }, + "title": "Occurrences", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "title": "Discovery Details", + "type": "object" + }, + "sso": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "SAML Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "duration_mins": { + "title": "SSO Session Duration", + "type": "integer" + }, + "idle_timeout": { + "title": "SSO Idle Timeout", + "type": "integer" + }, + "login_endpoint": { + "title": "SSO Login Endpoint", + "type": "string" + }, + "logout_endpoint": { + "title": "SSO Logout Endpoint", + "type": "string" + }, + "metadata_endpoint": { + "title": "SSO Metadata Endpoint", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "title": "Scopes", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + } + }, + "title": "SSO", + "type": "object" + }, + "token": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_renewable": { + "title": "Renewable", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Token", + "type": "object" + }, + "api": { + "additionalProperties": false, + "properties": { + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "request": { + "$ref": "#/$defs/request", + "title": "API Request Details" + }, + "response": { + "$ref": "#/$defs/response", + "title": "API Response Details" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "token": { + "$ref": "#/$defs/token", + "title": "Token" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "operation" + ], + "title": "API", + "type": "object" + }, + "d3fend": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "d3f_tactic" + ] + }, + { + "required": [ + "d3f_technique" + ] + } + ], + "properties": { + "d3f_tactic": { + "$ref": "#/$defs/d3f_tactic", + "title": "MITRE D3FEND\u2122 Tactic" + }, + "d3f_technique": { + "$ref": "#/$defs/d3f_technique", + "title": "MITRE D3FEND\u2122 Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122", + "type": "object" + }, + "reputation": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Reputation Score", + "type": "number" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "score": { + "title": "Reputation Score", + "type": "string" + }, + "score_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "Reputation Score ID", + "type": "integer" + } + }, + "required": [ + "base_score", + "score_id" + ], + "title": "Reputation", + "type": "object" + }, + "mitigation": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "countermeasures": { + "items": { + "$ref": "#/$defs/d3fend" + }, + "title": "Countermeasures", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Mitigation", + "type": "object" + }, + "location": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "city" + ] + }, + { + "required": [ + "country" + ] + }, + { + "required": [ + "postal_code" + ] + }, + { + "required": [ + "region" + ] + } + ], + "properties": { + "aerial_height": { + "title": "Aerial Height", + "type": "string" + }, + "city": { + "title": "City", + "type": "string" + }, + "continent": { + "title": "Continent", + "type": "string" + }, + "coordinates": { + "items": { + "type": "number" + }, + "title": "Coordinates", + "type": "array" + }, + "country": { + "title": "Country", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "geodetic_altitude": { + "title": "Geodetic Altitude", + "type": "string" + }, + "geodetic_vertical_accuracy": { + "title": "Geodetic Vertical Accuracy", + "type": "string" + }, + "geohash": { + "title": "Geohash", + "type": "string" + }, + "horizontal_accuracy": { + "title": "Horizontal Accuracy", + "type": "string" + }, + "is_on_premises": { + "title": "On-Premises", + "type": "boolean" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "lat": { + "title": "Latitude", + "type": "number" + }, + "long": { + "title": "Longitude", + "type": "number" + }, + "postal_code": { + "title": "Postal Code", + "type": "string" + }, + "pressure_altitude": { + "title": "Pressure Altitude", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + } + }, + "title": "Geo Location", + "type": "object" + }, + "idp": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "auth_factors": { + "items": { + "$ref": "#/$defs/auth_factor" + }, + "title": "Authentication Factors", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprint": { + "$ref": "#/$defs/fingerprint", + "title": "Certificate Fingerprint" + }, + "has_mfa": { + "title": "MFA Enforced", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scim": { + "$ref": "#/$defs/scim", + "title": "SCIM" + }, + "sso": { + "$ref": "#/$defs/sso", + "title": "SSO" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "Configuration URL", + "type": "string" + } + }, + "title": "Identity Provider", + "type": "object" + }, + "message_context": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "application" + ] + }, + { + "required": [ + "service" + ] + } + ], + "properties": { + "ai_role": { + "title": "AI Role", + "type": "string" + }, + "ai_role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "AI Role ID", + "type": "integer" + }, + "application": { + "$ref": "#/$defs/application", + "title": "Application" + }, + "completion_tokens": { + "title": "Completion Tokens", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "prompt_tokens": { + "title": "Prompt Tokens", + "type": "integer" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "total_tokens": { + "title": "Total Tokens", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Message Context", + "type": "object" + }, + "process": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ancestry": { + "items": { + "$ref": "#/$defs/process_entity" + }, + "title": "Ancestry", + "type": "array" + }, + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "environment_variables": { + "items": { + "$ref": "#/$defs/environment_variable" + }, + "title": "Environment Variables", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hosted_services": { + "items": { + "$ref": "#/$defs/win_win_service" + }, + "title": "Hosted Services", + "type": "array" + }, + "integrity": { + "title": "Integrity", + "type": "string" + }, + "integrity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Integrity Level", + "type": "integer" + }, + "lineage": { + "items": { + "type": "string" + }, + "title": "Lineage", + "type": "array" + }, + "loaded_modules": { + "items": { + "type": "string" + }, + "title": "Loaded Modules", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_process": { + "$ref": "#/$defs/process", + "title": "Parent Process" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "ptid": { + "title": "Process Thread ID", + "type": "integer" + }, + "sandbox": { + "title": "Sandbox", + "type": "string" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "terminated_time": { + "title": "Terminated Time", + "type": "integer" + }, + "tid": { + "title": "Thread ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + }, + "working_directory": { + "title": "Working Directory", + "type": "string" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + } + }, + "title": "Process", + "type": "object" + }, + "vulnerability": { + "additionalProperties": false, + "oneOf": [ + { + "not": { + "required": [ + "cve", + "cwe" + ] + }, + "required": [ + "advisory" + ] + }, + { + "not": { + "required": [ + "advisory", + "cwe" + ] + }, + "required": [ + "cve" + ] + }, + { + "not": { + "required": [ + "advisory", + "cve" + ] + }, + "required": [ + "cwe" + ] + } + ], + "properties": { + "advisory": { + "$ref": "#/$defs/advisory", + "title": "Security Advisory" + }, + "affected_code": { + "items": { + "$ref": "#/$defs/affected_code" + }, + "title": "Affected Code", + "type": "array" + }, + "affected_packages": { + "items": { + "$ref": "#/$defs/affected_package" + }, + "title": "Affected Software Packages", + "type": "array" + }, + "category": { + "title": "Category", + "type": "string" + }, + "cve": { + "$ref": "#/$defs/cve", + "title": "CVE" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "dependency_chain": { + "title": "Dependency Chain", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "exploit_last_seen_time": { + "title": "Exploit Last Seen Time", + "type": "integer" + }, + "exploit_ref_url": { + "title": "Exploit URL", + "type": "string" + }, + "exploit_requirement": { + "title": "Exploit Requirement", + "type": "string" + }, + "exploit_type": { + "title": "Exploit Type", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "fix_coverage": { + "title": "Fix Coverage", + "type": "string" + }, + "fix_coverage_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Fix Coverage ID", + "type": "integer" + }, + "is_exploit_available": { + "title": "Exploit Availability", + "type": "boolean" + }, + "is_fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "packages": { + "items": { + "$ref": "#/$defs/package" + }, + "title": "Software Packages", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_vulnerabilities": { + "items": { + "type": "string" + }, + "title": "Related Vulnerability IDs", + "type": "array" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Vulnerability Details", + "type": "object" + }, + "affected_code": { + "additionalProperties": false, + "properties": { + "end_column": { + "title": "End Column", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "rule": { + "$ref": "#/$defs/rule", + "title": "Related Rule" + }, + "start_column": { + "title": "Start Column", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "required": [ + "file" + ], + "title": "Affected Code", + "type": "object" + }, + "keyboard_info": { + "additionalProperties": false, + "properties": { + "function_keys": { + "title": "Function Keys", + "type": "integer" + }, + "ime": { + "title": "IME", + "type": "string" + }, + "keyboard_layout": { + "title": "Keyboard Layout", + "type": "string" + }, + "keyboard_subtype": { + "title": "Keyboard Subtype", + "type": "integer" + }, + "keyboard_type": { + "title": "Keyboard Type", + "type": "string" + } + }, + "title": "Keyboard Information", + "type": "object" + }, + "certificate": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "is_self_signed": { + "title": "Certificate Self-Signed", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Distinguished Name", + "type": "string" + }, + "sans": { + "items": { + "$ref": "#/$defs/san" + }, + "title": "Subject Alternative Names", + "type": "array" + }, + "serial_number": { + "title": "Certificate Serial Number", + "type": "string" + }, + "subject": { + "title": "Subject Distinguished Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "issuer", + "serial_number" + ], + "title": "Digital Certificate", + "type": "object" + }, + "graph": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "edges": { + "items": { + "$ref": "#/$defs/edge" + }, + "title": "Edges", + "type": "array" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "nodes": { + "items": { + "$ref": "#/$defs/node" + }, + "title": "Nodes", + "type": "array" + }, + "query_language": { + "title": "Query Language", + "type": "string" + }, + "query_language_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Query Language ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "nodes" + ], + "title": "Graph", + "type": "object" + }, + "account": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "is_disabled": { + "title": "Disabled", + "type": "boolean" + }, + "is_locked": { + "title": "Locked", + "type": "boolean" + }, + "is_on_premises_sync_enabled": { + "title": "On-Premises Sync Enabled", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Account", + "type": "object" + }, + "authorization": { + "additionalProperties": false, + "properties": { + "decision": { + "title": "Authorization Decision/Outcome", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "title": "Authorization Result", + "type": "object" + }, + "autonomous_system": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "number" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "number": { + "title": "Number", + "type": "integer" + } + }, + "title": "Autonomous System", + "type": "object" + }, + "classifier_details": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "Classifier Details", + "type": "object" + }, + "session": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "expiration_reason": { + "title": "Expiration Reason", + "type": "string" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_mfa": { + "title": "Multi Factor Authentication", + "type": "boolean" + }, + "is_remote": { + "title": "Remote", + "type": "boolean" + }, + "is_vpn": { + "title": "VPN Session", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "terminal": { + "title": "Terminal", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + } + }, + "title": "Session", + "type": "object" + }, + "d3f_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Technique", + "type": "object" + }, + "image": { + "additionalProperties": false, + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Image", + "type": "object" + }, + "application": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "criticality": { + "title": "Business Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Application Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Application Name", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Application Relationship" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "sbom": { + "$ref": "#/$defs/sbom", + "title": "Related SBOM" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Application Type", + "type": "string" + }, + "uid": { + "title": "Application ID", + "type": "string" + }, + "uid_alt": { + "title": "Application Alternative ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "version": { + "title": "Application Version", + "type": "string" + } + }, + "title": "Application", + "type": "object" + }, + "script": { + "additionalProperties": false, + "properties": { + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "script_content": { + "$ref": "#/$defs/long_string", + "title": "Script Content" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "script_content", + "type_id" + ], + "title": "Script", + "type": "object" + }, + "affected_package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "fixed_in_version": { + "title": "Fixed In Version", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Affected Software Package", + "type": "object" + }, + "cis_control": { + "additionalProperties": false, + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "CIS Control", + "type": "object" + }, + "analytic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Analytic", + "type": "object" + }, + "edge": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "relation": { + "title": "Relation", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "target": { + "title": "Target", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "source", + "target" + ], + "title": "Edge", + "type": "object" + }, + "actor": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "app_name" + ] + }, + { + "required": [ + "app_uid" + ] + }, + { + "required": [ + "invoked_by" + ] + }, + { + "required": [ + "process" + ] + }, + { + "required": [ + "session" + ] + }, + { + "required": [ + "user" + ] + } + ], + "properties": { + "app_name": { + "title": "Application Name", + "type": "string" + }, + "app_uid": { + "title": "Application ID", + "type": "string" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "idp": { + "$ref": "#/$defs/idp", + "title": "Identity Provider" + }, + "invoked_by": { + "title": "Invoked by", + "type": "string" + }, + "process": { + "$ref": "#/$defs/process", + "title": "Process" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + } + }, + "title": "Actor", + "type": "object" + }, + "cloud": { + "additionalProperties": false, + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "cloud_partition": { + "title": "Cloud Partition", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "project_uid": { + "title": "Project ID", + "type": "string" + }, + "provider": { + "title": "Cloud Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + }, + "zone": { + "title": "Cloud Availability Zone", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "Cloud", + "type": "object" + }, + "trace": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "span": { + "$ref": "#/$defs/span", + "title": "Span" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Trace", + "type": "object" + }, + "ai_model": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ai_provider": { + "title": "AI Provider", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "ai_provider" + ], + "title": "AI Model", + "type": "object" + }, + "file": { + "additionalProperties": false, + "properties": { + "parent_folder": { + "title": "Parent Folder", + "type": "string" + }, + "is_system": { + "title": "System", + "type": "boolean" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "accessor": { + "$ref": "#/$defs/user", + "title": "Accessor" + }, + "is_readonly": { + "title": "Read-Only", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "ext": { + "title": "File Extension", + "type": "string" + }, + "company_name": { + "title": "Company Name", + "type": "string" + }, + "attributes": { + "title": "Attributes", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uri": { + "title": "File URI", + "type": "string" + }, + "is_deleted": { + "title": "Deleted", + "type": "boolean" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "drive_type": { + "title": "Drive Type", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "imported_symbols": { + "items": { + "type": "string" + }, + "title": "Imported Symbols", + "type": "array" + }, + "signature": { + "$ref": "#/$defs/digital_signature", + "title": "Digital Signature" + }, + "is_encrypted": { + "title": "Encrypted", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "accessed_time": { + "title": "Accessed Time", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "internal_name": { + "title": "Internal Name", + "type": "string" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "drive_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Drive Type ID", + "type": "integer" + }, + "security_descriptor": { + "title": "Security Descriptor", + "type": "string" + }, + "volume": { + "title": "Volume", + "type": "string" + }, + "modifier": { + "$ref": "#/$defs/user", + "title": "Modifier" + }, + "mime_type": { + "title": "MIME type", + "type": "string" + }, + "encryption_details": { + "$ref": "#/$defs/encryption_details", + "title": "Encryption Details" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Digital Signatures", + "type": "array" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "File", + "type": "object" + }, + "remediation": { + "additionalProperties": false, + "properties": { + "cis_controls": { + "items": { + "$ref": "#/$defs/cis_control" + }, + "title": "CIS Controls", + "type": "array" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + } + }, + "required": [ + "desc" + ], + "title": "Remediation", + "type": "object" + }, + "email": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "from" + ] + }, + { + "required": [ + "to" + ] + } + ], + "properties": { + "cc": { + "items": { + "type": "string" + }, + "title": "Cc", + "type": "array" + }, + "cc_mailboxes": { + "items": { + "type": "string" + }, + "title": "Cc Mailboxes", + "type": "array" + }, + "delivered_to": { + "title": "Delivered To", + "type": "string" + }, + "delivered_to_list": { + "items": { + "type": "string" + }, + "title": "Delivered To List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "from": { + "title": "From", + "type": "string" + }, + "from_list": { + "items": { + "type": "string" + }, + "title": "From List", + "type": "array" + }, + "from_mailbox": { + "title": "From Mailbox", + "type": "string" + }, + "from_mailboxes": { + "items": { + "type": "string" + }, + "title": "From Mailboxes", + "type": "array" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "is_read": { + "title": "Read", + "type": "boolean" + }, + "message_uid": { + "title": "Message UID", + "type": "string" + }, + "raw_header": { + "title": "Raw Header", + "type": "string" + }, + "reply_to": { + "title": "Reply To", + "type": "string" + }, + "reply_to_list": { + "items": { + "type": "string" + }, + "title": "Reply To List", + "type": "array" + }, + "reply_to_mailboxes": { + "items": { + "type": "string" + }, + "title": "Reply To Mailboxes", + "type": "array" + }, + "return_path": { + "title": "Return Path", + "type": "string" + }, + "sender": { + "title": "Sender", + "type": "string" + }, + "sender_mailbox": { + "title": "Sender Mailbox", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "smtp_from": { + "title": "SMTP From", + "type": "string" + }, + "smtp_to": { + "items": { + "type": "string" + }, + "title": "SMTP To", + "type": "array" + }, + "subject": { + "title": "Subject", + "type": "string" + }, + "to": { + "items": { + "type": "string" + }, + "title": "To", + "type": "array" + }, + "to_mailboxes": { + "items": { + "type": "string" + }, + "title": "To Mailboxes", + "type": "array" + }, + "uid": { + "title": "Email Thread UID", + "type": "string" + }, + "urls": { + "items": { + "$ref": "#/$defs/url" + }, + "title": "URLs", + "type": "array" + }, + "x_originating_ip": { + "items": { + "type": "string" + }, + "title": "X-Originating-IP", + "type": "array" + } + }, + "title": "Email", + "type": "object" + }, + "environment_variable": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Environment Variable", + "type": "object" + }, + "technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Technique", + "type": "object" + }, + "http_request": { + "additionalProperties": false, + "properties": { + "args": { + "title": "HTTP Arguments", + "type": "string" + }, + "body_length": { + "title": "Request Body Length", + "type": "integer" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "http_method": { + "enum": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "title": "HTTP Method", + "type": "string" + }, + "length": { + "title": "Request Length", + "type": "integer" + }, + "referrer": { + "title": "HTTP Referrer", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "user_agent": { + "title": "HTTP User-Agent", + "type": "string" + }, + "version": { + "title": "HTTP Version", + "type": "string" + }, + "x_forwarded_for": { + "items": { + "type": "string" + }, + "title": "X-Forwarded-For", + "type": "array" + } + }, + "title": "HTTP Request", + "type": "object" + }, + "cvss": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Base Score", + "type": "number" + }, + "depth": { + "enum": [ + "Base", + "Environmental", + "Temporal" + ], + "title": "CVSS Depth", + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/$defs/metric" + }, + "title": "Metrics", + "type": "array" + }, + "overall_score": { + "title": "Overall Score", + "type": "number" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "vector_string": { + "title": "Vector String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "base_score", + "version" + ], + "title": "CVSS Score", + "type": "object" + }, + "fingerprint": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "algorithm_id", + "value" + ], + "title": "Fingerprint", + "type": "object" + }, + "process_entity": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "path" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Process Entity", + "type": "object" + }, + "observable": { + "additionalProperties": false, + "properties": { + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Observable", + "type": "object" + }, + "http_header": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "HTTP Header", + "type": "object" + }, + "attack": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "sub_technique" + ] + }, + { + "required": [ + "tactic" + ] + }, + { + "required": [ + "technique" + ] + } + ], + "properties": { + "mitigation": { + "$ref": "#/$defs/mitigation", + "title": "MITRE Mitigation" + }, + "sub_technique": { + "$ref": "#/$defs/sub_technique", + "title": "MITRE Sub-technique" + }, + "tactic": { + "$ref": "#/$defs/tactic", + "title": "MITRE Tactic" + }, + "tactics": { + "items": { + "$ref": "#/$defs/tactic" + }, + "title": "Tactics", + "type": "array" + }, + "technique": { + "$ref": "#/$defs/technique", + "title": "MITRE Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "type": "object" + }, + "network_interface": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "mac" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "open_ports": { + "items": { + "$ref": "#/$defs/port_info" + }, + "title": "Open Ports", + "type": "array" + }, + "subnet_prefix": { + "title": "Subnet Prefix Length", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Network Interface", + "type": "object" + }, + "user": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "account" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "forward_addr": { + "title": "Forwarding Address", + "type": "string" + }, + "full_name": { + "title": "Full Name", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "has_mfa": { + "title": "MFA Assigned", + "type": "boolean" + }, + "ldap_person": { + "$ref": "#/$defs/ldap_person", + "title": "LDAP Person" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "programmatic_credentials": { + "items": { + "$ref": "#/$defs/programmatic_credential" + }, + "title": "Programmatic Credentials", + "type": "array" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "User", + "type": "object" + }, + "auth_factor": { + "additionalProperties": false, + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "factor_type": { + "title": "Factor Type", + "type": "string" + }, + "factor_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Factor Type ID", + "type": "integer" + }, + "is_hotp": { + "title": "HMAC-based One-time Password (HOTP)", + "type": "boolean" + }, + "is_totp": { + "title": "Time-based One-time Password (TOTP)", + "type": "boolean" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "security_questions": { + "items": { + "type": "string" + }, + "title": "Security Questions", + "type": "array" + } + }, + "required": [ + "factor_type_id" + ], + "title": "Authentication Factor", + "type": "object" + }, + "osint": { + "additionalProperties": false, + "properties": { + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "email_auth": { + "$ref": "#/$defs/email_auth", + "title": "Related Email Authentication" + }, + "external_uid": { + "title": "External ID", + "type": "string" + }, + "tlp": { + "enum": [ + "AMBER", + "AMBER STRICT", + "CLEAR", + "GREEN", + "RED", + "WHITE" + ], + "title": "Traffic Light Protocol", + "type": "string" + }, + "detection_pattern": { + "title": "Detection Pattern", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Related Subdomains", + "type": "array" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uploaded_time": { + "title": "Uploaded Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "comment": { + "title": "Analyst Comments", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "value": { + "title": "Indicator", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "intrusion_sets": { + "items": { + "type": "string" + }, + "title": "Intrusion Sets", + "type": "array" + }, + "script": { + "$ref": "#/$defs/script", + "title": "Related Script Data" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "Related File" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "email": { + "$ref": "#/$defs/email", + "title": "Related Email" + }, + "kill_chain": { + "items": { + "$ref": "#/$defs/kill_chain_phase" + }, + "title": "Kill Chain", + "type": "array" + }, + "detection_pattern_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Detection Pattern Type ID", + "type": "integer" + }, + "detection_pattern_type": { + "title": "Detection Pattern", + "type": "string" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "threat_actor": { + "$ref": "#/$defs/threat_actor", + "title": "Threat Actor" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 99 + ], + "title": "Indicator Type ID", + "type": "integer" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "answers": { + "items": { + "$ref": "#/$defs/dns_answer" + }, + "title": "Related DNS Answers", + "type": "array" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "campaign": { + "$ref": "#/$defs/campaign", + "title": "Campaign" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "subnet": { + "title": "Related Subnet", + "type": "string" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Related Digital Signatures", + "type": "array" + }, + "whois": { + "$ref": "#/$defs/whois", + "title": "WHOIS" + }, + "vulnerabilities": { + "items": { + "$ref": "#/$defs/vulnerability" + }, + "title": "Related Vulnerabilities", + "type": "array" + } + }, + "required": [ + "type_id", + "value" + ], + "title": "OSINT", + "type": "object" + }, + "san": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "title": "Subject Alternative Name", + "type": "object" + }, + "occurrence_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cell_name" + ] + }, + { + "required": [ + "column_name" + ] + }, + { + "required": [ + "column_number" + ] + }, + { + "required": [ + "end_line" + ] + }, + { + "required": [ + "json_path" + ] + }, + { + "required": [ + "page_number" + ] + }, + { + "required": [ + "record_index_in_array" + ] + }, + { + "required": [ + "row_number" + ] + }, + { + "required": [ + "start_line" + ] + } + ], + "properties": { + "cell_name": { + "title": "Cell Name", + "type": "string" + }, + "column_name": { + "title": "Column Name", + "type": "string" + }, + "column_number": { + "title": "Column Number", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "json_path": { + "title": "JSON Path", + "type": "string" + }, + "page_number": { + "title": "Page Number", + "type": "integer" + }, + "record_index_in_array": { + "title": "Record Index in Array", + "type": "integer" + }, + "row_number": { + "title": "Row Number", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "title": "Occurrence Details", + "type": "object" + }, + "firewall_rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "condition": { + "title": "Condition", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "match_details": { + "items": { + "type": "string" + }, + "title": "Match Details", + "type": "array" + }, + "match_location": { + "title": "Match Location", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "sensitivity": { + "title": "Sensitivity", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Firewall Rule", + "type": "object" + }, + "threat_actor": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Threat Actor Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Threat Actor Type ID", + "type": "integer" + } + }, + "required": [ + "name" + ], + "title": "Threat Actor", + "type": "object" + }, + "package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Package", + "type": "object" + }, + "transformation_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Transformation Info", + "type": "object" + }, + "response": { + "additionalProperties": false, + "properties": { + "code": { + "title": "Response Code", + "type": "integer" + }, + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "error": { + "title": "Error Code", + "type": "string" + }, + "error_message": { + "title": "Error Message", + "type": "string" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "message": { + "title": "Message", + "type": "string" + } + }, + "title": "Response Elements", + "type": "object" + }, + "reporter": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Reporter", + "type": "object" + }, + "url": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "path" + ] + }, + { + "required": [ + "url_string" + ] + } + ], + "properties": { + "categories": { + "items": { + "type": "string" + }, + "title": "Website Categorization", + "type": "array" + }, + "category_ids": { + "items": { + "enum": [ + 0, + 1, + 3, + 4, + 5, + 6, + 7, + 9, + 11, + 14, + 15, + 16, + 17, + 18, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 40, + 43, + 44, + 45, + 46, + 47, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 63, + 64, + 65, + 66, + 67, + 68, + 71, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 92, + 93, + 95, + 96, + 97, + 98, + 99, + 101, + 102, + 103, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 118, + 121 + ], + "type": "integer" + }, + "title": "Website Categorization IDs", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "query_string": { + "title": "HTTP Query String", + "type": "string" + }, + "resource_type": { + "title": "Resource Type", + "type": "string" + }, + "scheme": { + "title": "Scheme", + "type": "string" + }, + "subdomain": { + "title": "Subdomain", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Uniform Resource Locator", + "type": "object" + }, + "object": { + "additionalProperties": true, + "properties": {}, + "title": "Object", + "type": "object" + }, + "extension": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "version" + ], + "title": "Schema Extension", + "type": "object" + }, + "gpu_info": { + "additionalProperties": false, + "properties": { + "bus_type": { + "title": "Bus Type", + "type": "string" + }, + "bus_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Bus Type ID", + "type": "integer" + }, + "cores": { + "title": "Cores", + "type": "integer" + }, + "model": { + "title": "Model", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "vram_mode": { + "title": "VRAM Mode", + "type": "string" + }, + "vram_mode_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "VRAM Mode ID", + "type": "integer" + }, + "vram_size": { + "title": "VRAM Size", + "type": "integer" + } + }, + "title": "GPU Information", + "type": "object" + }, + "port_info": { + "additionalProperties": false, + "properties": { + "port": { + "title": "Port", + "type": "integer" + }, + "protocol_name": { + "title": "Protocol Name", + "type": "string" + }, + "protocol_num": { + "title": "Protocol Number", + "type": "integer" + } + }, + "required": [ + "port" + ], + "title": "Port Information", + "type": "object" + }, + "service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Service", + "type": "object" + }, + "metric": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Metric", + "type": "object" + }, + "request": { + "additionalProperties": false, + "properties": { + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Request Elements", + "type": "object" + }, + "sub_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Sub-technique", + "type": "object" + }, + "container": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_driver": { + "title": "Network Driver", + "type": "string" + }, + "orchestrator": { + "title": "Orchestrator", + "type": "string" + }, + "pod_uuid": { + "title": "Pod UUID", + "type": "string" + }, + "runtime": { + "title": "Runtime", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Container", + "type": "object" + }, + "malware": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "classification_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 99 + ], + "type": "integer" + }, + "title": "Classification IDs", + "type": "array" + }, + "classifications": { + "items": { + "type": "string" + }, + "title": "Classifications", + "type": "array" + }, + "cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "CVE List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "path": { + "title": "Path", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "classification_ids" + ], + "title": "Malware", + "type": "object" + }, + "win_win_service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "service_category_id" + ] + }, + { + "required": [ + "service_dependencies" + ] + }, + { + "required": [ + "service_error_control_id" + ] + }, + { + "required": [ + "service_start_name" + ] + }, + { + "required": [ + "service_start_type_id" + ] + }, + { + "required": [ + "service_type_id" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "hosting_process": { + "$ref": "#/$defs/process_entity", + "title": "Hosting Process" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "load_order_group": { + "title": "Load Order Group", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "service_category": { + "title": "Service Category", + "type": "string" + }, + "service_category_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Service Category ID", + "type": "integer" + }, + "service_dependencies": { + "items": { + "type": "string" + }, + "title": "Service Dependencies", + "type": "array" + }, + "service_dll_file": { + "$ref": "#/$defs/file", + "title": "Service DLL" + }, + "service_error_control": { + "title": "Service Error Control", + "type": "string" + }, + "service_error_control_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Error Control ID", + "type": "integer" + }, + "service_file": { + "$ref": "#/$defs/file", + "title": "Service File" + }, + "service_start_name": { + "title": "Service Start Name", + "type": "string" + }, + "service_start_type": { + "title": "Service Start Type", + "type": "string" + }, + "service_start_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Service Start Type ID", + "type": "integer" + }, + "service_type": { + "title": "Service Type", + "type": "string" + }, + "service_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Type ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Windows Service", + "type": "object" + }, + "group": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "privileges": { + "items": { + "type": "string" + }, + "title": "Privileges", + "type": "array" + }, + "type": { + "title": "Group Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "Group", + "type": "object" + }, + "device": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "region": { + "title": "Region", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "os_machine_uuid": { + "title": "OS Machine UUID", + "type": "string" + }, + "hypervisor": { + "title": "Hypervisor", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "imei_list": { + "items": { + "type": "string" + }, + "title": "IMEI List", + "type": "array" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "is_shared": { + "title": "Shared Device", + "type": "boolean" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "meid": { + "title": "MEID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "is_personal": { + "title": "Personal Device", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "iccid": { + "title": "ICCID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "udid": { + "title": "Unique Device Identifier", + "type": "string" + }, + "model": { + "title": "Model", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "boot_time": { + "title": "Boot Time", + "type": "integer" + }, + "is_supervised": { + "title": "Supervised Device", + "type": "boolean" + }, + "is_managed": { + "title": "Managed Device", + "type": "boolean" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "is_trusted": { + "title": "Trusted Device", + "type": "boolean" + }, + "boot_uid": { + "title": "Boot UID", + "type": "string" + }, + "network_interfaces": { + "items": { + "$ref": "#/$defs/network_interface" + }, + "title": "Network Interfaces", + "type": "array" + }, + "autoscale_uid": { + "title": "Autoscale UID", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "subnet": { + "title": "Subnet", + "type": "string" + }, + "eid": { + "title": "EID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "imei": { + "title": "IMEI", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "is_mobile_account_active": { + "title": "Mobile Account Active", + "type": "boolean" + }, + "is_compliant": { + "title": "Compliant Device", + "type": "boolean" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Device", + "type": "object" + }, + "campaign": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Campaign", + "type": "object" + }, + "cwe": { + "additionalProperties": false, + "properties": { + "caption": { + "title": "Caption", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "CWE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CWE", + "type": "object" + }, + "digital_signature": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "developer_uid": { + "title": "Developer UID", + "type": "string" + }, + "digest": { + "$ref": "#/$defs/fingerprint", + "title": "Message Digest" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "State ID", + "type": "integer" + } + }, + "required": [ + "algorithm_id" + ], + "title": "Digital Signature", + "type": "object" + }, + "sbom": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "package": { + "$ref": "#/$defs/package", + "title": "Software Package" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "software_components": { + "items": { + "$ref": "#/$defs/software_component" + }, + "title": "Software Components", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "SBOM ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "package", + "software_components" + ], + "title": "Software Bill of Materials", + "type": "object" + }, + "malware_scan_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "end_time": { + "title": "End Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_files": { + "title": "Scanned Files", + "type": "integer" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "num_volumes": { + "title": "Number of Volumes", + "type": "integer" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Scan UID", + "type": "string" + }, + "unique_malware_count": { + "title": "Unique Malware Count", + "type": "integer" + } + }, + "required": [ + "type_id" + ], + "title": "Malware Scan Info", + "type": "object" + }, + "kill_chain_phase": { + "additionalProperties": false, + "properties": { + "phase": { + "title": "Kill Chain Phase", + "type": "string" + }, + "phase_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Kill Chain Phase ID", + "type": "integer" + } + }, + "required": [ + "phase_id" + ], + "title": "Kill Chain Phase", + "type": "object" + }, + "organization": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "ou_name": { + "title": "Org Unit Name", + "type": "string" + }, + "ou_uid": { + "title": "Org Unit ID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Organization", + "type": "object" + }, + "encryption_details": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Encryption Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Encryption Algorithm ID", + "type": "integer" + }, + "key_length": { + "title": "Encryption Key Length", + "type": "integer" + }, + "key_uid": { + "title": "Key UID", + "type": "string" + }, + "type": { + "title": "Encryption Type", + "type": "string" + } + }, + "title": "Encryption Details", + "type": "object" + }, + "logger": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "log_format": { + "title": "Log Format", + "type": "string" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Logger", + "type": "object" + }, + "feature": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Feature", + "type": "object" + }, + "timespan": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "duration" + ] + }, + { + "required": [ + "duration_days" + ] + }, + { + "required": [ + "duration_hours" + ] + }, + { + "required": [ + "duration_mins" + ] + }, + { + "required": [ + "duration_months" + ] + }, + { + "required": [ + "duration_secs" + ] + }, + { + "required": [ + "duration_weeks" + ] + }, + { + "required": [ + "duration_years" + ] + }, + { + "required": [ + "end_time" + ] + }, + { + "required": [ + "start_time" + ] + } + ], + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "duration_days": { + "title": "Duration Days", + "type": "integer" + }, + "duration_hours": { + "title": "Duration Hours", + "type": "integer" + }, + "duration_mins": { + "title": "Duration Minutes", + "type": "integer" + }, + "duration_months": { + "title": "Duration Months", + "type": "integer" + }, + "duration_secs": { + "title": "Duration Seconds", + "type": "integer" + }, + "duration_weeks": { + "title": "Duration Weeks", + "type": "integer" + }, + "duration_years": { + "title": "Duration Years", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Time Span Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Time Span Type ID", + "type": "integer" + } + }, + "title": "Time Span", + "type": "object" + }, + "rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Rule", + "type": "object" + }, + "software_component": { + "additionalProperties": false, + "properties": { + "author": { + "title": "Author", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "related_component": { + "title": "Related Component", + "type": "string" + }, + "relationship": { + "title": "Relationship", + "type": "string" + }, + "relationship_id": { + "enum": [ + 0, + 1, + 99 + ], + "title": "Relationship ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Component", + "type": "object" + }, + "agent": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Agent Name", + "type": "string" + }, + "policies": { + "items": { + "$ref": "#/$defs/policy" + }, + "title": "Agent Policies", + "type": "array" + }, + "type": { + "title": "Agent Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Agent ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate Agent ID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Agent Version", + "type": "string" + } + }, + "title": "Agent", + "type": "object" + }, + "whois": { + "additionalProperties": false, + "properties": { + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "created_time": { + "title": "Registered At", + "type": "integer" + }, + "dnssec_status": { + "title": "DNSSEC Status", + "type": "string" + }, + "dnssec_status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "DNSSEC Status ID", + "type": "integer" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "domain_contacts": { + "items": { + "$ref": "#/$defs/domain_contact" + }, + "title": "Domain Contacts", + "type": "array" + }, + "email_addr": { + "title": "Registrar Abuse Email Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "last_seen_time": { + "title": "Last Updated At", + "type": "integer" + }, + "name_servers": { + "items": { + "type": "string" + }, + "title": "Name Servers", + "type": "array" + }, + "phone_number": { + "title": "Registrar Abuse Phone Number", + "type": "string" + }, + "registrar": { + "title": "Domain Registrar", + "type": "string" + }, + "status": { + "title": "Domain Status", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Subdomains", + "type": "array" + }, + "subnet": { + "title": "Subnet Block", + "type": "string" + } + }, + "title": "WHOIS", + "type": "object" + }, + "key_value_object": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "value" + ] + }, + { + "required": [ + "values" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + }, + "values": { + "items": { + "type": "string" + }, + "title": "Values", + "type": "array" + } + }, + "required": [ + "name" + ], + "title": "Key:Value object", + "type": "object" + }, + "metadata": { + "additionalProperties": false, + "properties": { + "log_format": { + "title": "Log Source Format", + "type": "string" + }, + "transformation_info_list": { + "items": { + "$ref": "#/$defs/transformation_info" + }, + "title": "Transformation Info", + "type": "array" + }, + "uid": { + "title": "Event UID", + "type": "string" + }, + "original_event_uid": { + "title": "Original Event ID", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "extensions": { + "items": { + "$ref": "#/$defs/extension" + }, + "title": "Schema Extensions", + "type": "array" + }, + "source": { + "title": "Source", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "loggers": { + "items": { + "$ref": "#/$defs/logger" + }, + "title": "Loggers", + "type": "array" + }, + "processed_time": { + "title": "Processed Time", + "type": "integer" + }, + "log_source": { + "title": "Log Source", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "debug": { + "items": { + "type": "string" + }, + "title": "Debug Information", + "type": "array" + }, + "profiles": { + "items": { + "type": "string" + }, + "title": "Profiles", + "type": "array" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "event_code": { + "title": "Event Code", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "reporter": { + "$ref": "#/$defs/reporter", + "title": "Reporter" + }, + "correlation_uid": { + "title": "Correlation UID", + "type": "string" + }, + "extension": { + "$ref": "#/$defs/extension", + "title": "Schema Extension" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "sequence": { + "title": "Sequence Number", + "type": "integer" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "original_time": { + "title": "Original Time", + "type": "string" + }, + "total_queued_duration": { + "$ref": "#/$defs/timespan", + "title": "Total Queued Duration" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "product", + "version" + ], + "title": "Metadata", + "type": "object" + }, + "node": { + "additionalProperties": false, + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Node", + "type": "object" + }, + "product": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "feature": { + "$ref": "#/$defs/feature", + "title": "Feature" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Product", + "type": "object" + }, + "advisory": { + "additionalProperties": false, + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "Related CVEs", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Advisory ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Advisory", + "type": "object" + }, + "network_endpoint": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Network Endpoint", + "type": "object" + }, + "d3f_tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Tactic", + "type": "object" + }, + "cve": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "cvss": { + "items": { + "$ref": "#/$defs/cvss" + }, + "title": "CVSS Score", + "type": "array" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "cwe_uid": { + "title": "CWE UID", + "type": "string" + }, + "cwe_url": { + "title": "CWE URL", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "epss": { + "$ref": "#/$defs/epss", + "title": "EPSS" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "title": { + "title": "Title", + "type": "string" + }, + "type": { + "title": "Vulnerability Type", + "type": "string" + }, + "uid": { + "title": "CVE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CVE", + "type": "object" + }, + "domain_contact": { + "additionalProperties": false, + "properties": { + "email_addr": { + "title": "Contact Email", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Contact Location Information" + }, + "name": { + "title": "Name", + "type": "string" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "type": { + "title": "Domain Contact Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Domain Contact Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Domain Contact", + "type": "object" + }, + "policy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "type" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "is_applied": { + "title": "Applied", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Policy", + "type": "object" + }, + "span": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "end_time", + "start_time", + "uid" + ], + "title": "Span", + "type": "object" + } + }, + "$id": "https://schema.ocsf.io/schema/classes/api_activity", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "unmapped": { + "$ref": "#/$defs/object", + "title": "Unmapped Data" + }, + "message": { + "title": "Message", + "type": "string" + }, + "http_response": { + "$ref": "#/$defs/http_response", + "title": "HTTP Response" + }, + "disposition": { + "title": "Disposition", + "type": "string" + }, + "category_uid": { + "const": 6, + "title": "Category ID", + "type": "integer" + }, + "action": { + "title": "Action", + "type": "string" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "api": { + "$ref": "#/$defs/api", + "title": "API Details" + }, + "risk_details": { + "title": "Risk Details", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "status_detail": { + "title": "Status Detail", + "type": "string" + }, + "confidence_score": { + "title": "Confidence Score", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "disposition_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 99 + ], + "title": "Disposition ID", + "type": "integer" + }, + "type_name": { + "title": "Type Name", + "type": "string" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "raw_data": { + "title": "Raw Data", + "type": "string" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "actor": { + "$ref": "#/$defs/actor", + "title": "Actor" + }, + "trace": { + "$ref": "#/$defs/trace", + "title": "Trace" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "status": { + "title": "Status", + "type": "string" + }, + "activity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Activity ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "resources": { + "items": { + "$ref": "#/$defs/resource_details" + }, + "title": "Resources Array", + "type": "array" + }, + "observables": { + "items": { + "$ref": "#/$defs/observable" + }, + "title": "Observables", + "type": "array" + }, + "enrichments": { + "items": { + "$ref": "#/$defs/enrichment" + }, + "title": "Enrichments", + "type": "array" + }, + "http_request": { + "$ref": "#/$defs/http_request", + "title": "HTTP Request" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "raw_data_hash": { + "$ref": "#/$defs/fingerprint", + "title": "Raw Data Hash" + }, + "action_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Action ID", + "type": "integer" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "firewall_rule": { + "$ref": "#/$defs/firewall_rule", + "title": "Firewall Rule" + }, + "class_uid": { + "const": 6003, + "title": "Class ID", + "type": "integer" + }, + "activity_name": { + "title": "Activity", + "type": "string" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "class_name": { + "title": "Class", + "type": "string" + }, + "timezone_offset": { + "title": "Timezone Offset", + "type": "integer" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "raw_data_size": { + "title": "Raw Data Size", + "type": "integer" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "dst_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Destination Endpoint" + }, + "is_alert": { + "title": "Alert", + "type": "boolean" + }, + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "category_name": { + "title": "Category", + "type": "string" + }, + "malware_scan_info": { + "$ref": "#/$defs/malware_scan_info", + "title": "Malware Scan Info" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "count": { + "title": "Count", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "src_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Source Endpoint" + }, + "metadata": { + "$ref": "#/$defs/metadata", + "title": "Metadata" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "required": [ + "activity_id", + "actor", + "api", + "category_uid", + "class_uid", + "metadata", + "severity_id", + "src_endpoint", + "time", + "type_uid" + ], + "title": "API Activity", + "type": "object" +} diff --git a/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@host+trace.json b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@host+trace.json new file mode 100644 index 0000000000..d1315dd7c0 --- /dev/null +++ b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@host+trace.json @@ -0,0 +1,8098 @@ +{ + "$defs": { + "kb_article": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "src_url" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "KB Article", + "type": "object" + }, + "long_string": { + "additionalProperties": false, + "properties": { + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "value" + ], + "title": "Long String", + "type": "object" + }, + "enrichment": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "short_desc": { + "title": "Short Description", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "data", + "name", + "value" + ], + "title": "Enrichment", + "type": "object" + }, + "ldap_person": { + "additionalProperties": false, + "properties": { + "cost_center": { + "title": "Cost Center", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "deleted_time": { + "title": "Deleted Time", + "type": "integer" + }, + "department": { + "title": "Department", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "email_addrs": { + "items": { + "type": "string" + }, + "title": "Email Addresses", + "type": "array" + }, + "employee_uid": { + "title": "Employee ID", + "type": "string" + }, + "given_name": { + "title": "Given Name", + "type": "string" + }, + "hire_time": { + "title": "Hire Time", + "type": "integer" + }, + "job_title": { + "title": "Job Title", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "last_login_time": { + "title": "Last Login", + "type": "integer" + }, + "ldap_cn": { + "title": "LDAP Common Name", + "type": "string" + }, + "ldap_dn": { + "title": "LDAP Distinguished Name", + "type": "string" + }, + "leave_time": { + "title": "Leave Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "manager": { + "$ref": "#/$defs/user", + "title": "Manager" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "office_location": { + "title": "Office Location", + "type": "string" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "surname": { + "title": "Surname", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + } + }, + "title": "LDAP Person", + "type": "object" + }, + "email_auth": { + "additionalProperties": false, + "properties": { + "dkim": { + "title": "DKIM Status", + "type": "string" + }, + "dkim_domain": { + "title": "DKIM Domain", + "type": "string" + }, + "dkim_signature": { + "title": "DKIM Signature", + "type": "string" + }, + "dmarc": { + "title": "DMARC Status", + "type": "string" + }, + "dmarc_override": { + "title": "DMARC Override", + "type": "string" + }, + "dmarc_policy": { + "title": "DMARC Policy", + "type": "string" + }, + "spf": { + "title": "SPF Status", + "type": "string" + } + }, + "title": "Email Authentication", + "type": "object" + }, + "epss": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "percentile": { + "title": "EPSS Percentile", + "type": "number" + }, + "score": { + "title": "EPPS Score", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "score" + ], + "title": "EPSS", + "type": "object" + }, + "device_hw_info": { + "additionalProperties": false, + "properties": { + "bios_date": { + "title": "BIOS Date", + "type": "string" + }, + "bios_manufacturer": { + "title": "BIOS Manufacturer", + "type": "string" + }, + "bios_ver": { + "title": "BIOS Version", + "type": "string" + }, + "chassis": { + "title": "Chassis", + "type": "string" + }, + "cpu_architecture": { + "title": "CPU Architecture", + "type": "string" + }, + "cpu_architecture_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "CPU Architecture ID", + "type": "integer" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "cpu_cores": { + "title": "CPU Cores", + "type": "integer" + }, + "cpu_count": { + "title": "CPU Count", + "type": "integer" + }, + "cpu_speed": { + "title": "Processor Speed", + "type": "integer" + }, + "cpu_type": { + "title": "Processor Type", + "type": "string" + }, + "desktop_display": { + "$ref": "#/$defs/display", + "title": "Desktop Display" + }, + "gpu_count": { + "title": "GPU Count", + "type": "integer" + }, + "gpu_info_list": { + "items": { + "$ref": "#/$defs/gpu_info" + }, + "title": "GPU information", + "type": "array" + }, + "keyboard_info": { + "$ref": "#/$defs/keyboard_info", + "title": "Keyboard Information" + }, + "ram_size": { + "title": "RAM Size", + "type": "integer" + }, + "serial_number": { + "title": "Serial Number", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Device Hardware Info", + "type": "object" + }, + "network_proxy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Network Proxy Endpoint", + "type": "object" + }, + "data_classification": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "category_id" + ] + }, + { + "required": [ + "confidentiality_id" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "category_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Category ID", + "type": "integer" + }, + "classifier_details": { + "$ref": "#/$defs/classifier_details", + "title": "Classifier Details" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "discovery_details": { + "items": { + "$ref": "#/$defs/discovery_details" + }, + "title": "Discovery Details", + "type": "array" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "status_details": { + "items": { + "type": "string" + }, + "title": "Status Details", + "type": "array" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Data Classification", + "type": "object" + }, + "http_response": { + "additionalProperties": false, + "properties": { + "body_length": { + "title": "Response Body Length", + "type": "integer" + }, + "code": { + "title": "Response Code", + "type": "integer" + }, + "content_type": { + "title": "HTTP Content Type", + "type": "string" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "latency": { + "title": "Latency", + "type": "integer" + }, + "length": { + "title": "Response Length", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "code" + ], + "title": "HTTP Response", + "type": "object" + }, + "resource_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "criticality": { + "title": "Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Resource Relationship" + }, + "role": { + "title": "Role", + "type": "string" + }, + "role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Role ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Resource Details", + "type": "object" + }, + "display": { + "additionalProperties": false, + "properties": { + "color_depth": { + "title": "Color Depth", + "type": "integer" + }, + "physical_height": { + "title": "Physical Height", + "type": "integer" + }, + "physical_orientation": { + "title": "Physical Orientation", + "type": "integer" + }, + "physical_width": { + "title": "Physical Width", + "type": "integer" + }, + "scale_factor": { + "title": "Scale Factor", + "type": "integer" + } + }, + "title": "Display", + "type": "object" + }, + "scim": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "error_message": { + "title": "Last Error Message", + "type": "string" + }, + "is_group_provisioning_enabled": { + "title": "SCIM Group Provisioning Enabled", + "type": "boolean" + }, + "is_user_provisioning_enabled": { + "title": "SCIM User Provisioning Enabled", + "type": "boolean" + }, + "last_run_time": { + "title": "Last Sync Time", + "type": "integer" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "scim_group_schema": { + "title": "SCIM Group Schema" + }, + "scim_user_schema": { + "title": "SCIM User Schema" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "External ID", + "type": "string" + }, + "url_string": { + "title": "SCIM Endpoint URL", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + }, + "version": { + "title": "SCIM Version", + "type": "string" + } + }, + "title": "SCIM", + "type": "object" + }, + "programmatic_credential": { + "additionalProperties": false, + "properties": { + "last_used_time": { + "title": "Last Used Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Programmatic Credential", + "type": "object" + }, + "os": { + "additionalProperties": false, + "properties": { + "build": { + "title": "OS Build", + "type": "string" + }, + "country": { + "title": "Country", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "edition": { + "title": "OS Edition", + "type": "string" + }, + "kernel_release": { + "title": "Kernel Release", + "type": "string" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "sp_name": { + "title": "OS Service Pack", + "type": "string" + }, + "sp_ver": { + "title": "OS Service Pack Version", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 99, + 100, + 101, + 200, + 201, + 300, + 301, + 302, + 400, + 401, + 402 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "Operating System (OS)", + "type": "object" + }, + "tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Tactic", + "type": "object" + }, + "dns_answer": { + "additionalProperties": false, + "properties": { + "class": { + "title": "Resource Record Class", + "type": "string" + }, + "flag_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "type": "integer" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "packet_uid": { + "title": "Packet UID", + "type": "integer" + }, + "rdata": { + "title": "DNS RData", + "type": "string" + }, + "ttl": { + "title": "TTL", + "type": "integer" + }, + "type": { + "title": "Resource Record Type", + "type": "string" + } + }, + "required": [ + "rdata" + ], + "title": "DNS Answer", + "type": "object" + }, + "discovery_details": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "occurrence_details": { + "$ref": "#/$defs/occurrence_details", + "title": "Occurrence Details" + }, + "occurrences": { + "items": { + "$ref": "#/$defs/occurrence_details" + }, + "title": "Occurrences", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "title": "Discovery Details", + "type": "object" + }, + "sso": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "SAML Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "duration_mins": { + "title": "SSO Session Duration", + "type": "integer" + }, + "idle_timeout": { + "title": "SSO Idle Timeout", + "type": "integer" + }, + "login_endpoint": { + "title": "SSO Login Endpoint", + "type": "string" + }, + "logout_endpoint": { + "title": "SSO Logout Endpoint", + "type": "string" + }, + "metadata_endpoint": { + "title": "SSO Metadata Endpoint", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "title": "Scopes", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + } + }, + "title": "SSO", + "type": "object" + }, + "token": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_renewable": { + "title": "Renewable", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Token", + "type": "object" + }, + "api": { + "additionalProperties": false, + "properties": { + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "request": { + "$ref": "#/$defs/request", + "title": "API Request Details" + }, + "response": { + "$ref": "#/$defs/response", + "title": "API Response Details" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "token": { + "$ref": "#/$defs/token", + "title": "Token" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "operation" + ], + "title": "API", + "type": "object" + }, + "d3fend": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "d3f_tactic" + ] + }, + { + "required": [ + "d3f_technique" + ] + } + ], + "properties": { + "d3f_tactic": { + "$ref": "#/$defs/d3f_tactic", + "title": "MITRE D3FEND\u2122 Tactic" + }, + "d3f_technique": { + "$ref": "#/$defs/d3f_technique", + "title": "MITRE D3FEND\u2122 Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122", + "type": "object" + }, + "reputation": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Reputation Score", + "type": "number" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "score": { + "title": "Reputation Score", + "type": "string" + }, + "score_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "Reputation Score ID", + "type": "integer" + } + }, + "required": [ + "base_score", + "score_id" + ], + "title": "Reputation", + "type": "object" + }, + "mitigation": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "countermeasures": { + "items": { + "$ref": "#/$defs/d3fend" + }, + "title": "Countermeasures", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Mitigation", + "type": "object" + }, + "location": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "city" + ] + }, + { + "required": [ + "country" + ] + }, + { + "required": [ + "postal_code" + ] + }, + { + "required": [ + "region" + ] + } + ], + "properties": { + "aerial_height": { + "title": "Aerial Height", + "type": "string" + }, + "city": { + "title": "City", + "type": "string" + }, + "continent": { + "title": "Continent", + "type": "string" + }, + "coordinates": { + "items": { + "type": "number" + }, + "title": "Coordinates", + "type": "array" + }, + "country": { + "title": "Country", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "geodetic_altitude": { + "title": "Geodetic Altitude", + "type": "string" + }, + "geodetic_vertical_accuracy": { + "title": "Geodetic Vertical Accuracy", + "type": "string" + }, + "geohash": { + "title": "Geohash", + "type": "string" + }, + "horizontal_accuracy": { + "title": "Horizontal Accuracy", + "type": "string" + }, + "is_on_premises": { + "title": "On-Premises", + "type": "boolean" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "lat": { + "title": "Latitude", + "type": "number" + }, + "long": { + "title": "Longitude", + "type": "number" + }, + "postal_code": { + "title": "Postal Code", + "type": "string" + }, + "pressure_altitude": { + "title": "Pressure Altitude", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + } + }, + "title": "Geo Location", + "type": "object" + }, + "idp": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "auth_factors": { + "items": { + "$ref": "#/$defs/auth_factor" + }, + "title": "Authentication Factors", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprint": { + "$ref": "#/$defs/fingerprint", + "title": "Certificate Fingerprint" + }, + "has_mfa": { + "title": "MFA Enforced", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scim": { + "$ref": "#/$defs/scim", + "title": "SCIM" + }, + "sso": { + "$ref": "#/$defs/sso", + "title": "SSO" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "Configuration URL", + "type": "string" + } + }, + "title": "Identity Provider", + "type": "object" + }, + "message_context": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "application" + ] + }, + { + "required": [ + "service" + ] + } + ], + "properties": { + "ai_role": { + "title": "AI Role", + "type": "string" + }, + "ai_role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "AI Role ID", + "type": "integer" + }, + "application": { + "$ref": "#/$defs/application", + "title": "Application" + }, + "completion_tokens": { + "title": "Completion Tokens", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "prompt_tokens": { + "title": "Prompt Tokens", + "type": "integer" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "total_tokens": { + "title": "Total Tokens", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Message Context", + "type": "object" + }, + "process": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ancestry": { + "items": { + "$ref": "#/$defs/process_entity" + }, + "title": "Ancestry", + "type": "array" + }, + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "environment_variables": { + "items": { + "$ref": "#/$defs/environment_variable" + }, + "title": "Environment Variables", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hosted_services": { + "items": { + "$ref": "#/$defs/win_win_service" + }, + "title": "Hosted Services", + "type": "array" + }, + "integrity": { + "title": "Integrity", + "type": "string" + }, + "integrity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Integrity Level", + "type": "integer" + }, + "lineage": { + "items": { + "type": "string" + }, + "title": "Lineage", + "type": "array" + }, + "loaded_modules": { + "items": { + "type": "string" + }, + "title": "Loaded Modules", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_process": { + "$ref": "#/$defs/process", + "title": "Parent Process" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "ptid": { + "title": "Process Thread ID", + "type": "integer" + }, + "sandbox": { + "title": "Sandbox", + "type": "string" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "terminated_time": { + "title": "Terminated Time", + "type": "integer" + }, + "tid": { + "title": "Thread ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + }, + "working_directory": { + "title": "Working Directory", + "type": "string" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + } + }, + "title": "Process", + "type": "object" + }, + "vulnerability": { + "additionalProperties": false, + "oneOf": [ + { + "not": { + "required": [ + "cve", + "cwe" + ] + }, + "required": [ + "advisory" + ] + }, + { + "not": { + "required": [ + "advisory", + "cwe" + ] + }, + "required": [ + "cve" + ] + }, + { + "not": { + "required": [ + "advisory", + "cve" + ] + }, + "required": [ + "cwe" + ] + } + ], + "properties": { + "advisory": { + "$ref": "#/$defs/advisory", + "title": "Security Advisory" + }, + "affected_code": { + "items": { + "$ref": "#/$defs/affected_code" + }, + "title": "Affected Code", + "type": "array" + }, + "affected_packages": { + "items": { + "$ref": "#/$defs/affected_package" + }, + "title": "Affected Software Packages", + "type": "array" + }, + "category": { + "title": "Category", + "type": "string" + }, + "cve": { + "$ref": "#/$defs/cve", + "title": "CVE" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "dependency_chain": { + "title": "Dependency Chain", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "exploit_last_seen_time": { + "title": "Exploit Last Seen Time", + "type": "integer" + }, + "exploit_ref_url": { + "title": "Exploit URL", + "type": "string" + }, + "exploit_requirement": { + "title": "Exploit Requirement", + "type": "string" + }, + "exploit_type": { + "title": "Exploit Type", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "fix_coverage": { + "title": "Fix Coverage", + "type": "string" + }, + "fix_coverage_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Fix Coverage ID", + "type": "integer" + }, + "is_exploit_available": { + "title": "Exploit Availability", + "type": "boolean" + }, + "is_fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "packages": { + "items": { + "$ref": "#/$defs/package" + }, + "title": "Software Packages", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_vulnerabilities": { + "items": { + "type": "string" + }, + "title": "Related Vulnerability IDs", + "type": "array" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Vulnerability Details", + "type": "object" + }, + "affected_code": { + "additionalProperties": false, + "properties": { + "end_column": { + "title": "End Column", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "rule": { + "$ref": "#/$defs/rule", + "title": "Related Rule" + }, + "start_column": { + "title": "Start Column", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "required": [ + "file" + ], + "title": "Affected Code", + "type": "object" + }, + "keyboard_info": { + "additionalProperties": false, + "properties": { + "function_keys": { + "title": "Function Keys", + "type": "integer" + }, + "ime": { + "title": "IME", + "type": "string" + }, + "keyboard_layout": { + "title": "Keyboard Layout", + "type": "string" + }, + "keyboard_subtype": { + "title": "Keyboard Subtype", + "type": "integer" + }, + "keyboard_type": { + "title": "Keyboard Type", + "type": "string" + } + }, + "title": "Keyboard Information", + "type": "object" + }, + "certificate": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "is_self_signed": { + "title": "Certificate Self-Signed", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Distinguished Name", + "type": "string" + }, + "sans": { + "items": { + "$ref": "#/$defs/san" + }, + "title": "Subject Alternative Names", + "type": "array" + }, + "serial_number": { + "title": "Certificate Serial Number", + "type": "string" + }, + "subject": { + "title": "Subject Distinguished Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "issuer", + "serial_number" + ], + "title": "Digital Certificate", + "type": "object" + }, + "graph": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "edges": { + "items": { + "$ref": "#/$defs/edge" + }, + "title": "Edges", + "type": "array" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "nodes": { + "items": { + "$ref": "#/$defs/node" + }, + "title": "Nodes", + "type": "array" + }, + "query_language": { + "title": "Query Language", + "type": "string" + }, + "query_language_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Query Language ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "nodes" + ], + "title": "Graph", + "type": "object" + }, + "account": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "is_disabled": { + "title": "Disabled", + "type": "boolean" + }, + "is_locked": { + "title": "Locked", + "type": "boolean" + }, + "is_on_premises_sync_enabled": { + "title": "On-Premises Sync Enabled", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Account", + "type": "object" + }, + "authorization": { + "additionalProperties": false, + "properties": { + "decision": { + "title": "Authorization Decision/Outcome", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "title": "Authorization Result", + "type": "object" + }, + "autonomous_system": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "number" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "number": { + "title": "Number", + "type": "integer" + } + }, + "title": "Autonomous System", + "type": "object" + }, + "classifier_details": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "Classifier Details", + "type": "object" + }, + "session": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "expiration_reason": { + "title": "Expiration Reason", + "type": "string" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_mfa": { + "title": "Multi Factor Authentication", + "type": "boolean" + }, + "is_remote": { + "title": "Remote", + "type": "boolean" + }, + "is_vpn": { + "title": "VPN Session", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "terminal": { + "title": "Terminal", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + } + }, + "title": "Session", + "type": "object" + }, + "d3f_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Technique", + "type": "object" + }, + "image": { + "additionalProperties": false, + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Image", + "type": "object" + }, + "application": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "criticality": { + "title": "Business Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Application Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Application Name", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Application Relationship" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "sbom": { + "$ref": "#/$defs/sbom", + "title": "Related SBOM" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Application Type", + "type": "string" + }, + "uid": { + "title": "Application ID", + "type": "string" + }, + "uid_alt": { + "title": "Application Alternative ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "version": { + "title": "Application Version", + "type": "string" + } + }, + "title": "Application", + "type": "object" + }, + "script": { + "additionalProperties": false, + "properties": { + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "script_content": { + "$ref": "#/$defs/long_string", + "title": "Script Content" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "script_content", + "type_id" + ], + "title": "Script", + "type": "object" + }, + "affected_package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "fixed_in_version": { + "title": "Fixed In Version", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Affected Software Package", + "type": "object" + }, + "cis_control": { + "additionalProperties": false, + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "CIS Control", + "type": "object" + }, + "analytic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Analytic", + "type": "object" + }, + "edge": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "relation": { + "title": "Relation", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "target": { + "title": "Target", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "source", + "target" + ], + "title": "Edge", + "type": "object" + }, + "actor": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "app_name" + ] + }, + { + "required": [ + "app_uid" + ] + }, + { + "required": [ + "invoked_by" + ] + }, + { + "required": [ + "process" + ] + }, + { + "required": [ + "session" + ] + }, + { + "required": [ + "user" + ] + } + ], + "properties": { + "app_name": { + "title": "Application Name", + "type": "string" + }, + "app_uid": { + "title": "Application ID", + "type": "string" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "idp": { + "$ref": "#/$defs/idp", + "title": "Identity Provider" + }, + "invoked_by": { + "title": "Invoked by", + "type": "string" + }, + "process": { + "$ref": "#/$defs/process", + "title": "Process" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + } + }, + "title": "Actor", + "type": "object" + }, + "cloud": { + "additionalProperties": false, + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "cloud_partition": { + "title": "Cloud Partition", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "project_uid": { + "title": "Project ID", + "type": "string" + }, + "provider": { + "title": "Cloud Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + }, + "zone": { + "title": "Cloud Availability Zone", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "Cloud", + "type": "object" + }, + "trace": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "span": { + "$ref": "#/$defs/span", + "title": "Span" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Trace", + "type": "object" + }, + "ai_model": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ai_provider": { + "title": "AI Provider", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "ai_provider" + ], + "title": "AI Model", + "type": "object" + }, + "file": { + "additionalProperties": false, + "properties": { + "parent_folder": { + "title": "Parent Folder", + "type": "string" + }, + "is_system": { + "title": "System", + "type": "boolean" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "accessor": { + "$ref": "#/$defs/user", + "title": "Accessor" + }, + "is_readonly": { + "title": "Read-Only", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "ext": { + "title": "File Extension", + "type": "string" + }, + "company_name": { + "title": "Company Name", + "type": "string" + }, + "attributes": { + "title": "Attributes", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uri": { + "title": "File URI", + "type": "string" + }, + "is_deleted": { + "title": "Deleted", + "type": "boolean" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "drive_type": { + "title": "Drive Type", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "imported_symbols": { + "items": { + "type": "string" + }, + "title": "Imported Symbols", + "type": "array" + }, + "signature": { + "$ref": "#/$defs/digital_signature", + "title": "Digital Signature" + }, + "is_encrypted": { + "title": "Encrypted", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "accessed_time": { + "title": "Accessed Time", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "internal_name": { + "title": "Internal Name", + "type": "string" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "drive_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Drive Type ID", + "type": "integer" + }, + "security_descriptor": { + "title": "Security Descriptor", + "type": "string" + }, + "volume": { + "title": "Volume", + "type": "string" + }, + "modifier": { + "$ref": "#/$defs/user", + "title": "Modifier" + }, + "mime_type": { + "title": "MIME type", + "type": "string" + }, + "encryption_details": { + "$ref": "#/$defs/encryption_details", + "title": "Encryption Details" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Digital Signatures", + "type": "array" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "File", + "type": "object" + }, + "remediation": { + "additionalProperties": false, + "properties": { + "cis_controls": { + "items": { + "$ref": "#/$defs/cis_control" + }, + "title": "CIS Controls", + "type": "array" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + } + }, + "required": [ + "desc" + ], + "title": "Remediation", + "type": "object" + }, + "email": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "from" + ] + }, + { + "required": [ + "to" + ] + } + ], + "properties": { + "cc": { + "items": { + "type": "string" + }, + "title": "Cc", + "type": "array" + }, + "cc_mailboxes": { + "items": { + "type": "string" + }, + "title": "Cc Mailboxes", + "type": "array" + }, + "delivered_to": { + "title": "Delivered To", + "type": "string" + }, + "delivered_to_list": { + "items": { + "type": "string" + }, + "title": "Delivered To List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "from": { + "title": "From", + "type": "string" + }, + "from_list": { + "items": { + "type": "string" + }, + "title": "From List", + "type": "array" + }, + "from_mailbox": { + "title": "From Mailbox", + "type": "string" + }, + "from_mailboxes": { + "items": { + "type": "string" + }, + "title": "From Mailboxes", + "type": "array" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "is_read": { + "title": "Read", + "type": "boolean" + }, + "message_uid": { + "title": "Message UID", + "type": "string" + }, + "raw_header": { + "title": "Raw Header", + "type": "string" + }, + "reply_to": { + "title": "Reply To", + "type": "string" + }, + "reply_to_list": { + "items": { + "type": "string" + }, + "title": "Reply To List", + "type": "array" + }, + "reply_to_mailboxes": { + "items": { + "type": "string" + }, + "title": "Reply To Mailboxes", + "type": "array" + }, + "return_path": { + "title": "Return Path", + "type": "string" + }, + "sender": { + "title": "Sender", + "type": "string" + }, + "sender_mailbox": { + "title": "Sender Mailbox", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "smtp_from": { + "title": "SMTP From", + "type": "string" + }, + "smtp_to": { + "items": { + "type": "string" + }, + "title": "SMTP To", + "type": "array" + }, + "subject": { + "title": "Subject", + "type": "string" + }, + "to": { + "items": { + "type": "string" + }, + "title": "To", + "type": "array" + }, + "to_mailboxes": { + "items": { + "type": "string" + }, + "title": "To Mailboxes", + "type": "array" + }, + "uid": { + "title": "Email Thread UID", + "type": "string" + }, + "urls": { + "items": { + "$ref": "#/$defs/url" + }, + "title": "URLs", + "type": "array" + }, + "x_originating_ip": { + "items": { + "type": "string" + }, + "title": "X-Originating-IP", + "type": "array" + } + }, + "title": "Email", + "type": "object" + }, + "environment_variable": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Environment Variable", + "type": "object" + }, + "technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Technique", + "type": "object" + }, + "http_request": { + "additionalProperties": false, + "properties": { + "args": { + "title": "HTTP Arguments", + "type": "string" + }, + "body_length": { + "title": "Request Body Length", + "type": "integer" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "http_method": { + "enum": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "title": "HTTP Method", + "type": "string" + }, + "length": { + "title": "Request Length", + "type": "integer" + }, + "referrer": { + "title": "HTTP Referrer", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "user_agent": { + "title": "HTTP User-Agent", + "type": "string" + }, + "version": { + "title": "HTTP Version", + "type": "string" + }, + "x_forwarded_for": { + "items": { + "type": "string" + }, + "title": "X-Forwarded-For", + "type": "array" + } + }, + "title": "HTTP Request", + "type": "object" + }, + "cvss": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Base Score", + "type": "number" + }, + "depth": { + "enum": [ + "Base", + "Environmental", + "Temporal" + ], + "title": "CVSS Depth", + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/$defs/metric" + }, + "title": "Metrics", + "type": "array" + }, + "overall_score": { + "title": "Overall Score", + "type": "number" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "vector_string": { + "title": "Vector String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "base_score", + "version" + ], + "title": "CVSS Score", + "type": "object" + }, + "fingerprint": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "algorithm_id", + "value" + ], + "title": "Fingerprint", + "type": "object" + }, + "process_entity": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "path" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Process Entity", + "type": "object" + }, + "observable": { + "additionalProperties": false, + "properties": { + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Observable", + "type": "object" + }, + "http_header": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "HTTP Header", + "type": "object" + }, + "attack": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "sub_technique" + ] + }, + { + "required": [ + "tactic" + ] + }, + { + "required": [ + "technique" + ] + } + ], + "properties": { + "mitigation": { + "$ref": "#/$defs/mitigation", + "title": "MITRE Mitigation" + }, + "sub_technique": { + "$ref": "#/$defs/sub_technique", + "title": "MITRE Sub-technique" + }, + "tactic": { + "$ref": "#/$defs/tactic", + "title": "MITRE Tactic" + }, + "tactics": { + "items": { + "$ref": "#/$defs/tactic" + }, + "title": "Tactics", + "type": "array" + }, + "technique": { + "$ref": "#/$defs/technique", + "title": "MITRE Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "type": "object" + }, + "network_interface": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "mac" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "open_ports": { + "items": { + "$ref": "#/$defs/port_info" + }, + "title": "Open Ports", + "type": "array" + }, + "subnet_prefix": { + "title": "Subnet Prefix Length", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Network Interface", + "type": "object" + }, + "user": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "account" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "forward_addr": { + "title": "Forwarding Address", + "type": "string" + }, + "full_name": { + "title": "Full Name", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "has_mfa": { + "title": "MFA Assigned", + "type": "boolean" + }, + "ldap_person": { + "$ref": "#/$defs/ldap_person", + "title": "LDAP Person" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "programmatic_credentials": { + "items": { + "$ref": "#/$defs/programmatic_credential" + }, + "title": "Programmatic Credentials", + "type": "array" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "User", + "type": "object" + }, + "auth_factor": { + "additionalProperties": false, + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "factor_type": { + "title": "Factor Type", + "type": "string" + }, + "factor_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Factor Type ID", + "type": "integer" + }, + "is_hotp": { + "title": "HMAC-based One-time Password (HOTP)", + "type": "boolean" + }, + "is_totp": { + "title": "Time-based One-time Password (TOTP)", + "type": "boolean" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "security_questions": { + "items": { + "type": "string" + }, + "title": "Security Questions", + "type": "array" + } + }, + "required": [ + "factor_type_id" + ], + "title": "Authentication Factor", + "type": "object" + }, + "osint": { + "additionalProperties": false, + "properties": { + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "email_auth": { + "$ref": "#/$defs/email_auth", + "title": "Related Email Authentication" + }, + "external_uid": { + "title": "External ID", + "type": "string" + }, + "tlp": { + "enum": [ + "AMBER", + "AMBER STRICT", + "CLEAR", + "GREEN", + "RED", + "WHITE" + ], + "title": "Traffic Light Protocol", + "type": "string" + }, + "detection_pattern": { + "title": "Detection Pattern", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Related Subdomains", + "type": "array" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uploaded_time": { + "title": "Uploaded Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "comment": { + "title": "Analyst Comments", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "value": { + "title": "Indicator", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "intrusion_sets": { + "items": { + "type": "string" + }, + "title": "Intrusion Sets", + "type": "array" + }, + "script": { + "$ref": "#/$defs/script", + "title": "Related Script Data" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "Related File" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "email": { + "$ref": "#/$defs/email", + "title": "Related Email" + }, + "kill_chain": { + "items": { + "$ref": "#/$defs/kill_chain_phase" + }, + "title": "Kill Chain", + "type": "array" + }, + "detection_pattern_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Detection Pattern Type ID", + "type": "integer" + }, + "detection_pattern_type": { + "title": "Detection Pattern", + "type": "string" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "threat_actor": { + "$ref": "#/$defs/threat_actor", + "title": "Threat Actor" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 99 + ], + "title": "Indicator Type ID", + "type": "integer" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "answers": { + "items": { + "$ref": "#/$defs/dns_answer" + }, + "title": "Related DNS Answers", + "type": "array" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "campaign": { + "$ref": "#/$defs/campaign", + "title": "Campaign" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "subnet": { + "title": "Related Subnet", + "type": "string" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Related Digital Signatures", + "type": "array" + }, + "whois": { + "$ref": "#/$defs/whois", + "title": "WHOIS" + }, + "vulnerabilities": { + "items": { + "$ref": "#/$defs/vulnerability" + }, + "title": "Related Vulnerabilities", + "type": "array" + } + }, + "required": [ + "type_id", + "value" + ], + "title": "OSINT", + "type": "object" + }, + "san": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "title": "Subject Alternative Name", + "type": "object" + }, + "occurrence_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cell_name" + ] + }, + { + "required": [ + "column_name" + ] + }, + { + "required": [ + "column_number" + ] + }, + { + "required": [ + "end_line" + ] + }, + { + "required": [ + "json_path" + ] + }, + { + "required": [ + "page_number" + ] + }, + { + "required": [ + "record_index_in_array" + ] + }, + { + "required": [ + "row_number" + ] + }, + { + "required": [ + "start_line" + ] + } + ], + "properties": { + "cell_name": { + "title": "Cell Name", + "type": "string" + }, + "column_name": { + "title": "Column Name", + "type": "string" + }, + "column_number": { + "title": "Column Number", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "json_path": { + "title": "JSON Path", + "type": "string" + }, + "page_number": { + "title": "Page Number", + "type": "integer" + }, + "record_index_in_array": { + "title": "Record Index in Array", + "type": "integer" + }, + "row_number": { + "title": "Row Number", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "title": "Occurrence Details", + "type": "object" + }, + "firewall_rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "condition": { + "title": "Condition", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "match_details": { + "items": { + "type": "string" + }, + "title": "Match Details", + "type": "array" + }, + "match_location": { + "title": "Match Location", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "sensitivity": { + "title": "Sensitivity", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Firewall Rule", + "type": "object" + }, + "threat_actor": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Threat Actor Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Threat Actor Type ID", + "type": "integer" + } + }, + "required": [ + "name" + ], + "title": "Threat Actor", + "type": "object" + }, + "package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Package", + "type": "object" + }, + "transformation_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Transformation Info", + "type": "object" + }, + "response": { + "additionalProperties": false, + "properties": { + "code": { + "title": "Response Code", + "type": "integer" + }, + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "error": { + "title": "Error Code", + "type": "string" + }, + "error_message": { + "title": "Error Message", + "type": "string" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "message": { + "title": "Message", + "type": "string" + } + }, + "title": "Response Elements", + "type": "object" + }, + "reporter": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Reporter", + "type": "object" + }, + "url": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "path" + ] + }, + { + "required": [ + "url_string" + ] + } + ], + "properties": { + "categories": { + "items": { + "type": "string" + }, + "title": "Website Categorization", + "type": "array" + }, + "category_ids": { + "items": { + "enum": [ + 0, + 1, + 3, + 4, + 5, + 6, + 7, + 9, + 11, + 14, + 15, + 16, + 17, + 18, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 40, + 43, + 44, + 45, + 46, + 47, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 63, + 64, + 65, + 66, + 67, + 68, + 71, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 92, + 93, + 95, + 96, + 97, + 98, + 99, + 101, + 102, + 103, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 118, + 121 + ], + "type": "integer" + }, + "title": "Website Categorization IDs", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "query_string": { + "title": "HTTP Query String", + "type": "string" + }, + "resource_type": { + "title": "Resource Type", + "type": "string" + }, + "scheme": { + "title": "Scheme", + "type": "string" + }, + "subdomain": { + "title": "Subdomain", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Uniform Resource Locator", + "type": "object" + }, + "object": { + "additionalProperties": true, + "properties": {}, + "title": "Object", + "type": "object" + }, + "extension": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "version" + ], + "title": "Schema Extension", + "type": "object" + }, + "gpu_info": { + "additionalProperties": false, + "properties": { + "bus_type": { + "title": "Bus Type", + "type": "string" + }, + "bus_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Bus Type ID", + "type": "integer" + }, + "cores": { + "title": "Cores", + "type": "integer" + }, + "model": { + "title": "Model", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "vram_mode": { + "title": "VRAM Mode", + "type": "string" + }, + "vram_mode_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "VRAM Mode ID", + "type": "integer" + }, + "vram_size": { + "title": "VRAM Size", + "type": "integer" + } + }, + "title": "GPU Information", + "type": "object" + }, + "port_info": { + "additionalProperties": false, + "properties": { + "port": { + "title": "Port", + "type": "integer" + }, + "protocol_name": { + "title": "Protocol Name", + "type": "string" + }, + "protocol_num": { + "title": "Protocol Number", + "type": "integer" + } + }, + "required": [ + "port" + ], + "title": "Port Information", + "type": "object" + }, + "service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Service", + "type": "object" + }, + "metric": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Metric", + "type": "object" + }, + "request": { + "additionalProperties": false, + "properties": { + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Request Elements", + "type": "object" + }, + "sub_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Sub-technique", + "type": "object" + }, + "container": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_driver": { + "title": "Network Driver", + "type": "string" + }, + "orchestrator": { + "title": "Orchestrator", + "type": "string" + }, + "pod_uuid": { + "title": "Pod UUID", + "type": "string" + }, + "runtime": { + "title": "Runtime", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Container", + "type": "object" + }, + "malware": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "classification_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 99 + ], + "type": "integer" + }, + "title": "Classification IDs", + "type": "array" + }, + "classifications": { + "items": { + "type": "string" + }, + "title": "Classifications", + "type": "array" + }, + "cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "CVE List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "path": { + "title": "Path", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "classification_ids" + ], + "title": "Malware", + "type": "object" + }, + "win_win_service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "service_category_id" + ] + }, + { + "required": [ + "service_dependencies" + ] + }, + { + "required": [ + "service_error_control_id" + ] + }, + { + "required": [ + "service_start_name" + ] + }, + { + "required": [ + "service_start_type_id" + ] + }, + { + "required": [ + "service_type_id" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "hosting_process": { + "$ref": "#/$defs/process_entity", + "title": "Hosting Process" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "load_order_group": { + "title": "Load Order Group", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "service_category": { + "title": "Service Category", + "type": "string" + }, + "service_category_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Service Category ID", + "type": "integer" + }, + "service_dependencies": { + "items": { + "type": "string" + }, + "title": "Service Dependencies", + "type": "array" + }, + "service_dll_file": { + "$ref": "#/$defs/file", + "title": "Service DLL" + }, + "service_error_control": { + "title": "Service Error Control", + "type": "string" + }, + "service_error_control_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Error Control ID", + "type": "integer" + }, + "service_file": { + "$ref": "#/$defs/file", + "title": "Service File" + }, + "service_start_name": { + "title": "Service Start Name", + "type": "string" + }, + "service_start_type": { + "title": "Service Start Type", + "type": "string" + }, + "service_start_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Service Start Type ID", + "type": "integer" + }, + "service_type": { + "title": "Service Type", + "type": "string" + }, + "service_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Type ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Windows Service", + "type": "object" + }, + "group": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "privileges": { + "items": { + "type": "string" + }, + "title": "Privileges", + "type": "array" + }, + "type": { + "title": "Group Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "Group", + "type": "object" + }, + "device": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "region": { + "title": "Region", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "os_machine_uuid": { + "title": "OS Machine UUID", + "type": "string" + }, + "hypervisor": { + "title": "Hypervisor", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "imei_list": { + "items": { + "type": "string" + }, + "title": "IMEI List", + "type": "array" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "is_shared": { + "title": "Shared Device", + "type": "boolean" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "meid": { + "title": "MEID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "is_personal": { + "title": "Personal Device", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "iccid": { + "title": "ICCID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "udid": { + "title": "Unique Device Identifier", + "type": "string" + }, + "model": { + "title": "Model", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "boot_time": { + "title": "Boot Time", + "type": "integer" + }, + "is_supervised": { + "title": "Supervised Device", + "type": "boolean" + }, + "is_managed": { + "title": "Managed Device", + "type": "boolean" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "is_trusted": { + "title": "Trusted Device", + "type": "boolean" + }, + "boot_uid": { + "title": "Boot UID", + "type": "string" + }, + "network_interfaces": { + "items": { + "$ref": "#/$defs/network_interface" + }, + "title": "Network Interfaces", + "type": "array" + }, + "autoscale_uid": { + "title": "Autoscale UID", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "subnet": { + "title": "Subnet", + "type": "string" + }, + "eid": { + "title": "EID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "imei": { + "title": "IMEI", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "is_mobile_account_active": { + "title": "Mobile Account Active", + "type": "boolean" + }, + "is_compliant": { + "title": "Compliant Device", + "type": "boolean" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Device", + "type": "object" + }, + "campaign": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Campaign", + "type": "object" + }, + "cwe": { + "additionalProperties": false, + "properties": { + "caption": { + "title": "Caption", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "CWE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CWE", + "type": "object" + }, + "digital_signature": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "developer_uid": { + "title": "Developer UID", + "type": "string" + }, + "digest": { + "$ref": "#/$defs/fingerprint", + "title": "Message Digest" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "State ID", + "type": "integer" + } + }, + "required": [ + "algorithm_id" + ], + "title": "Digital Signature", + "type": "object" + }, + "sbom": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "package": { + "$ref": "#/$defs/package", + "title": "Software Package" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "software_components": { + "items": { + "$ref": "#/$defs/software_component" + }, + "title": "Software Components", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "SBOM ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "package", + "software_components" + ], + "title": "Software Bill of Materials", + "type": "object" + }, + "malware_scan_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "end_time": { + "title": "End Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_files": { + "title": "Scanned Files", + "type": "integer" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "num_volumes": { + "title": "Number of Volumes", + "type": "integer" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Scan UID", + "type": "string" + }, + "unique_malware_count": { + "title": "Unique Malware Count", + "type": "integer" + } + }, + "required": [ + "type_id" + ], + "title": "Malware Scan Info", + "type": "object" + }, + "kill_chain_phase": { + "additionalProperties": false, + "properties": { + "phase": { + "title": "Kill Chain Phase", + "type": "string" + }, + "phase_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Kill Chain Phase ID", + "type": "integer" + } + }, + "required": [ + "phase_id" + ], + "title": "Kill Chain Phase", + "type": "object" + }, + "organization": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "ou_name": { + "title": "Org Unit Name", + "type": "string" + }, + "ou_uid": { + "title": "Org Unit ID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Organization", + "type": "object" + }, + "encryption_details": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Encryption Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Encryption Algorithm ID", + "type": "integer" + }, + "key_length": { + "title": "Encryption Key Length", + "type": "integer" + }, + "key_uid": { + "title": "Key UID", + "type": "string" + }, + "type": { + "title": "Encryption Type", + "type": "string" + } + }, + "title": "Encryption Details", + "type": "object" + }, + "logger": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "log_format": { + "title": "Log Format", + "type": "string" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Logger", + "type": "object" + }, + "feature": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Feature", + "type": "object" + }, + "timespan": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "duration" + ] + }, + { + "required": [ + "duration_days" + ] + }, + { + "required": [ + "duration_hours" + ] + }, + { + "required": [ + "duration_mins" + ] + }, + { + "required": [ + "duration_months" + ] + }, + { + "required": [ + "duration_secs" + ] + }, + { + "required": [ + "duration_weeks" + ] + }, + { + "required": [ + "duration_years" + ] + }, + { + "required": [ + "end_time" + ] + }, + { + "required": [ + "start_time" + ] + } + ], + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "duration_days": { + "title": "Duration Days", + "type": "integer" + }, + "duration_hours": { + "title": "Duration Hours", + "type": "integer" + }, + "duration_mins": { + "title": "Duration Minutes", + "type": "integer" + }, + "duration_months": { + "title": "Duration Months", + "type": "integer" + }, + "duration_secs": { + "title": "Duration Seconds", + "type": "integer" + }, + "duration_weeks": { + "title": "Duration Weeks", + "type": "integer" + }, + "duration_years": { + "title": "Duration Years", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Time Span Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Time Span Type ID", + "type": "integer" + } + }, + "title": "Time Span", + "type": "object" + }, + "rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Rule", + "type": "object" + }, + "software_component": { + "additionalProperties": false, + "properties": { + "author": { + "title": "Author", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "related_component": { + "title": "Related Component", + "type": "string" + }, + "relationship": { + "title": "Relationship", + "type": "string" + }, + "relationship_id": { + "enum": [ + 0, + 1, + 99 + ], + "title": "Relationship ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Component", + "type": "object" + }, + "agent": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Agent Name", + "type": "string" + }, + "policies": { + "items": { + "$ref": "#/$defs/policy" + }, + "title": "Agent Policies", + "type": "array" + }, + "type": { + "title": "Agent Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Agent ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate Agent ID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Agent Version", + "type": "string" + } + }, + "title": "Agent", + "type": "object" + }, + "whois": { + "additionalProperties": false, + "properties": { + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "created_time": { + "title": "Registered At", + "type": "integer" + }, + "dnssec_status": { + "title": "DNSSEC Status", + "type": "string" + }, + "dnssec_status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "DNSSEC Status ID", + "type": "integer" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "domain_contacts": { + "items": { + "$ref": "#/$defs/domain_contact" + }, + "title": "Domain Contacts", + "type": "array" + }, + "email_addr": { + "title": "Registrar Abuse Email Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "last_seen_time": { + "title": "Last Updated At", + "type": "integer" + }, + "name_servers": { + "items": { + "type": "string" + }, + "title": "Name Servers", + "type": "array" + }, + "phone_number": { + "title": "Registrar Abuse Phone Number", + "type": "string" + }, + "registrar": { + "title": "Domain Registrar", + "type": "string" + }, + "status": { + "title": "Domain Status", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Subdomains", + "type": "array" + }, + "subnet": { + "title": "Subnet Block", + "type": "string" + } + }, + "title": "WHOIS", + "type": "object" + }, + "key_value_object": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "value" + ] + }, + { + "required": [ + "values" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + }, + "values": { + "items": { + "type": "string" + }, + "title": "Values", + "type": "array" + } + }, + "required": [ + "name" + ], + "title": "Key:Value object", + "type": "object" + }, + "metadata": { + "additionalProperties": false, + "properties": { + "log_format": { + "title": "Log Source Format", + "type": "string" + }, + "transformation_info_list": { + "items": { + "$ref": "#/$defs/transformation_info" + }, + "title": "Transformation Info", + "type": "array" + }, + "uid": { + "title": "Event UID", + "type": "string" + }, + "original_event_uid": { + "title": "Original Event ID", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "extensions": { + "items": { + "$ref": "#/$defs/extension" + }, + "title": "Schema Extensions", + "type": "array" + }, + "source": { + "title": "Source", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "loggers": { + "items": { + "$ref": "#/$defs/logger" + }, + "title": "Loggers", + "type": "array" + }, + "processed_time": { + "title": "Processed Time", + "type": "integer" + }, + "log_source": { + "title": "Log Source", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "debug": { + "items": { + "type": "string" + }, + "title": "Debug Information", + "type": "array" + }, + "profiles": { + "items": { + "type": "string" + }, + "title": "Profiles", + "type": "array" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "event_code": { + "title": "Event Code", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "reporter": { + "$ref": "#/$defs/reporter", + "title": "Reporter" + }, + "correlation_uid": { + "title": "Correlation UID", + "type": "string" + }, + "extension": { + "$ref": "#/$defs/extension", + "title": "Schema Extension" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "sequence": { + "title": "Sequence Number", + "type": "integer" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "original_time": { + "title": "Original Time", + "type": "string" + }, + "total_queued_duration": { + "$ref": "#/$defs/timespan", + "title": "Total Queued Duration" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "product", + "version" + ], + "title": "Metadata", + "type": "object" + }, + "node": { + "additionalProperties": false, + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Node", + "type": "object" + }, + "product": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "feature": { + "$ref": "#/$defs/feature", + "title": "Feature" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Product", + "type": "object" + }, + "advisory": { + "additionalProperties": false, + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "Related CVEs", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Advisory ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Advisory", + "type": "object" + }, + "network_endpoint": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Network Endpoint", + "type": "object" + }, + "d3f_tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Tactic", + "type": "object" + }, + "cve": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "cvss": { + "items": { + "$ref": "#/$defs/cvss" + }, + "title": "CVSS Score", + "type": "array" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "cwe_uid": { + "title": "CWE UID", + "type": "string" + }, + "cwe_url": { + "title": "CWE URL", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "epss": { + "$ref": "#/$defs/epss", + "title": "EPSS" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "title": { + "title": "Title", + "type": "string" + }, + "type": { + "title": "Vulnerability Type", + "type": "string" + }, + "uid": { + "title": "CVE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CVE", + "type": "object" + }, + "domain_contact": { + "additionalProperties": false, + "properties": { + "email_addr": { + "title": "Contact Email", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Contact Location Information" + }, + "name": { + "title": "Name", + "type": "string" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "type": { + "title": "Domain Contact Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Domain Contact Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Domain Contact", + "type": "object" + }, + "policy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "type" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "is_applied": { + "title": "Applied", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Policy", + "type": "object" + }, + "span": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "end_time", + "start_time", + "uid" + ], + "title": "Span", + "type": "object" + } + }, + "$id": "https://schema.ocsf.io/schema/classes/api_activity", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "unmapped": { + "$ref": "#/$defs/object", + "title": "Unmapped Data" + }, + "message": { + "title": "Message", + "type": "string" + }, + "http_response": { + "$ref": "#/$defs/http_response", + "title": "HTTP Response" + }, + "category_uid": { + "const": 6, + "title": "Category ID", + "type": "integer" + }, + "api": { + "$ref": "#/$defs/api", + "title": "API Details" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "status_detail": { + "title": "Status Detail", + "type": "string" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "type_name": { + "title": "Type Name", + "type": "string" + }, + "raw_data": { + "title": "Raw Data", + "type": "string" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "actor": { + "$ref": "#/$defs/actor", + "title": "Actor" + }, + "trace": { + "$ref": "#/$defs/trace", + "title": "Trace" + }, + "status": { + "title": "Status", + "type": "string" + }, + "activity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Activity ID", + "type": "integer" + }, + "resources": { + "items": { + "$ref": "#/$defs/resource_details" + }, + "title": "Resources Array", + "type": "array" + }, + "observables": { + "items": { + "$ref": "#/$defs/observable" + }, + "title": "Observables", + "type": "array" + }, + "enrichments": { + "items": { + "$ref": "#/$defs/enrichment" + }, + "title": "Enrichments", + "type": "array" + }, + "http_request": { + "$ref": "#/$defs/http_request", + "title": "HTTP Request" + }, + "raw_data_hash": { + "$ref": "#/$defs/fingerprint", + "title": "Raw Data Hash" + }, + "class_uid": { + "const": 6003, + "title": "Class ID", + "type": "integer" + }, + "activity_name": { + "title": "Activity", + "type": "string" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "class_name": { + "title": "Class", + "type": "string" + }, + "timezone_offset": { + "title": "Timezone Offset", + "type": "integer" + }, + "raw_data_size": { + "title": "Raw Data Size", + "type": "integer" + }, + "dst_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Destination Endpoint" + }, + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "category_name": { + "title": "Category", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "count": { + "title": "Count", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "src_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Source Endpoint" + }, + "metadata": { + "$ref": "#/$defs/metadata", + "title": "Metadata" + } + }, + "required": [ + "activity_id", + "actor", + "api", + "category_uid", + "class_uid", + "metadata", + "severity_id", + "src_endpoint", + "time", + "type_uid" + ], + "title": "API Activity", + "type": "object" +} diff --git a/crates/openshell-ocsf-validate/schemas/v1.8.0/http_activity@container+host+network_proxy+security_control+trace.json b/crates/openshell-ocsf-validate/schemas/v1.8.0/http_activity@container+host+network_proxy+security_control+trace.json new file mode 100644 index 0000000000..654712b727 --- /dev/null +++ b/crates/openshell-ocsf-validate/schemas/v1.8.0/http_activity@container+host+network_proxy+security_control+trace.json @@ -0,0 +1,8326 @@ +{ + "$defs": { + "kb_article": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "src_url" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "KB Article", + "type": "object" + }, + "long_string": { + "additionalProperties": false, + "properties": { + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "value" + ], + "title": "Long String", + "type": "object" + }, + "enrichment": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "short_desc": { + "title": "Short Description", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "data", + "name", + "value" + ], + "title": "Enrichment", + "type": "object" + }, + "ldap_person": { + "additionalProperties": false, + "properties": { + "cost_center": { + "title": "Cost Center", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "deleted_time": { + "title": "Deleted Time", + "type": "integer" + }, + "department": { + "title": "Department", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "email_addrs": { + "items": { + "type": "string" + }, + "title": "Email Addresses", + "type": "array" + }, + "employee_uid": { + "title": "Employee ID", + "type": "string" + }, + "given_name": { + "title": "Given Name", + "type": "string" + }, + "hire_time": { + "title": "Hire Time", + "type": "integer" + }, + "job_title": { + "title": "Job Title", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "last_login_time": { + "title": "Last Login", + "type": "integer" + }, + "ldap_cn": { + "title": "LDAP Common Name", + "type": "string" + }, + "ldap_dn": { + "title": "LDAP Distinguished Name", + "type": "string" + }, + "leave_time": { + "title": "Leave Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "manager": { + "$ref": "#/$defs/user", + "title": "Manager" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "office_location": { + "title": "Office Location", + "type": "string" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "surname": { + "title": "Surname", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + } + }, + "title": "LDAP Person", + "type": "object" + }, + "email_auth": { + "additionalProperties": false, + "properties": { + "dkim": { + "title": "DKIM Status", + "type": "string" + }, + "dkim_domain": { + "title": "DKIM Domain", + "type": "string" + }, + "dkim_signature": { + "title": "DKIM Signature", + "type": "string" + }, + "dmarc": { + "title": "DMARC Status", + "type": "string" + }, + "dmarc_override": { + "title": "DMARC Override", + "type": "string" + }, + "dmarc_policy": { + "title": "DMARC Policy", + "type": "string" + }, + "spf": { + "title": "SPF Status", + "type": "string" + } + }, + "title": "Email Authentication", + "type": "object" + }, + "epss": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "percentile": { + "title": "EPSS Percentile", + "type": "number" + }, + "score": { + "title": "EPPS Score", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "score" + ], + "title": "EPSS", + "type": "object" + }, + "endpoint_connection": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "code" + ] + }, + { + "required": [ + "network_endpoint" + ] + } + ], + "properties": { + "code": { + "title": "Response Code", + "type": "integer" + }, + "network_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Network Endpoint" + } + }, + "title": "Endpoint Connection", + "type": "object" + }, + "device_hw_info": { + "additionalProperties": false, + "properties": { + "bios_date": { + "title": "BIOS Date", + "type": "string" + }, + "bios_manufacturer": { + "title": "BIOS Manufacturer", + "type": "string" + }, + "bios_ver": { + "title": "BIOS Version", + "type": "string" + }, + "chassis": { + "title": "Chassis", + "type": "string" + }, + "cpu_architecture": { + "title": "CPU Architecture", + "type": "string" + }, + "cpu_architecture_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "CPU Architecture ID", + "type": "integer" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "cpu_cores": { + "title": "CPU Cores", + "type": "integer" + }, + "cpu_count": { + "title": "CPU Count", + "type": "integer" + }, + "cpu_speed": { + "title": "Processor Speed", + "type": "integer" + }, + "cpu_type": { + "title": "Processor Type", + "type": "string" + }, + "desktop_display": { + "$ref": "#/$defs/display", + "title": "Desktop Display" + }, + "gpu_count": { + "title": "GPU Count", + "type": "integer" + }, + "gpu_info_list": { + "items": { + "$ref": "#/$defs/gpu_info" + }, + "title": "GPU information", + "type": "array" + }, + "keyboard_info": { + "$ref": "#/$defs/keyboard_info", + "title": "Keyboard Information" + }, + "ram_size": { + "title": "RAM Size", + "type": "integer" + }, + "serial_number": { + "title": "Serial Number", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Device Hardware Info", + "type": "object" + }, + "network_proxy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "title": "Network Proxy Endpoint", + "type": "object" + }, + "data_classification": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "category_id" + ] + }, + { + "required": [ + "confidentiality_id" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "category_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Category ID", + "type": "integer" + }, + "classifier_details": { + "$ref": "#/$defs/classifier_details", + "title": "Classifier Details" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "discovery_details": { + "items": { + "$ref": "#/$defs/discovery_details" + }, + "title": "Discovery Details", + "type": "array" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "status_details": { + "items": { + "type": "string" + }, + "title": "Status Details", + "type": "array" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Data Classification", + "type": "object" + }, + "http_response": { + "additionalProperties": false, + "properties": { + "body_length": { + "title": "Response Body Length", + "type": "integer" + }, + "code": { + "title": "Response Code", + "type": "integer" + }, + "content_type": { + "title": "HTTP Content Type", + "type": "string" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "latency": { + "title": "Latency", + "type": "integer" + }, + "length": { + "title": "Response Length", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "code" + ], + "title": "HTTP Response", + "type": "object" + }, + "display": { + "additionalProperties": false, + "properties": { + "color_depth": { + "title": "Color Depth", + "type": "integer" + }, + "physical_height": { + "title": "Physical Height", + "type": "integer" + }, + "physical_orientation": { + "title": "Physical Orientation", + "type": "integer" + }, + "physical_width": { + "title": "Physical Width", + "type": "integer" + }, + "scale_factor": { + "title": "Scale Factor", + "type": "integer" + } + }, + "title": "Display", + "type": "object" + }, + "scim": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "error_message": { + "title": "Last Error Message", + "type": "string" + }, + "is_group_provisioning_enabled": { + "title": "SCIM Group Provisioning Enabled", + "type": "boolean" + }, + "is_user_provisioning_enabled": { + "title": "SCIM User Provisioning Enabled", + "type": "boolean" + }, + "last_run_time": { + "title": "Last Sync Time", + "type": "integer" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "scim_group_schema": { + "title": "SCIM Group Schema" + }, + "scim_user_schema": { + "title": "SCIM User Schema" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "External ID", + "type": "string" + }, + "url_string": { + "title": "SCIM Endpoint URL", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + }, + "version": { + "title": "SCIM Version", + "type": "string" + } + }, + "title": "SCIM", + "type": "object" + }, + "programmatic_credential": { + "additionalProperties": false, + "properties": { + "last_used_time": { + "title": "Last Used Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Programmatic Credential", + "type": "object" + }, + "os": { + "additionalProperties": false, + "properties": { + "build": { + "title": "OS Build", + "type": "string" + }, + "country": { + "title": "Country", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "edition": { + "title": "OS Edition", + "type": "string" + }, + "kernel_release": { + "title": "Kernel Release", + "type": "string" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "sp_name": { + "title": "OS Service Pack", + "type": "string" + }, + "sp_ver": { + "title": "OS Service Pack Version", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 99, + 100, + 101, + 200, + 201, + 300, + 301, + 302, + 400, + 401, + 402 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "Operating System (OS)", + "type": "object" + }, + "tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Tactic", + "type": "object" + }, + "dns_answer": { + "additionalProperties": false, + "properties": { + "class": { + "title": "Resource Record Class", + "type": "string" + }, + "flag_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "type": "integer" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "packet_uid": { + "title": "Packet UID", + "type": "integer" + }, + "rdata": { + "title": "DNS RData", + "type": "string" + }, + "ttl": { + "title": "TTL", + "type": "integer" + }, + "type": { + "title": "Resource Record Type", + "type": "string" + } + }, + "required": [ + "rdata" + ], + "title": "DNS Answer", + "type": "object" + }, + "discovery_details": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "occurrence_details": { + "$ref": "#/$defs/occurrence_details", + "title": "Occurrence Details" + }, + "occurrences": { + "items": { + "$ref": "#/$defs/occurrence_details" + }, + "title": "Occurrences", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "title": "Discovery Details", + "type": "object" + }, + "sso": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "SAML Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "duration_mins": { + "title": "SSO Session Duration", + "type": "integer" + }, + "idle_timeout": { + "title": "SSO Idle Timeout", + "type": "integer" + }, + "login_endpoint": { + "title": "SSO Login Endpoint", + "type": "string" + }, + "logout_endpoint": { + "title": "SSO Logout Endpoint", + "type": "string" + }, + "metadata_endpoint": { + "title": "SSO Metadata Endpoint", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "title": "Scopes", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + } + }, + "title": "SSO", + "type": "object" + }, + "token": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_renewable": { + "title": "Renewable", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Token", + "type": "object" + }, + "api": { + "additionalProperties": false, + "properties": { + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "request": { + "$ref": "#/$defs/request", + "title": "API Request Details" + }, + "response": { + "$ref": "#/$defs/response", + "title": "API Response Details" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "token": { + "$ref": "#/$defs/token", + "title": "Token" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "operation" + ], + "title": "API", + "type": "object" + }, + "d3fend": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "d3f_tactic" + ] + }, + { + "required": [ + "d3f_technique" + ] + } + ], + "properties": { + "d3f_tactic": { + "$ref": "#/$defs/d3f_tactic", + "title": "MITRE D3FEND\u2122 Tactic" + }, + "d3f_technique": { + "$ref": "#/$defs/d3f_technique", + "title": "MITRE D3FEND\u2122 Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122", + "type": "object" + }, + "reputation": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Reputation Score", + "type": "number" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "score": { + "title": "Reputation Score", + "type": "string" + }, + "score_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "Reputation Score ID", + "type": "integer" + } + }, + "required": [ + "base_score", + "score_id" + ], + "title": "Reputation", + "type": "object" + }, + "mitigation": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "countermeasures": { + "items": { + "$ref": "#/$defs/d3fend" + }, + "title": "Countermeasures", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Mitigation", + "type": "object" + }, + "location": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "city" + ] + }, + { + "required": [ + "country" + ] + }, + { + "required": [ + "postal_code" + ] + }, + { + "required": [ + "region" + ] + } + ], + "properties": { + "aerial_height": { + "title": "Aerial Height", + "type": "string" + }, + "city": { + "title": "City", + "type": "string" + }, + "continent": { + "title": "Continent", + "type": "string" + }, + "coordinates": { + "items": { + "type": "number" + }, + "title": "Coordinates", + "type": "array" + }, + "country": { + "title": "Country", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "geodetic_altitude": { + "title": "Geodetic Altitude", + "type": "string" + }, + "geodetic_vertical_accuracy": { + "title": "Geodetic Vertical Accuracy", + "type": "string" + }, + "geohash": { + "title": "Geohash", + "type": "string" + }, + "horizontal_accuracy": { + "title": "Horizontal Accuracy", + "type": "string" + }, + "is_on_premises": { + "title": "On-Premises", + "type": "boolean" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "lat": { + "title": "Latitude", + "type": "number" + }, + "long": { + "title": "Longitude", + "type": "number" + }, + "postal_code": { + "title": "Postal Code", + "type": "string" + }, + "pressure_altitude": { + "title": "Pressure Altitude", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + } + }, + "title": "Geo Location", + "type": "object" + }, + "network_traffic": { + "additionalProperties": false, + "properties": { + "bytes": { + "title": "Total Bytes", + "type": "integer" + }, + "bytes_in": { + "title": "Bytes In", + "type": "integer" + }, + "bytes_missed": { + "title": "Bytes Missed", + "type": "integer" + }, + "bytes_out": { + "title": "Bytes Out", + "type": "integer" + }, + "chunks": { + "title": "Chunks", + "type": "integer" + }, + "chunks_in": { + "title": "Chunks In", + "type": "integer" + }, + "chunks_out": { + "title": "Chunks Out", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "packets": { + "title": "Total Packets", + "type": "integer" + }, + "packets_in": { + "title": "Packets In", + "type": "integer" + }, + "packets_out": { + "title": "Packets Out", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "timespan": { + "$ref": "#/$defs/timespan", + "title": "Time Span" + } + }, + "title": "Network Traffic", + "type": "object" + }, + "idp": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "auth_factors": { + "items": { + "$ref": "#/$defs/auth_factor" + }, + "title": "Authentication Factors", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprint": { + "$ref": "#/$defs/fingerprint", + "title": "Certificate Fingerprint" + }, + "has_mfa": { + "title": "MFA Enforced", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scim": { + "$ref": "#/$defs/scim", + "title": "SCIM" + }, + "sso": { + "$ref": "#/$defs/sso", + "title": "SSO" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "Configuration URL", + "type": "string" + } + }, + "title": "Identity Provider", + "type": "object" + }, + "process": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ancestry": { + "items": { + "$ref": "#/$defs/process_entity" + }, + "title": "Ancestry", + "type": "array" + }, + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "environment_variables": { + "items": { + "$ref": "#/$defs/environment_variable" + }, + "title": "Environment Variables", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hosted_services": { + "items": { + "$ref": "#/$defs/win_win_service" + }, + "title": "Hosted Services", + "type": "array" + }, + "integrity": { + "title": "Integrity", + "type": "string" + }, + "integrity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Integrity Level", + "type": "integer" + }, + "lineage": { + "items": { + "type": "string" + }, + "title": "Lineage", + "type": "array" + }, + "loaded_modules": { + "items": { + "type": "string" + }, + "title": "Loaded Modules", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "parent_process": { + "$ref": "#/$defs/process", + "title": "Parent Process" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "ptid": { + "title": "Process Thread ID", + "type": "integer" + }, + "sandbox": { + "title": "Sandbox", + "type": "string" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "terminated_time": { + "title": "Terminated Time", + "type": "integer" + }, + "tid": { + "title": "Thread ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + }, + "working_directory": { + "title": "Working Directory", + "type": "string" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + } + }, + "title": "Process", + "type": "object" + }, + "vulnerability": { + "additionalProperties": false, + "oneOf": [ + { + "not": { + "required": [ + "cve", + "cwe" + ] + }, + "required": [ + "advisory" + ] + }, + { + "not": { + "required": [ + "advisory", + "cwe" + ] + }, + "required": [ + "cve" + ] + }, + { + "not": { + "required": [ + "advisory", + "cve" + ] + }, + "required": [ + "cwe" + ] + } + ], + "properties": { + "advisory": { + "$ref": "#/$defs/advisory", + "title": "Security Advisory" + }, + "affected_code": { + "items": { + "$ref": "#/$defs/affected_code" + }, + "title": "Affected Code", + "type": "array" + }, + "affected_packages": { + "items": { + "$ref": "#/$defs/affected_package" + }, + "title": "Affected Software Packages", + "type": "array" + }, + "category": { + "title": "Category", + "type": "string" + }, + "cve": { + "$ref": "#/$defs/cve", + "title": "CVE" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "dependency_chain": { + "title": "Dependency Chain", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "exploit_last_seen_time": { + "title": "Exploit Last Seen Time", + "type": "integer" + }, + "exploit_ref_url": { + "title": "Exploit URL", + "type": "string" + }, + "exploit_requirement": { + "title": "Exploit Requirement", + "type": "string" + }, + "exploit_type": { + "title": "Exploit Type", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "fix_coverage": { + "title": "Fix Coverage", + "type": "string" + }, + "fix_coverage_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Fix Coverage ID", + "type": "integer" + }, + "is_exploit_available": { + "title": "Exploit Availability", + "type": "boolean" + }, + "is_fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "packages": { + "items": { + "$ref": "#/$defs/package" + }, + "title": "Software Packages", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_vulnerabilities": { + "items": { + "type": "string" + }, + "title": "Related Vulnerability IDs", + "type": "array" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Vulnerability Details", + "type": "object" + }, + "affected_code": { + "additionalProperties": false, + "properties": { + "end_column": { + "title": "End Column", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "rule": { + "$ref": "#/$defs/rule", + "title": "Related Rule" + }, + "start_column": { + "title": "Start Column", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "required": [ + "file" + ], + "title": "Affected Code", + "type": "object" + }, + "keyboard_info": { + "additionalProperties": false, + "properties": { + "function_keys": { + "title": "Function Keys", + "type": "integer" + }, + "ime": { + "title": "IME", + "type": "string" + }, + "keyboard_layout": { + "title": "Keyboard Layout", + "type": "string" + }, + "keyboard_subtype": { + "title": "Keyboard Subtype", + "type": "integer" + }, + "keyboard_type": { + "title": "Keyboard Type", + "type": "string" + } + }, + "title": "Keyboard Information", + "type": "object" + }, + "certificate": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "is_self_signed": { + "title": "Certificate Self-Signed", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Distinguished Name", + "type": "string" + }, + "sans": { + "items": { + "$ref": "#/$defs/san" + }, + "title": "Subject Alternative Names", + "type": "array" + }, + "serial_number": { + "title": "Certificate Serial Number", + "type": "string" + }, + "subject": { + "title": "Subject Distinguished Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "issuer", + "serial_number" + ], + "title": "Digital Certificate", + "type": "object" + }, + "account": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "is_disabled": { + "title": "Disabled", + "type": "boolean" + }, + "is_locked": { + "title": "Locked", + "type": "boolean" + }, + "is_on_premises_sync_enabled": { + "title": "On-Premises Sync Enabled", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Account", + "type": "object" + }, + "authorization": { + "additionalProperties": false, + "properties": { + "decision": { + "title": "Authorization Decision/Outcome", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "title": "Authorization Result", + "type": "object" + }, + "autonomous_system": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "number" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "number": { + "title": "Number", + "type": "integer" + } + }, + "title": "Autonomous System", + "type": "object" + }, + "classifier_details": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "Classifier Details", + "type": "object" + }, + "session": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "expiration_reason": { + "title": "Expiration Reason", + "type": "string" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_mfa": { + "title": "Multi Factor Authentication", + "type": "boolean" + }, + "is_remote": { + "title": "Remote", + "type": "boolean" + }, + "is_vpn": { + "title": "VPN Session", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "terminal": { + "title": "Terminal", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + } + }, + "title": "Session", + "type": "object" + }, + "d3f_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Technique", + "type": "object" + }, + "image": { + "additionalProperties": false, + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Image", + "type": "object" + }, + "script": { + "additionalProperties": false, + "properties": { + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "script_content": { + "$ref": "#/$defs/long_string", + "title": "Script Content" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "script_content", + "type_id" + ], + "title": "Script", + "type": "object" + }, + "affected_package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "fixed_in_version": { + "title": "Fixed In Version", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Affected Software Package", + "type": "object" + }, + "cis_control": { + "additionalProperties": false, + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "CIS Control", + "type": "object" + }, + "analytic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Analytic", + "type": "object" + }, + "actor": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "app_name" + ] + }, + { + "required": [ + "app_uid" + ] + }, + { + "required": [ + "invoked_by" + ] + }, + { + "required": [ + "process" + ] + }, + { + "required": [ + "session" + ] + }, + { + "required": [ + "user" + ] + } + ], + "properties": { + "app_name": { + "title": "Application Name", + "type": "string" + }, + "app_uid": { + "title": "Application ID", + "type": "string" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "idp": { + "$ref": "#/$defs/idp", + "title": "Identity Provider" + }, + "invoked_by": { + "title": "Invoked by", + "type": "string" + }, + "process": { + "$ref": "#/$defs/process", + "title": "Process" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + } + }, + "title": "Actor", + "type": "object" + }, + "cloud": { + "additionalProperties": false, + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "cloud_partition": { + "title": "Cloud Partition", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "project_uid": { + "title": "Project ID", + "type": "string" + }, + "provider": { + "title": "Cloud Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + }, + "zone": { + "title": "Cloud Availability Zone", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "Cloud", + "type": "object" + }, + "trace": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "span": { + "$ref": "#/$defs/span", + "title": "Span" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Trace", + "type": "object" + }, + "file": { + "additionalProperties": false, + "properties": { + "parent_folder": { + "title": "Parent Folder", + "type": "string" + }, + "is_system": { + "title": "System", + "type": "boolean" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "accessor": { + "$ref": "#/$defs/user", + "title": "Accessor" + }, + "is_readonly": { + "title": "Read-Only", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "ext": { + "title": "File Extension", + "type": "string" + }, + "company_name": { + "title": "Company Name", + "type": "string" + }, + "attributes": { + "title": "Attributes", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uri": { + "title": "File URI", + "type": "string" + }, + "is_deleted": { + "title": "Deleted", + "type": "boolean" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "drive_type": { + "title": "Drive Type", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "imported_symbols": { + "items": { + "type": "string" + }, + "title": "Imported Symbols", + "type": "array" + }, + "signature": { + "$ref": "#/$defs/digital_signature", + "title": "Digital Signature" + }, + "is_encrypted": { + "title": "Encrypted", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "accessed_time": { + "title": "Accessed Time", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "internal_name": { + "title": "Internal Name", + "type": "string" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "drive_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Drive Type ID", + "type": "integer" + }, + "security_descriptor": { + "title": "Security Descriptor", + "type": "string" + }, + "volume": { + "title": "Volume", + "type": "string" + }, + "modifier": { + "$ref": "#/$defs/user", + "title": "Modifier" + }, + "mime_type": { + "title": "MIME type", + "type": "string" + }, + "encryption_details": { + "$ref": "#/$defs/encryption_details", + "title": "Encryption Details" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Digital Signatures", + "type": "array" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "File", + "type": "object" + }, + "remediation": { + "additionalProperties": false, + "properties": { + "cis_controls": { + "items": { + "$ref": "#/$defs/cis_control" + }, + "title": "CIS Controls", + "type": "array" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + } + }, + "required": [ + "desc" + ], + "title": "Remediation", + "type": "object" + }, + "email": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "from" + ] + }, + { + "required": [ + "to" + ] + } + ], + "properties": { + "cc": { + "items": { + "type": "string" + }, + "title": "Cc", + "type": "array" + }, + "cc_mailboxes": { + "items": { + "type": "string" + }, + "title": "Cc Mailboxes", + "type": "array" + }, + "delivered_to": { + "title": "Delivered To", + "type": "string" + }, + "delivered_to_list": { + "items": { + "type": "string" + }, + "title": "Delivered To List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "from": { + "title": "From", + "type": "string" + }, + "from_list": { + "items": { + "type": "string" + }, + "title": "From List", + "type": "array" + }, + "from_mailbox": { + "title": "From Mailbox", + "type": "string" + }, + "from_mailboxes": { + "items": { + "type": "string" + }, + "title": "From Mailboxes", + "type": "array" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "is_read": { + "title": "Read", + "type": "boolean" + }, + "message_uid": { + "title": "Message UID", + "type": "string" + }, + "raw_header": { + "title": "Raw Header", + "type": "string" + }, + "reply_to": { + "title": "Reply To", + "type": "string" + }, + "reply_to_list": { + "items": { + "type": "string" + }, + "title": "Reply To List", + "type": "array" + }, + "reply_to_mailboxes": { + "items": { + "type": "string" + }, + "title": "Reply To Mailboxes", + "type": "array" + }, + "return_path": { + "title": "Return Path", + "type": "string" + }, + "sender": { + "title": "Sender", + "type": "string" + }, + "sender_mailbox": { + "title": "Sender Mailbox", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "smtp_from": { + "title": "SMTP From", + "type": "string" + }, + "smtp_to": { + "items": { + "type": "string" + }, + "title": "SMTP To", + "type": "array" + }, + "subject": { + "title": "Subject", + "type": "string" + }, + "to": { + "items": { + "type": "string" + }, + "title": "To", + "type": "array" + }, + "to_mailboxes": { + "items": { + "type": "string" + }, + "title": "To Mailboxes", + "type": "array" + }, + "uid": { + "title": "Email Thread UID", + "type": "string" + }, + "urls": { + "items": { + "$ref": "#/$defs/url" + }, + "title": "URLs", + "type": "array" + }, + "x_originating_ip": { + "items": { + "type": "string" + }, + "title": "X-Originating-IP", + "type": "array" + } + }, + "title": "Email", + "type": "object" + }, + "environment_variable": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Environment Variable", + "type": "object" + }, + "technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Technique", + "type": "object" + }, + "http_request": { + "additionalProperties": false, + "properties": { + "args": { + "title": "HTTP Arguments", + "type": "string" + }, + "body_length": { + "title": "Request Body Length", + "type": "integer" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "http_method": { + "enum": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "title": "HTTP Method", + "type": "string" + }, + "length": { + "title": "Request Length", + "type": "integer" + }, + "referrer": { + "title": "HTTP Referrer", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "user_agent": { + "title": "HTTP User-Agent", + "type": "string" + }, + "version": { + "title": "HTTP Version", + "type": "string" + }, + "x_forwarded_for": { + "items": { + "type": "string" + }, + "title": "X-Forwarded-For", + "type": "array" + } + }, + "title": "HTTP Request", + "type": "object" + }, + "cvss": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Base Score", + "type": "number" + }, + "depth": { + "enum": [ + "Base", + "Environmental", + "Temporal" + ], + "title": "CVSS Depth", + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/$defs/metric" + }, + "title": "Metrics", + "type": "array" + }, + "overall_score": { + "title": "Overall Score", + "type": "number" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "vector_string": { + "title": "Vector String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "base_score", + "version" + ], + "title": "CVSS Score", + "type": "object" + }, + "fingerprint": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "algorithm_id", + "value" + ], + "title": "Fingerprint", + "type": "object" + }, + "process_entity": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "path" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Process Entity", + "type": "object" + }, + "observable": { + "additionalProperties": false, + "properties": { + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Observable", + "type": "object" + }, + "http_header": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "HTTP Header", + "type": "object" + }, + "attack": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "sub_technique" + ] + }, + { + "required": [ + "tactic" + ] + }, + { + "required": [ + "technique" + ] + } + ], + "properties": { + "mitigation": { + "$ref": "#/$defs/mitigation", + "title": "MITRE Mitigation" + }, + "sub_technique": { + "$ref": "#/$defs/sub_technique", + "title": "MITRE Sub-technique" + }, + "tactic": { + "$ref": "#/$defs/tactic", + "title": "MITRE Tactic" + }, + "tactics": { + "items": { + "$ref": "#/$defs/tactic" + }, + "title": "Tactics", + "type": "array" + }, + "technique": { + "$ref": "#/$defs/technique", + "title": "MITRE Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "type": "object" + }, + "network_interface": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "mac" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "open_ports": { + "items": { + "$ref": "#/$defs/port_info" + }, + "title": "Open Ports", + "type": "array" + }, + "subnet_prefix": { + "title": "Subnet Prefix Length", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Network Interface", + "type": "object" + }, + "user": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "account" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "forward_addr": { + "title": "Forwarding Address", + "type": "string" + }, + "full_name": { + "title": "Full Name", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "has_mfa": { + "title": "MFA Assigned", + "type": "boolean" + }, + "ldap_person": { + "$ref": "#/$defs/ldap_person", + "title": "LDAP Person" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "programmatic_credentials": { + "items": { + "$ref": "#/$defs/programmatic_credential" + }, + "title": "Programmatic Credentials", + "type": "array" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "User", + "type": "object" + }, + "auth_factor": { + "additionalProperties": false, + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "factor_type": { + "title": "Factor Type", + "type": "string" + }, + "factor_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Factor Type ID", + "type": "integer" + }, + "is_hotp": { + "title": "HMAC-based One-time Password (HOTP)", + "type": "boolean" + }, + "is_totp": { + "title": "Time-based One-time Password (TOTP)", + "type": "boolean" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "security_questions": { + "items": { + "type": "string" + }, + "title": "Security Questions", + "type": "array" + } + }, + "required": [ + "factor_type_id" + ], + "title": "Authentication Factor", + "type": "object" + }, + "osint": { + "additionalProperties": false, + "properties": { + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "email_auth": { + "$ref": "#/$defs/email_auth", + "title": "Related Email Authentication" + }, + "external_uid": { + "title": "External ID", + "type": "string" + }, + "tlp": { + "enum": [ + "AMBER", + "AMBER STRICT", + "CLEAR", + "GREEN", + "RED", + "WHITE" + ], + "title": "Traffic Light Protocol", + "type": "string" + }, + "detection_pattern": { + "title": "Detection Pattern", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Related Subdomains", + "type": "array" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uploaded_time": { + "title": "Uploaded Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "comment": { + "title": "Analyst Comments", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "value": { + "title": "Indicator", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "intrusion_sets": { + "items": { + "type": "string" + }, + "title": "Intrusion Sets", + "type": "array" + }, + "script": { + "$ref": "#/$defs/script", + "title": "Related Script Data" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "Related File" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "email": { + "$ref": "#/$defs/email", + "title": "Related Email" + }, + "kill_chain": { + "items": { + "$ref": "#/$defs/kill_chain_phase" + }, + "title": "Kill Chain", + "type": "array" + }, + "detection_pattern_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Detection Pattern Type ID", + "type": "integer" + }, + "detection_pattern_type": { + "title": "Detection Pattern", + "type": "string" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "threat_actor": { + "$ref": "#/$defs/threat_actor", + "title": "Threat Actor" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 99 + ], + "title": "Indicator Type ID", + "type": "integer" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "answers": { + "items": { + "$ref": "#/$defs/dns_answer" + }, + "title": "Related DNS Answers", + "type": "array" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "campaign": { + "$ref": "#/$defs/campaign", + "title": "Campaign" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "subnet": { + "title": "Related Subnet", + "type": "string" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Related Digital Signatures", + "type": "array" + }, + "whois": { + "$ref": "#/$defs/whois", + "title": "WHOIS" + }, + "vulnerabilities": { + "items": { + "$ref": "#/$defs/vulnerability" + }, + "title": "Related Vulnerabilities", + "type": "array" + } + }, + "required": [ + "type_id", + "value" + ], + "title": "OSINT", + "type": "object" + }, + "san": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "title": "Subject Alternative Name", + "type": "object" + }, + "occurrence_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cell_name" + ] + }, + { + "required": [ + "column_name" + ] + }, + { + "required": [ + "column_number" + ] + }, + { + "required": [ + "end_line" + ] + }, + { + "required": [ + "json_path" + ] + }, + { + "required": [ + "page_number" + ] + }, + { + "required": [ + "record_index_in_array" + ] + }, + { + "required": [ + "row_number" + ] + }, + { + "required": [ + "start_line" + ] + } + ], + "properties": { + "cell_name": { + "title": "Cell Name", + "type": "string" + }, + "column_name": { + "title": "Column Name", + "type": "string" + }, + "column_number": { + "title": "Column Number", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "json_path": { + "title": "JSON Path", + "type": "string" + }, + "page_number": { + "title": "Page Number", + "type": "integer" + }, + "record_index_in_array": { + "title": "Record Index in Array", + "type": "integer" + }, + "row_number": { + "title": "Row Number", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "title": "Occurrence Details", + "type": "object" + }, + "firewall_rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "condition": { + "title": "Condition", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "match_details": { + "items": { + "type": "string" + }, + "title": "Match Details", + "type": "array" + }, + "match_location": { + "title": "Match Location", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "sensitivity": { + "title": "Sensitivity", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Firewall Rule", + "type": "object" + }, + "threat_actor": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Threat Actor Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Threat Actor Type ID", + "type": "integer" + } + }, + "required": [ + "name" + ], + "title": "Threat Actor", + "type": "object" + }, + "package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Package", + "type": "object" + }, + "packet": { + "additionalProperties": false, + "properties": { + "encoding": { + "title": "Encoding", + "type": "string" + }, + "encoding_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Encoding ID", + "type": "integer" + }, + "end_offset": { + "title": "End Offset", + "type": "integer" + }, + "format": { + "title": "Format", + "type": "string" + }, + "format_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Format ID", + "type": "integer" + }, + "sequence_number": { + "title": "Sequence Number", + "type": "integer" + }, + "source": { + "title": "Source", + "type": "string" + }, + "source_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Source ID", + "type": "integer" + }, + "start_offset": { + "title": "Start Offset", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "value" + ], + "title": "Packet", + "type": "object" + }, + "transformation_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Transformation Info", + "type": "object" + }, + "network_connection_info": { + "additionalProperties": false, + "properties": { + "boundary": { + "title": "Boundary", + "type": "string" + }, + "boundary_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Boundary ID", + "type": "integer" + }, + "community_uid": { + "title": "Community ID", + "type": "string" + }, + "direction": { + "title": "Direction", + "type": "string" + }, + "direction_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Direction ID", + "type": "integer" + }, + "flag_history": { + "title": "Connection Flag History", + "type": "string" + }, + "protocol_name": { + "title": "Protocol Name", + "type": "string" + }, + "protocol_num": { + "title": "Protocol Number", + "type": "integer" + }, + "protocol_ver": { + "title": "IP Version", + "type": "string" + }, + "protocol_ver_id": { + "enum": [ + 0, + 4, + 6, + 99 + ], + "title": "IP Version ID", + "type": "integer" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "tcp_flags": { + "title": "TCP Flags", + "type": "integer" + }, + "uid": { + "title": "Connection UID", + "type": "string" + } + }, + "required": [ + "direction_id" + ], + "title": "Network Connection Information", + "type": "object" + }, + "response": { + "additionalProperties": false, + "properties": { + "code": { + "title": "Response Code", + "type": "integer" + }, + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "error": { + "title": "Error Code", + "type": "string" + }, + "error_message": { + "title": "Error Message", + "type": "string" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "message": { + "title": "Message", + "type": "string" + } + }, + "title": "Response Elements", + "type": "object" + }, + "reporter": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Reporter", + "type": "object" + }, + "load_balancer": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "classification": { + "title": "Classification", + "type": "string" + }, + "code": { + "title": "Response Code", + "type": "integer" + }, + "dst_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Destination Endpoint" + }, + "endpoint_connections": { + "items": { + "$ref": "#/$defs/endpoint_connection" + }, + "title": "Endpoint Connections", + "type": "array" + }, + "error_message": { + "title": "Error Message", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "message": { + "title": "Message", + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/$defs/metric" + }, + "title": "Metrics", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "status_detail": { + "title": "Status Detail", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Load Balancer", + "type": "object" + }, + "url": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "path" + ] + }, + { + "required": [ + "url_string" + ] + } + ], + "properties": { + "categories": { + "items": { + "type": "string" + }, + "title": "Website Categorization", + "type": "array" + }, + "category_ids": { + "items": { + "enum": [ + 0, + 1, + 3, + 4, + 5, + 6, + 7, + 9, + 11, + 14, + 15, + 16, + 17, + 18, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 40, + 43, + 44, + 45, + 46, + 47, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 63, + 64, + 65, + 66, + 67, + 68, + 71, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 92, + 93, + 95, + 96, + 97, + 98, + 99, + 101, + 102, + 103, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 118, + 121 + ], + "type": "integer" + }, + "title": "Website Categorization IDs", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "query_string": { + "title": "HTTP Query String", + "type": "string" + }, + "resource_type": { + "title": "Resource Type", + "type": "string" + }, + "scheme": { + "title": "Scheme", + "type": "string" + }, + "subdomain": { + "title": "Subdomain", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Uniform Resource Locator", + "type": "object" + }, + "object": { + "additionalProperties": true, + "properties": {}, + "title": "Object", + "type": "object" + }, + "extension": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "version" + ], + "title": "Schema Extension", + "type": "object" + }, + "gpu_info": { + "additionalProperties": false, + "properties": { + "bus_type": { + "title": "Bus Type", + "type": "string" + }, + "bus_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Bus Type ID", + "type": "integer" + }, + "cores": { + "title": "Cores", + "type": "integer" + }, + "model": { + "title": "Model", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "vram_mode": { + "title": "VRAM Mode", + "type": "string" + }, + "vram_mode_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "VRAM Mode ID", + "type": "integer" + }, + "vram_size": { + "title": "VRAM Size", + "type": "integer" + } + }, + "title": "GPU Information", + "type": "object" + }, + "port_info": { + "additionalProperties": false, + "properties": { + "port": { + "title": "Port", + "type": "integer" + }, + "protocol_name": { + "title": "Protocol Name", + "type": "string" + }, + "protocol_num": { + "title": "Protocol Number", + "type": "integer" + } + }, + "required": [ + "port" + ], + "title": "Port Information", + "type": "object" + }, + "service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Service", + "type": "object" + }, + "tls": { + "additionalProperties": false, + "properties": { + "alert": { + "title": "Client TLS Alert", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "Certificate" + }, + "certificate_chain": { + "items": { + "type": "string" + }, + "title": "Certificate Chain", + "type": "array" + }, + "cipher": { + "title": "Cipher Suite", + "type": "string" + }, + "client_ciphers": { + "items": { + "type": "string" + }, + "title": "Client Cipher Suites", + "type": "array" + }, + "extension_list": { + "items": { + "$ref": "#/$defs/tls_extension" + }, + "title": "Extension List", + "type": "array" + }, + "handshake_dur": { + "title": "Handshake Duration", + "type": "integer" + }, + "ja3_hash": { + "$ref": "#/$defs/fingerprint", + "title": "JA3 Hash" + }, + "ja3s_hash": { + "$ref": "#/$defs/fingerprint", + "title": "JA3S Hash" + }, + "key_length": { + "title": "Key Length", + "type": "integer" + }, + "sans": { + "items": { + "$ref": "#/$defs/san" + }, + "title": "Subject Alternative Names", + "type": "array" + }, + "server_ciphers": { + "items": { + "type": "string" + }, + "title": "Server Cipher Suites", + "type": "array" + }, + "sni": { + "title": "Server Name Indication", + "type": "string" + }, + "tls_extension_list": { + "items": { + "$ref": "#/$defs/tls_extension" + }, + "title": "TLS Extension List", + "type": "array" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "version" + ], + "title": "Transport Layer Security (TLS)", + "type": "object" + }, + "metric": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Metric", + "type": "object" + }, + "request": { + "additionalProperties": false, + "properties": { + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Request Elements", + "type": "object" + }, + "sub_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Sub-technique", + "type": "object" + }, + "container": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_driver": { + "title": "Network Driver", + "type": "string" + }, + "orchestrator": { + "title": "Orchestrator", + "type": "string" + }, + "pod_uuid": { + "title": "Pod UUID", + "type": "string" + }, + "runtime": { + "title": "Runtime", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Container", + "type": "object" + }, + "malware": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "classification_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 99 + ], + "type": "integer" + }, + "title": "Classification IDs", + "type": "array" + }, + "classifications": { + "items": { + "type": "string" + }, + "title": "Classifications", + "type": "array" + }, + "cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "CVE List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "path": { + "title": "Path", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "classification_ids" + ], + "title": "Malware", + "type": "object" + }, + "win_win_service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "service_category_id" + ] + }, + { + "required": [ + "service_dependencies" + ] + }, + { + "required": [ + "service_error_control_id" + ] + }, + { + "required": [ + "service_start_name" + ] + }, + { + "required": [ + "service_start_type_id" + ] + }, + { + "required": [ + "service_type_id" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "hosting_process": { + "$ref": "#/$defs/process_entity", + "title": "Hosting Process" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "load_order_group": { + "title": "Load Order Group", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "service_category": { + "title": "Service Category", + "type": "string" + }, + "service_category_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Service Category ID", + "type": "integer" + }, + "service_dependencies": { + "items": { + "type": "string" + }, + "title": "Service Dependencies", + "type": "array" + }, + "service_dll_file": { + "$ref": "#/$defs/file", + "title": "Service DLL" + }, + "service_error_control": { + "title": "Service Error Control", + "type": "string" + }, + "service_error_control_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Error Control ID", + "type": "integer" + }, + "service_file": { + "$ref": "#/$defs/file", + "title": "Service File" + }, + "service_start_name": { + "title": "Service Start Name", + "type": "string" + }, + "service_start_type": { + "title": "Service Start Type", + "type": "string" + }, + "service_start_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Service Start Type ID", + "type": "integer" + }, + "service_type": { + "title": "Service Type", + "type": "string" + }, + "service_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Type ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Windows Service", + "type": "object" + }, + "group": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "privileges": { + "items": { + "type": "string" + }, + "title": "Privileges", + "type": "array" + }, + "type": { + "title": "Group Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "Group", + "type": "object" + }, + "device": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "region": { + "title": "Region", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "os_machine_uuid": { + "title": "OS Machine UUID", + "type": "string" + }, + "hypervisor": { + "title": "Hypervisor", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "imei_list": { + "items": { + "type": "string" + }, + "title": "IMEI List", + "type": "array" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "is_shared": { + "title": "Shared Device", + "type": "boolean" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "meid": { + "title": "MEID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "is_personal": { + "title": "Personal Device", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "iccid": { + "title": "ICCID", + "type": "string" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "udid": { + "title": "Unique Device Identifier", + "type": "string" + }, + "model": { + "title": "Model", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "boot_time": { + "title": "Boot Time", + "type": "integer" + }, + "is_supervised": { + "title": "Supervised Device", + "type": "boolean" + }, + "is_managed": { + "title": "Managed Device", + "type": "boolean" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "is_trusted": { + "title": "Trusted Device", + "type": "boolean" + }, + "boot_uid": { + "title": "Boot UID", + "type": "string" + }, + "network_interfaces": { + "items": { + "$ref": "#/$defs/network_interface" + }, + "title": "Network Interfaces", + "type": "array" + }, + "autoscale_uid": { + "title": "Autoscale UID", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "subnet": { + "title": "Subnet", + "type": "string" + }, + "eid": { + "title": "EID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "imei": { + "title": "IMEI", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "is_mobile_account_active": { + "title": "Mobile Account Active", + "type": "boolean" + }, + "is_compliant": { + "title": "Compliant Device", + "type": "boolean" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Device", + "type": "object" + }, + "campaign": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Campaign", + "type": "object" + }, + "cwe": { + "additionalProperties": false, + "properties": { + "caption": { + "title": "Caption", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "CWE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CWE", + "type": "object" + }, + "digital_signature": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "developer_uid": { + "title": "Developer UID", + "type": "string" + }, + "digest": { + "$ref": "#/$defs/fingerprint", + "title": "Message Digest" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "State ID", + "type": "integer" + } + }, + "required": [ + "algorithm_id" + ], + "title": "Digital Signature", + "type": "object" + }, + "malware_scan_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "end_time": { + "title": "End Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_files": { + "title": "Scanned Files", + "type": "integer" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "num_volumes": { + "title": "Number of Volumes", + "type": "integer" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Scan UID", + "type": "string" + }, + "unique_malware_count": { + "title": "Unique Malware Count", + "type": "integer" + } + }, + "required": [ + "type_id" + ], + "title": "Malware Scan Info", + "type": "object" + }, + "kill_chain_phase": { + "additionalProperties": false, + "properties": { + "phase": { + "title": "Kill Chain Phase", + "type": "string" + }, + "phase_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Kill Chain Phase ID", + "type": "integer" + } + }, + "required": [ + "phase_id" + ], + "title": "Kill Chain Phase", + "type": "object" + }, + "organization": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "ou_name": { + "title": "Org Unit Name", + "type": "string" + }, + "ou_uid": { + "title": "Org Unit ID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Organization", + "type": "object" + }, + "encryption_details": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Encryption Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Encryption Algorithm ID", + "type": "integer" + }, + "key_length": { + "title": "Encryption Key Length", + "type": "integer" + }, + "key_uid": { + "title": "Key UID", + "type": "string" + }, + "type": { + "title": "Encryption Type", + "type": "string" + } + }, + "title": "Encryption Details", + "type": "object" + }, + "logger": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "log_format": { + "title": "Log Format", + "type": "string" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Logger", + "type": "object" + }, + "feature": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Feature", + "type": "object" + }, + "ja4_fingerprint": { + "additionalProperties": false, + "properties": { + "section_a": { + "title": "JA4 Section A", + "type": "string" + }, + "section_b": { + "title": "JA4 Section B", + "type": "string" + }, + "section_c": { + "title": "JA4 Section C", + "type": "string" + }, + "section_d": { + "title": "JA4 Section D", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "type_id", + "value" + ], + "title": "JA4+ Fingerprint", + "type": "object" + }, + "timespan": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "duration" + ] + }, + { + "required": [ + "duration_days" + ] + }, + { + "required": [ + "duration_hours" + ] + }, + { + "required": [ + "duration_mins" + ] + }, + { + "required": [ + "duration_months" + ] + }, + { + "required": [ + "duration_secs" + ] + }, + { + "required": [ + "duration_weeks" + ] + }, + { + "required": [ + "duration_years" + ] + }, + { + "required": [ + "end_time" + ] + }, + { + "required": [ + "start_time" + ] + } + ], + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "duration_days": { + "title": "Duration Days", + "type": "integer" + }, + "duration_hours": { + "title": "Duration Hours", + "type": "integer" + }, + "duration_mins": { + "title": "Duration Minutes", + "type": "integer" + }, + "duration_months": { + "title": "Duration Months", + "type": "integer" + }, + "duration_secs": { + "title": "Duration Seconds", + "type": "integer" + }, + "duration_weeks": { + "title": "Duration Weeks", + "type": "integer" + }, + "duration_years": { + "title": "Duration Years", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Time Span Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Time Span Type ID", + "type": "integer" + } + }, + "title": "Time Span", + "type": "object" + }, + "rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Rule", + "type": "object" + }, + "http_cookie": { + "additionalProperties": false, + "properties": { + "domain": { + "title": "Domain", + "type": "string" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "http_only": { + "title": "HTTP Only", + "type": "boolean" + }, + "is_http_only": { + "title": "HTTP Only", + "type": "boolean" + }, + "is_secure": { + "title": "Secure", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "samesite": { + "title": "SameSite", + "type": "string" + }, + "secure": { + "title": "Secure", + "type": "boolean" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "HTTP Cookie", + "type": "object" + }, + "agent": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Agent Name", + "type": "string" + }, + "policies": { + "items": { + "$ref": "#/$defs/policy" + }, + "title": "Agent Policies", + "type": "array" + }, + "type": { + "title": "Agent Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Agent ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate Agent ID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Agent Version", + "type": "string" + } + }, + "title": "Agent", + "type": "object" + }, + "whois": { + "additionalProperties": false, + "properties": { + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "created_time": { + "title": "Registered At", + "type": "integer" + }, + "dnssec_status": { + "title": "DNSSEC Status", + "type": "string" + }, + "dnssec_status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "DNSSEC Status ID", + "type": "integer" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "domain_contacts": { + "items": { + "$ref": "#/$defs/domain_contact" + }, + "title": "Domain Contacts", + "type": "array" + }, + "email_addr": { + "title": "Registrar Abuse Email Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "last_seen_time": { + "title": "Last Updated At", + "type": "integer" + }, + "name_servers": { + "items": { + "type": "string" + }, + "title": "Name Servers", + "type": "array" + }, + "phone_number": { + "title": "Registrar Abuse Phone Number", + "type": "string" + }, + "registrar": { + "title": "Domain Registrar", + "type": "string" + }, + "status": { + "title": "Domain Status", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Subdomains", + "type": "array" + }, + "subnet": { + "title": "Subnet Block", + "type": "string" + } + }, + "title": "WHOIS", + "type": "object" + }, + "key_value_object": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "value" + ] + }, + { + "required": [ + "values" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + }, + "values": { + "items": { + "type": "string" + }, + "title": "Values", + "type": "array" + } + }, + "required": [ + "name" + ], + "title": "Key:Value object", + "type": "object" + }, + "metadata": { + "additionalProperties": false, + "properties": { + "log_format": { + "title": "Log Source Format", + "type": "string" + }, + "transformation_info_list": { + "items": { + "$ref": "#/$defs/transformation_info" + }, + "title": "Transformation Info", + "type": "array" + }, + "uid": { + "title": "Event UID", + "type": "string" + }, + "original_event_uid": { + "title": "Original Event ID", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "extensions": { + "items": { + "$ref": "#/$defs/extension" + }, + "title": "Schema Extensions", + "type": "array" + }, + "source": { + "title": "Source", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "loggers": { + "items": { + "$ref": "#/$defs/logger" + }, + "title": "Loggers", + "type": "array" + }, + "processed_time": { + "title": "Processed Time", + "type": "integer" + }, + "log_source": { + "title": "Log Source", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "debug": { + "items": { + "type": "string" + }, + "title": "Debug Information", + "type": "array" + }, + "profiles": { + "items": { + "type": "string" + }, + "title": "Profiles", + "type": "array" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "event_code": { + "title": "Event Code", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "reporter": { + "$ref": "#/$defs/reporter", + "title": "Reporter" + }, + "correlation_uid": { + "title": "Correlation UID", + "type": "string" + }, + "extension": { + "$ref": "#/$defs/extension", + "title": "Schema Extension" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "sequence": { + "title": "Sequence Number", + "type": "integer" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "original_time": { + "title": "Original Time", + "type": "string" + }, + "total_queued_duration": { + "$ref": "#/$defs/timespan", + "title": "Total Queued Duration" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "product", + "version" + ], + "title": "Metadata", + "type": "object" + }, + "product": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "feature": { + "$ref": "#/$defs/feature", + "title": "Feature" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Product", + "type": "object" + }, + "advisory": { + "additionalProperties": false, + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "Related CVEs", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Advisory ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Advisory", + "type": "object" + }, + "network_endpoint": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "title": "Network Endpoint", + "type": "object" + }, + "d3f_tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Tactic", + "type": "object" + }, + "cve": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "cvss": { + "items": { + "$ref": "#/$defs/cvss" + }, + "title": "CVSS Score", + "type": "array" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "cwe_uid": { + "title": "CWE UID", + "type": "string" + }, + "cwe_url": { + "title": "CWE URL", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "epss": { + "$ref": "#/$defs/epss", + "title": "EPSS" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "title": { + "title": "Title", + "type": "string" + }, + "type": { + "title": "Vulnerability Type", + "type": "string" + }, + "uid": { + "title": "CVE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CVE", + "type": "object" + }, + "domain_contact": { + "additionalProperties": false, + "properties": { + "email_addr": { + "title": "Contact Email", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Contact Location Information" + }, + "name": { + "title": "Name", + "type": "string" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "type": { + "title": "Domain Contact Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Domain Contact Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Domain Contact", + "type": "object" + }, + "tls_extension": { + "additionalProperties": false, + "properties": { + "data": { + "title": "Data" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 5, + 10, + 13, + 14, + 15, + 16, + 18, + 19, + 20, + 21, + 41, + 42, + 43, + 44, + 45, + 47, + 48, + 49, + 50, + 51, + 99 + ], + "title": "Type ID", + "type": "integer" + } + }, + "required": [ + "type_id" + ], + "title": "TLS Extension", + "type": "object" + }, + "policy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "type" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "is_applied": { + "title": "Applied", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Policy", + "type": "object" + }, + "span": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "end_time", + "start_time", + "uid" + ], + "title": "Span", + "type": "object" + } + }, + "$id": "https://schema.ocsf.io/schema/classes/http_activity", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "http_request" + ] + }, + { + "required": [ + "http_response" + ] + } + ], + "properties": { + "traffic": { + "$ref": "#/$defs/network_traffic", + "title": "Traffic" + }, + "unmapped": { + "$ref": "#/$defs/object", + "title": "Unmapped Data" + }, + "message": { + "title": "Message", + "type": "string" + }, + "http_response": { + "$ref": "#/$defs/http_response", + "title": "HTTP Response" + }, + "app_name": { + "title": "Application Name", + "type": "string" + }, + "disposition": { + "title": "Disposition", + "type": "string" + }, + "proxy_http_request": { + "$ref": "#/$defs/http_request", + "title": "Proxy HTTP Request" + }, + "app_protocol_name": { + "title": "Application Protocol Name", + "type": "string" + }, + "proxy_connection_info": { + "$ref": "#/$defs/network_connection_info", + "title": "Proxy Connection Info" + }, + "category_uid": { + "const": 4, + "title": "Category ID", + "type": "integer" + }, + "action": { + "title": "Action", + "type": "string" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "ja4_fingerprint_list": { + "items": { + "$ref": "#/$defs/ja4_fingerprint" + }, + "title": "JA4+ Fingerprints", + "type": "array" + }, + "cumulative_traffic": { + "$ref": "#/$defs/network_traffic", + "title": "Cumulative Traffic" + }, + "risk_details": { + "title": "Risk Details", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "status_detail": { + "title": "Status Detail", + "type": "string" + }, + "confidence_score": { + "title": "Confidence Score", + "type": "integer" + }, + "proxy_tls": { + "$ref": "#/$defs/tls", + "title": "Proxy TLS" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "disposition_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 99 + ], + "title": "Disposition ID", + "type": "integer" + }, + "type_name": { + "title": "Type Name", + "type": "string" + }, + "network_observation_point": { + "$ref": "#/$defs/network_endpoint", + "title": "Network Observation Point" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "raw_data": { + "title": "Raw Data", + "type": "string" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "actor": { + "$ref": "#/$defs/actor", + "title": "Actor" + }, + "proxy_http_response": { + "$ref": "#/$defs/http_response", + "title": "Proxy HTTP Response" + }, + "trace": { + "$ref": "#/$defs/trace", + "title": "Trace" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "status": { + "title": "Status", + "type": "string" + }, + "activity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Activity ID", + "type": "integer" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "packet_list": { + "items": { + "$ref": "#/$defs/packet" + }, + "title": "Packets", + "type": "array" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "proxy_traffic": { + "$ref": "#/$defs/network_traffic", + "title": "Proxy Traffic" + }, + "observables": { + "items": { + "$ref": "#/$defs/observable" + }, + "title": "Observables", + "type": "array" + }, + "enrichments": { + "items": { + "$ref": "#/$defs/enrichment" + }, + "title": "Enrichments", + "type": "array" + }, + "http_request": { + "$ref": "#/$defs/http_request", + "title": "HTTP Request" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "raw_data_hash": { + "$ref": "#/$defs/fingerprint", + "title": "Raw Data Hash" + }, + "action_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Action ID", + "type": "integer" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "observation_point_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Observation Point ID", + "type": "integer" + }, + "firewall_rule": { + "$ref": "#/$defs/firewall_rule", + "title": "Firewall Rule" + }, + "proxy": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy" + }, + "class_uid": { + "const": 4002, + "title": "Class ID", + "type": "integer" + }, + "http_status": { + "title": "HTTP Status", + "type": "integer" + }, + "activity_name": { + "title": "Activity", + "type": "string" + }, + "tls": { + "$ref": "#/$defs/tls", + "title": "TLS" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "class_name": { + "title": "Class", + "type": "string" + }, + "timezone_offset": { + "title": "Timezone Offset", + "type": "integer" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "raw_data_size": { + "title": "Raw Data Size", + "type": "integer" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "observation_point": { + "title": "Observation Point", + "type": "string" + }, + "dst_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Destination Endpoint" + }, + "is_alert": { + "title": "Alert", + "type": "boolean" + }, + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "category_name": { + "title": "Category", + "type": "string" + }, + "malware_scan_info": { + "$ref": "#/$defs/malware_scan_info", + "title": "Malware Scan Info" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "count": { + "title": "Count", + "type": "integer" + }, + "http_cookies": { + "items": { + "$ref": "#/$defs/http_cookie" + }, + "title": "HTTP Cookies", + "type": "array" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "src_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Source Endpoint" + }, + "metadata": { + "$ref": "#/$defs/metadata", + "title": "Metadata" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "connection_info": { + "$ref": "#/$defs/network_connection_info", + "title": "Connection Info" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "required": [ + "activity_id", + "category_uid", + "class_uid", + "metadata", + "severity_id", + "time", + "type_uid" + ], + "title": "HTTP Activity", + "type": "object" +} diff --git a/crates/openshell-ocsf/src/events/base_event.rs b/crates/openshell-ocsf/src/events/base_event.rs index e023516de5..4e8554f7a8 100644 --- a/crates/openshell-ocsf/src/events/base_event.rs +++ b/crates/openshell-ocsf/src/events/base_event.rs @@ -13,6 +13,10 @@ use crate::objects::{AiModel, Device, Metadata}; /// Every event class embeds this struct via `#[serde(flatten)]`. #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct BaseEventData { + /// Trace correlation (Trace profile), written only through `set_trace`. + #[serde(default)] + pub(crate) trace: Option, + /// OCSF class UID (e.g., 4001 for Network Activity). pub class_uid: u32, @@ -76,7 +80,6 @@ impl Serialize for BaseEventData { fn serialize(&self, serializer: S) -> Result { use serde::ser::SerializeMap; - // Count fields: 9 required + severity pair (2) + up to 6 optional let mut map = serializer.serialize_map(None)?; map.serialize_entry("class_uid", &self.class_uid)?; @@ -112,6 +115,11 @@ impl Serialize for BaseEventData { if let Some(ref ai_model) = self.ai_model { map.serialize_entry("ai_model", ai_model)?; } + if let Some(ref trace) = self.trace + && crate::trace::accepts_trace(self.class_uid) + { + map.serialize_entry("trace", trace)?; + } if let Some(ref unmapped) = self.unmapped { map.serialize_entry("unmapped", unmapped)?; } @@ -154,6 +162,7 @@ impl BaseEventData { metadata, device: None, ai_model: None, + trace: None, unmapped: None, } } @@ -188,6 +197,25 @@ impl BaseEventData { self.ai_model = Some(ai_model); } + /// Set trace correlation and add the Trace profile once. + /// + /// OCSF 1.8.0 declares the Trace profile only for HTTP Activity and API + /// Activity, so other classes ignore it. + pub(crate) fn set_trace(&mut self, trace: crate::TraceCorrelation) { + if !crate::trace::accepts_trace(self.class_uid) { + return; + } + self.trace = Some(trace); + if !self + .metadata + .profiles + .iter() + .any(|profile| profile == "trace") + { + self.metadata.profiles.push("trace".to_string()); + } + } + /// Add an unmapped field. pub fn add_unmapped(&mut self, key: &str, value: impl Into) { let map = self diff --git a/crates/openshell-ocsf/src/events/mod.rs b/crates/openshell-ocsf/src/events/mod.rs index e57a870f54..66038cd9cb 100644 --- a/crates/openshell-ocsf/src/events/mod.rs +++ b/crates/openshell-ocsf/src/events/mod.rs @@ -168,6 +168,22 @@ impl OcsfEvent { Self::Base(e) => &e.base, } } + + /// Returns mutable base data common to all event classes. + pub(crate) fn base_mut(&mut self) -> &mut BaseEventData { + match self { + Self::NetworkActivity(e) => &mut e.base, + Self::HttpActivity(e) => &mut e.base, + Self::SshActivity(e) => &mut e.base, + Self::ProcessActivity(e) => &mut e.base, + Self::Authentication(e) => &mut e.base, + Self::DetectionFinding(e) => &mut e.base, + Self::ApplicationLifecycle(e) => &mut e.base, + Self::DeviceConfigStateChange(e) => &mut e.base, + Self::ApiActivity(e) => &mut e.base, + Self::Base(e) => &mut e.base, + } + } } #[cfg(test)] diff --git a/crates/openshell-ocsf/src/lib.rs b/crates/openshell-ocsf/src/lib.rs index 661b4c6f5a..4b169e165c 100644 --- a/crates/openshell-ocsf/src/lib.rs +++ b/crates/openshell-ocsf/src/lib.rs @@ -31,6 +31,9 @@ pub mod events; pub mod format; pub mod objects; pub mod sanitize; +mod trace; + +pub use trace::{TraceCorrelation, register_trace_correlation_extractor}; pub mod tracing_layers; #[cfg(any(test, feature = "test-support"))] diff --git a/crates/openshell-ocsf/src/trace.rs b/crates/openshell-ocsf/src/trace.rs new file mode 100644 index 0000000000..9651e00336 --- /dev/null +++ b/crates/openshell-ocsf/src/trace.rs @@ -0,0 +1,226 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF trace correlation and optional enrichment at emission. + +use std::sync::OnceLock; + +use serde::{Deserialize, Serialize}; + +/// W3C trace ID of the span active when an OCSF event was emitted. +/// +/// This is a correlation hint, not an identity assertion: gateway context may +/// inherit a caller's traceparent, including before authentication. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(try_from = "TraceFields")] +pub struct TraceCorrelation { + uid: String, +} + +#[derive(Deserialize)] +struct TraceFields { + uid: String, +} + +impl TryFrom for TraceCorrelation { + type Error = &'static str; + + fn try_from(fields: TraceFields) -> Result { + let uid = fields.uid; + let hex = uid.len() == 32 && uid.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f')); + if !hex || uid.bytes().all(|b| b == b'0') { + return Err("trace.uid must be a non-zero W3C trace ID in lowercase hex"); + } + Ok(Self { uid }) + } +} + +impl TraceCorrelation { + /// Returns `None` for the invalid all-zero trace ID. + #[must_use] + pub fn from_trace_id(trace_id: [u8; 16]) -> Option { + if trace_id == [0; 16] { + return None; + } + Some(Self { + uid: format!("{:032x}", u128::from_be_bytes(trace_id)), + }) + } + + /// The trace ID as 32 lowercase hexadecimal characters. + #[must_use] + pub fn uid(&self) -> &str { + &self.uid + } +} + +static EXTRACTOR: OnceLock Option> = OnceLock::new(); + +/// Register the process-wide trace extractor at startup. +/// +/// Returns `false` if an extractor is already registered. Without an extractor, +/// events carry no trace correlation. +pub fn register_trace_correlation_extractor(extractor: fn() -> Option) -> bool { + EXTRACTOR.set(extractor).is_ok() +} + +/// HTTP Activity and API Activity: the classes OCSF 1.8.0 declares the Trace +/// profile for. +pub const fn accepts_trace(class_uid: u32) -> bool { + matches!(class_uid, 4002 | 6003) +} + +pub fn enrich(event: &mut crate::OcsfEvent) { + if accepts_trace(event.class_uid()) + && event.base().trace.is_none() + && let Some(trace) = EXTRACTOR.get().and_then(|extract| extract()) + { + event.base_mut().set_trace(trace); + } +} + +#[cfg(test)] +mod tests { + use crate::*; + use openshell_ocsf_validate::validate_event; + + const TRACE_ID: [u8; 16] = [ + 0x0a, 0xf7, 0x65, 0x19, 0x16, 0xcd, 0x43, 0xdd, 0x84, 0x48, 0xeb, 0x21, 0x1c, 0x80, 0x31, + 0x9c, + ]; + + fn trace() -> TraceCorrelation { + TraceCorrelation::from_trace_id(TRACE_ID).unwrap() + } + + #[test] + fn trace_id_renders_as_lowercase_w3c_hex() { + assert_eq!(trace().uid(), "0af7651916cd43dd8448eb211c80319c"); + assert!(TraceCorrelation::from_trace_id([0; 16]).is_none()); + } + + #[test] + fn deserialization_rejects_malformed_trace_ids() { + let valid = serde_json::json!({"uid": "0af7651916cd43dd8448eb211c80319c"}); + assert_eq!( + serde_json::from_value::(valid).unwrap(), + trace() + ); + for uid in [ + "", + "0AF7651916CD43DD8448EB211C80319C", + "0af7651916cd43dd8448eb211c80319", + "0af7651916cd43dd8448eb211c80319cz", + "00000000000000000000000000000000", + ] { + assert!( + serde_json::from_value::(serde_json::json!({ "uid": uid })) + .is_err(), + "{uid:?} accepted" + ); + } + } + + #[test] + fn production_event_shapes_validate_with_and_without_trace() { + let ctx = builders::test_sandbox_context(); + for (class, mut event) in builders::sample_events(&ctx) { + let plain = event.to_json().unwrap(); + validate_event(&plain).unwrap_or_else(|e| panic!("{class} without trace: {e}")); + event.base_mut().set_trace(trace()); + let correlated = event.to_json().unwrap(); + validate_event(&correlated).unwrap_or_else(|e| panic!("{class} with trace: {e}")); + if matches!(class, "http_activity" | "api_activity") { + assert_eq!(correlated["trace"]["uid"], trace().uid(), "{class}"); + } else { + assert_eq!(correlated, plain, "{class}"); + } + let decoded: OcsfEvent = serde_json::from_value(correlated.clone()).unwrap(); + assert_eq!(decoded.to_json().unwrap(), correlated); + } + } + + #[test] + fn deserialized_trace_is_dropped_from_classes_without_the_profile() { + let ctx = builders::test_sandbox_context(); + let (_, event) = builders::sample_events(&ctx) + .into_iter() + .find(|(class, _)| *class == "network_activity") + .unwrap(); + let mut value = event.to_json().unwrap(); + value["trace"] = serde_json::json!({"uid": trace().uid()}); + let decoded: OcsfEvent = serde_json::from_value(value).unwrap(); + assert!(decoded.to_json().unwrap().get("trace").is_none()); + } + + #[test] + fn gateway_api_activity_shapes_validate_with_trace() { + let ctx = builders::test_gateway_context(); + let events = [ + ApiActivityBuilder::new(&ctx, "CreateSandbox").build(), + ApiActivityBuilder::new(&ctx, "CreateSandbox") + .action(ActionId::Denied) + .build(), + ApiActivityBuilder::new(&ctx, "chat.completions") + .ai_model(AiModel::new("model", "provider")) + .build(), + ]; + for mut event in events { + event.base_mut().set_trace(trace()); + let correlated = event.to_json().unwrap(); + validate_event(&correlated).unwrap_or_else(|e| panic!("{correlated}: {e}")); + assert_eq!(correlated["trace"]["uid"], trace().uid()); + } + } + + #[test] + fn exported_schema_checks_trace_shape_and_declared_profile() { + let ctx = builders::test_sandbox_context(); + let (_, mut event) = builders::sample_events(&ctx) + .into_iter() + .find(|(class, _)| *class == "http_activity") + .unwrap(); + event.base_mut().set_trace(trace()); + let mut value = event.to_json().unwrap(); + validate_event(&value).unwrap_or_else(|e| panic!("{e}")); + value["trace"]["uid"] = serde_json::json!(42); + assert!(matches!( + validate_event(&value), + Err(openshell_ocsf_validate::Error::Invalid(_)) + )); + value["trace"] = serde_json::json!({}); + assert!(matches!( + validate_event(&value), + Err(openshell_ocsf_validate::Error::Invalid(_)) + )); + value["trace"] = + serde_json::json!({"uid": "0af7651916cd43dd8448eb211c80319c", "bogus": true}); + assert!(matches!( + validate_event(&value), + Err(openshell_ocsf_validate::Error::Invalid(_)) + )); + value["trace"].as_object_mut().unwrap().remove("bogus"); + value["metadata"]["profiles"] + .as_array_mut() + .unwrap() + .retain(|name| name != "trace"); + assert!(matches!( + validate_event(&value), + Err(openshell_ocsf_validate::Error::Invalid(_)) + )); + } + + #[test] + fn plain_event_omits_trace_profile_and_object() { + let ctx = builders::test_sandbox_context(); + let value = BaseEventBuilder::new(&ctx).build().to_json().unwrap(); + assert!(value.get("trace").is_none()); + assert!( + !value["metadata"]["profiles"] + .as_array() + .unwrap() + .iter() + .any(|p| p == "trace") + ); + } +} diff --git a/crates/openshell-ocsf/src/tracing_layers/event_bridge.rs b/crates/openshell-ocsf/src/tracing_layers/event_bridge.rs index c4a5ccb895..ca5a5e9cdd 100644 --- a/crates/openshell-ocsf/src/tracing_layers/event_bridge.rs +++ b/crates/openshell-ocsf/src/tracing_layers/event_bridge.rs @@ -34,7 +34,8 @@ pub fn clone_current_event() -> Option { /// as human-readable shorthand and structured JSONL for their configured sinks. /// /// Both layers receive the event — `clone_current_event()` is non-consuming. -pub fn emit_ocsf_event(event: OcsfEvent) { +pub fn emit_ocsf_event(mut event: OcsfEvent) { + crate::trace::enrich(&mut event); // Store the event in thread-local so layers can access it set_current_event(event); @@ -81,7 +82,8 @@ pub fn clear_current_event() { /// bare `emit_ocsf_event`, but the gateway hosts many sandboxes, so it stamps a /// per-event `sandbox_id` field here instead. One tracing event feeds both the /// JSONL audit file and the routing bus. -pub fn emit_ocsf_event_routed(sandbox_id: &str, event: OcsfEvent) { +pub fn emit_ocsf_event_routed(sandbox_id: &str, mut event: OcsfEvent) { + crate::trace::enrich(&mut event); let message = event.format_shorthand(); set_current_event(event); tracing::info!(target: "ocsf", sandbox_id = %sandbox_id, message = %message); diff --git a/crates/openshell-otel/Cargo.toml b/crates/openshell-otel/Cargo.toml index c6cd0111c9..a857018b91 100644 --- a/crates/openshell-otel/Cargo.toml +++ b/crates/openshell-otel/Cargo.toml @@ -11,31 +11,31 @@ license.workspace = true repository.workspace = true [dependencies] -async-trait = "0.1" -bytes = { workspace = true } -futures = { workspace = true } +async-trait = { version = "0.1", optional = true } +bytes = { workspace = true, optional = true } +futures = { workspace = true, optional = true } http = { workspace = true } -http-body-util = { workspace = true } -hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "logging", "tls12"] } -hyper-util = { workspace = true, features = ["client-legacy", "http1", "tokio"] } -metrics = { workspace = true } -openshell-crypto = { workspace = true } -openshell-ocsf = { path = "../openshell-ocsf" } +http-body-util = { workspace = true, optional = true } +hyper-rustls = { version = "0.27", default-features = false, optional = true, features = ["http1", "logging", "tls12"] } +hyper-util = { workspace = true, optional = true, features = ["client-legacy", "http1", "tokio"] } +metrics = { workspace = true, optional = true } +openshell-crypto = { workspace = true, optional = true } +openshell-ocsf = { path = "../openshell-ocsf", optional = true } opentelemetry = { workspace = true } -opentelemetry_sdk = { workspace = true, features = ["logs"] } -opentelemetry-http = { workspace = true } -opentelemetry-otlp = { workspace = true, features = ["gzip-http", "http-proto", "logs"] } -rustls = { workspace = true } -rustls-native-certs = { workspace = true } -serde_json = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true } +opentelemetry_sdk = { workspace = true } +opentelemetry-http = { workspace = true, optional = true } +opentelemetry-otlp = { workspace = true, optional = true, features = ["gzip-http", "http-proto", "logs"] } +rustls = { workspace = true, optional = true } +rustls-native-certs = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } +thiserror = { workspace = true, optional = true } +tokio = { workspace = true, optional = true } tracing = { workspace = true } tracing-opentelemetry = { workspace = true } -tracing-subscriber = { workspace = true } +tracing-subscriber = { workspace = true, optional = true } tonic = { workspace = true } -tower-http = { workspace = true } -zeroize = { workspace = true } +tower-http = { workspace = true, optional = true } +zeroize = { workspace = true, optional = true } [dev-dependencies] openshell-otel-test-support = { path = "../openshell-otel-test-support" } @@ -44,6 +44,37 @@ opentelemetry_sdk = { workspace = true, features = ["testing"] } prost = { workspace = true } tempfile = "3" tokio = { workspace = true, features = ["test-util"] } +tracing-subscriber = { workspace = true } + +[features] +default = ["export"] +# OTLP/gRPC export: tracer providers, tracing layers, compute-driver and +# gRPC tracing machinery, and the OCSF log bridge. Services that report to +# a collector keep the default; clients that only forward trace context +# (e.g. the CLI) opt out with `default-features = false` so the exporter +# stack stays out of their binaries. +export = [ + "dep:async-trait", + "dep:bytes", + "dep:futures", + "dep:http-body-util", + "dep:hyper-rustls", + "dep:hyper-util", + "dep:metrics", + "dep:openshell-crypto", + "dep:openshell-ocsf", + "dep:opentelemetry-http", + "dep:opentelemetry-otlp", + "dep:rustls", + "dep:rustls-native-certs", + "dep:serde_json", + "dep:thiserror", + "dep:tokio", + "dep:tower-http", + "dep:tracing-subscriber", + "dep:zeroize", + "opentelemetry_sdk/logs", +] [lints] workspace = true diff --git a/crates/openshell-otel/src/correlation.rs b/crates/openshell-otel/src/correlation.rs new file mode 100644 index 0000000000..d451ef725b --- /dev/null +++ b/crates/openshell-otel/src/correlation.rs @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Sampled trace correlation for OCSF producers. + +use opentelemetry::trace::{SpanContext, TraceContextExt as _}; + +/// Extract the active span's W3C trace ID for an OCSF event. +/// +/// Returns `None` unless the current span has valid, sampled context. This reads +/// the current span without extracting workload HTTP headers or environment +/// variables. The active span may already inherit a gateway caller's context, +/// including before authentication. This is a correlation hint, not proof of +/// origin. A sampled ID does not guarantee export or retention. +#[must_use] +pub fn current_ocsf_trace_correlation() -> Option { + let span_context = sampled_span_context()?; + openshell_ocsf::TraceCorrelation::from_trace_id(span_context.trace_id().to_bytes()) +} + +/// The OpenTelemetry tracing layer activates a span's context on entry, so this +/// also works inside a layer, where the tracing dispatcher is busy. +pub fn sampled_span_context() -> Option { + let context = opentelemetry::Context::current(); + let span_context = context.span().span_context().clone(); + (span_context.is_valid() && span_context.is_sampled()).then_some(span_context) +} diff --git a/crates/openshell-otel/src/lib.rs b/crates/openshell-otel/src/lib.rs index 78b995ee0d..32077a7f93 100644 --- a/crates/openshell-otel/src/lib.rs +++ b/crates/openshell-otel/src/lib.rs @@ -2,558 +2,79 @@ // SPDX-License-Identifier: Apache-2.0 //! Shared OpenTelemetry trace and log export support for `OpenShell` services. +//! +//! The crate has two tiers: +//! +//! - Trace-context propagation, always compiled in: W3C `traceparent` / +//! `tracestate` extraction and injection for HTTP headers, tonic metadata, +//! and process environments. +//! - The `export` feature (on by default): OTLP tracer providers, tracing +//! layers, compute-driver and gRPC tracing machinery, and the OCSF log +//! bridge. Services that report to a collector keep the default; clients +//! that only forward trace context (e.g. the CLI) opt out with +//! `default-features = false` to keep the exporter stack out of their +//! binaries. +mod propagation; + +pub use propagation::{ + EnvTraceContextInterceptor, HeaderMapExtractor, MetadataMapInjector, TRACEPARENT_ENV, + TRACESTATE_ENV, TraceContextInterceptor, current_trace_context_carrier, + current_trace_context_environment, set_parent_from_environment, set_parent_from_trace_context, + trace_context_environment, +}; + +#[cfg(feature = "export")] +mod correlation; +#[cfg(feature = "export")] mod driver; +#[cfg(feature = "export")] mod grpc; +#[cfg(feature = "export")] mod headers; +#[cfg(feature = "export")] mod logs; +#[cfg(feature = "export")] mod ocsf; -mod propagation; +#[cfg(feature = "export")] +mod setup; +#[cfg(feature = "export")] mod tls; +#[cfg(feature = "export")] mod transport; #[cfg(test)] #[path = "../../../tests/support/environment.rs"] mod test_environment; +#[cfg(feature = "export")] +pub use correlation::current_ocsf_trace_correlation; + +#[cfg(feature = "export")] pub use driver::{ BoxGrpcStream, ComputeDriverTracing, DriverTracingConfig, DriverTracingHandle, IN_PROCESS_COMPUTE_DRIVER_TARGET, InProcessRpcTracer, install_driver_tracing, }; - +#[cfg(feature = "export")] pub use grpc::{ COMPUTE_DRIVER_RPC_SERVICE, ComputeDriverRpc, ComputeDriverRpcSpan, RecordGrpcFailure, RecordGrpcStatus, TracedGrpcStream, compute_driver_rpc_layer, compute_driver_rpc_operation, grpc_status_code_name, record_grpc_status, rpc, }; +#[cfg(feature = "export")] pub use headers::{encode_otlp_headers, merge_otlp_headers, parse_otlp_headers}; +#[cfg(feature = "export")] pub use ocsf::{OcsfOtlp, OcsfOtlpLayer}; -pub use propagation::{ - HeaderMapExtractor, MetadataMapInjector, TRACEPARENT_ENV, TRACESTATE_ENV, - TraceContextInterceptor, current_trace_context_carrier, current_trace_context_environment, - set_parent_from_environment, set_parent_from_trace_context, trace_context_environment, -}; +#[cfg(feature = "export")] pub use tls::{MAX_TLS_FILE_BYTES, OtlpTls, TlsError, TlsFileError}; - +#[cfg(feature = "export")] pub use transport::{ CONNECT_TIMEOUT, OtlpHttpClient, TCP_KEEPALIVE_IDLE, TCP_KEEPALIVE_INTERVAL, TCP_KEEPALIVE_RETRIES, signal_url, }; -use opentelemetry::KeyValue; -use opentelemetry::trace::TracerProvider as _; -pub use opentelemetry_otlp::LogExporter; -use opentelemetry_otlp::tonic_types::metadata::MetadataMap; -use opentelemetry_otlp::{SpanExporter, WithExportConfig, WithHttpConfig, WithTonicConfig}; -use opentelemetry_sdk::Resource; -use opentelemetry_sdk::trace::SdkTracer; -pub use opentelemetry_sdk::trace::SdkTracerProvider; -use tracing::Subscriber; -use tracing_opentelemetry::OpenTelemetryLayer; -use tracing_subscriber::Layer as _; -use tracing_subscriber::layer::{Context, Filter}; -use tracing_subscriber::registry::LookupSpan; - -const SDK_UNKNOWN_SERVICE_PREFIX: &str = "unknown_service"; - -/// Build the resource attributes shared by gateway and compute-driver providers. -pub fn gateway_resource_attributes( - gateway_name: Option<&str>, - compute_driver: Option<&str>, -) -> Vec { - let mut attributes = Vec::new(); - if let Some(name) = gateway_name.map(str::trim).filter(|name| !name.is_empty()) { - attributes.push(KeyValue::new("openshell.gateway.name", name.to_string())); - } - if let Some(driver) = compute_driver - .map(str::trim) - .filter(|driver| !driver.is_empty()) - { - attributes.push(KeyValue::new( - "openshell.gateway.compute_driver", - driver.to_string(), - )); - } - attributes -} - -/// Mark `span` as failed. -/// -/// The field must be declared on the span at creation because `tracing` drops -/// records for fields a span does not have. -pub fn mark_error(span: &tracing::Span) { - span.record("otel.status_code", "ERROR"); -} - -/// Marks the current span when an instrumented operation returns an error. -pub fn record_error_result(result: Result) -> Result { - if result.is_err() { - mark_error(&tracing::Span::current()); - } - result -} - -/// Marks an instrumented function's span when it exits before returning success. -/// -/// Create the guard at the start of the instrumented function and return the -/// successful result through [`Self::finish`]. An early `?` or error return -/// drops the unfinished guard and marks the captured span as failed. -#[must_use] -pub struct ErrorStatusGuard { - span: tracing::Span, - finished: bool, -} - -impl ErrorStatusGuard { - /// Captures the current instrumented span. - pub fn current() -> Self { - Self { - span: tracing::Span::current(), - finished: false, - } - } - - /// Returns `result`, marking this guard complete when it is successful. - pub fn finish(mut self, result: Result) -> Result { - self.finished = result.is_ok(); - result - } -} - -impl Drop for ErrorStatusGuard { - fn drop(&mut self) { - if !self.finished { - mark_error(&self.span); - } - } -} - -/// How a process chooses its OpenTelemetry `service.name`. -#[derive(Debug, Clone, Copy)] -pub enum ServiceName<'a> { - /// Always use this name, overriding `OTEL_SERVICE_NAME`. - Fixed(&'a str), - /// Use `OTEL_SERVICE_NAME` when set, otherwise use this default. - EnvironmentOr(&'a str), -} - -/// OTLP transport to a collector. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub enum Protocol { - #[default] - Grpc, - HttpProtobuf, -} - -/// Inputs for OTLP export. -#[derive(Debug, Clone)] -pub struct OtlpExportConfig<'a> { - pub endpoint: &'a str, - pub service_name: ServiceName<'a>, - pub service_version: Option<&'a str>, - pub resource_attributes: Vec, -} - -/// How exports reach a collector: the transport, request headers, and TLS. -#[derive(Clone, Default, PartialEq, Eq)] -pub struct CollectorTransport { - pub protocol: Protocol, - pub headers: http::HeaderMap, - pub tls: OtlpTls, -} - -impl std::fmt::Debug for CollectorTransport { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("CollectorTransport") - .field("protocol", &self.protocol) - .field("header_names", &self.headers.keys().collect::>()) - .field("tls", &self.tls) - .finish() - } -} - -/// Failure to set up OTLP export. -#[derive(Debug, thiserror::Error)] -pub enum SetupError { - #[error("OTLP endpoint is empty")] - EmptyEndpoint, - - #[error("invalid OTLP endpoint {endpoint:?}: {source}")] - InvalidEndpoint { - endpoint: String, - source: http::uri::InvalidUri, - }, - - #[error("failed to build the OTLP exporter: {0}")] - Exporter(#[from] opentelemetry_otlp::ExporterBuildError), - - #[error("invalid OTLP endpoint {endpoint:?}: {source}")] - InvalidSignalUrl { - endpoint: String, - source: http::Error, - }, - - #[error("OTLP TLS settings need an https:// endpoint, not {endpoint:?}")] - TlsWithoutHttps { endpoint: String }, - - #[error("invalid OTLP TLS settings: {0}")] - Tls(#[from] TlsError), - - #[error("invalid OTLP TLS files: {0}")] - TlsFiles(#[from] TlsFileError), - - #[error("OTLP/HTTP export needs a tokio runtime")] - NoRuntime, -} - -fn resource_attributes(config: &OtlpExportConfig<'_>) -> Vec { - let mut attributes = config.resource_attributes.clone(); - if let Some(version) = config - .service_version - .map(str::trim) - .filter(|version| !version.is_empty()) - { - attributes.push(KeyValue::new("service.version", version.to_string())); - } - attributes -} - -/// Build the OpenTelemetry resource for an export configuration. -#[must_use] -pub fn resource_for(config: &OtlpExportConfig<'_>) -> Resource { - let attributes = resource_attributes(config); - match config.service_name { - ServiceName::Fixed(name) => Resource::builder() - .with_service_name(name.trim().to_string()) - .with_attributes(attributes) - .build(), - ServiceName::EnvironmentOr(default) => { - let detected = Resource::builder() - .with_attributes(attributes.clone()) - .build(); - if detected - .get(&opentelemetry::Key::from_static_str("service.name")) - .is_some_and(|value| !value.to_string().starts_with(SDK_UNKNOWN_SERVICE_PREFIX)) - { - detected - } else { - Resource::builder() - .with_service_name(default.trim().to_string()) - .with_attributes(attributes) - .build() - } - } - } -} - -/// Trim and validate an OTLP endpoint before handing it to an exporter. -/// -/// Every exporter that reads an operator-supplied endpoint uses this, so a -/// value accepted for one signal is accepted for all of them. -pub fn validated_endpoint(endpoint: &str) -> Result<(&str, http::Uri), SetupError> { - let endpoint = endpoint.trim(); - if endpoint.is_empty() { - return Err(SetupError::EmptyEndpoint); - } - let uri = endpoint - .parse::() - .map_err(|source| SetupError::InvalidEndpoint { - endpoint: endpoint.to_string(), - source, - })?; - Ok((endpoint, uri)) -} - -/// A validated OTLP collector endpoint and the resource that every signal -/// exported to it describes. -#[derive(Clone)] -pub struct OtlpTarget { - endpoint: String, - tls: bool, - protocol: Protocol, - headers: http::HeaderMap, - tls_settings: OtlpTls, - resource: Resource, -} - -impl std::fmt::Debug for OtlpTarget { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("OtlpTarget") - .field("endpoint", &self.endpoint) - .field("tls", &self.tls) - .field("protocol", &self.protocol) - .field("header_names", &self.headers.keys().collect::>()) - .field("tls_settings", &self.tls_settings) - .field("resource", &self.resource) - .finish() - } -} - -impl OtlpTarget { - /// Validate the endpoint and build the resource. - pub fn new(config: &OtlpExportConfig<'_>) -> Result { - let (endpoint, uri) = validated_endpoint(config.endpoint)?; - Ok(Self { - endpoint: endpoint.to_string(), - tls: uri.scheme() == Some(&http::uri::Scheme::HTTPS), - protocol: Protocol::Grpc, - headers: http::HeaderMap::new(), - tls_settings: OtlpTls::default(), - resource: resource_for(config), - }) - } - - /// Export over `protocol` instead of gRPC. - #[must_use] - pub fn with_protocol(mut self, protocol: Protocol) -> Self { - self.protocol = protocol; - self - } - - /// Send `headers` with every export request. Headers from the process - /// environment (`OTEL_EXPORTER_OTLP_*HEADERS`) still apply and replace - /// any of the same name. - #[must_use] - pub fn with_headers(mut self, headers: http::HeaderMap) -> Self { - self.headers = headers; - self - } - - /// Reach the collector the way `transport` describes. - #[must_use] - pub fn with_transport(self, transport: &CollectorTransport) -> Self { - self.with_protocol(transport.protocol) - .with_headers(transport.headers.clone()) - .with_tls(transport.tls.clone()) - } - - /// Verify an `https://` collector and authenticate to it with `tls`. - #[must_use] - pub fn with_tls(mut self, tls: OtlpTls) -> Self { - self.tls_settings = tls; - self - } - - /// The URL of `signal_path` under the endpoint, for OTLP/HTTP export. - pub(crate) fn http_signal_url(&self, signal_path: &str) -> Result { - signal_url(&self.endpoint, signal_path) - .map(|url| url.to_string()) - .map_err(|source| SetupError::InvalidSignalUrl { - endpoint: self.endpoint.clone(), - source, - }) - } - - /// Point an exporter builder at this target, over TLS for `https://`. - pub(crate) fn configure(&self, builder: B) -> B - where - B: WithExportConfig + WithTonicConfig, - { - let builder = builder - .with_endpoint(self.endpoint.clone()) - .with_metadata(MetadataMap::from_headers(self.headers.clone())); - if self.tls { - builder.with_tls_config(self.tls_settings.grpc_config()) - } else { - builder - } - } - - /// Point an OTLP/HTTP exporter builder at `signal_path` under this - /// target. - pub(crate) fn configure_http( - &self, - builder: B, - signal_path: &str, - timeout_var: &str, - ) -> Result - where - B: WithExportConfig + WithHttpConfig, - { - // The SDK percent-decodes these values again. - let headers = self - .headers - .iter() - .filter_map(|(name, value)| { - Some((name.to_string(), value.to_str().ok()?.replace('%', "%25"))) - }) - .collect(); - Ok(builder - .with_protocol(opentelemetry_otlp::Protocol::HttpBinary) - .with_endpoint(self.http_signal_url(signal_path)?) - .with_http_client(OtlpHttpClient::new( - self.tls.then_some(&self.tls_settings), - transport::export_timeout(timeout_var), - )?) - .with_headers(headers)) - } - - #[must_use] - pub fn endpoint(&self) -> &str { - &self.endpoint - } - - #[must_use] - pub fn resource(&self) -> &Resource { - &self.resource - } - - /// Fails when TLS settings are given for a plaintext endpoint. - pub(crate) fn check_tls(&self) -> Result<(), SetupError> { - if self.tls || self.tls_settings.is_default() { - Ok(()) - } else { - Err(SetupError::TlsWithoutHttps { - endpoint: self.endpoint.clone(), - }) - } - } - - /// Build a trace provider that exports spans to this target in batches. - pub fn tracer_provider(&self) -> Result { - self.check_tls()?; - let exporter = match self.protocol { - Protocol::Grpc => self - .configure(SpanExporter::builder().with_tonic()) - .build()?, - Protocol::HttpProtobuf => self - .configure_http( - SpanExporter::builder().with_http(), - "v1/traces", - "OTEL_EXPORTER_OTLP_TRACES_TIMEOUT", - )? - .build()?, - }; - Ok(SdkTracerProvider::builder() - .with_batch_exporter(exporter) - .with_resource(self.resource.clone()) - .build()) - } -} - -/// Build an OTLP trace provider. -pub fn build_provider(config: &OtlpExportConfig<'_>) -> Result { - OtlpTarget::new(config)?.tracer_provider() -} - -/// Build the provider for an optional OTLP configuration. -/// -/// Telemetry setup failures disable export and remain available for the caller -/// to report after its tracing subscriber is installed. -#[must_use] -pub fn provider_for( - config: Option>, -) -> (Option, Option) { - match config.as_ref().map(build_provider) { - None => (None, None), - Some(Ok(provider)) => (Some(provider), None), - Some(Err(error)) => (None, Some(error)), - } -} - -/// Filtered OpenTelemetry layer returned by [`layer`]. -pub type OtlpLayer = tracing_subscriber::filter::Filtered< - OpenTelemetryLayer, - tracing_subscriber::filter::FilterFn, - S, ->; - -pub type TargetOtlpLayer = - tracing_subscriber::filter::Filtered, TargetPrefixFilter, S>; - -#[derive(Debug, Clone)] -pub struct TargetPrefixFilter { - prefixes: Vec<&'static str>, - include: bool, -} - -impl Filter for TargetPrefixFilter { - fn enabled(&self, metadata: &tracing::Metadata<'_>, _ctx: &Context<'_, S>) -> bool { - let matches_prefix = self - .prefixes - .iter() - .any(|prefix| metadata.target().starts_with(prefix)); - metadata.is_span() - && !metadata.target().starts_with("opentelemetry") - && (matches_prefix == self.include) - } -} - -/// Build a tracing layer that exports spans and excludes exporter callsites. -pub fn layer(provider: &SdkTracerProvider, instrumentation_scope: &'static str) -> OtlpLayer -where - S: Subscriber + for<'span> LookupSpan<'span>, -{ - tracing_opentelemetry::layer() - .with_tracer(provider.tracer(instrumentation_scope)) - .with_filter(tracing_subscriber::filter::filter_fn(|metadata| { - metadata.is_span() && !metadata.target().starts_with("opentelemetry") - })) -} - -/// Build a tracing layer that exports only spans from `target_prefix`. -pub fn layer_for_target_prefix( - provider: &SdkTracerProvider, - instrumentation_scope: &'static str, - target_prefix: &'static str, -) -> TargetOtlpLayer -where - S: Subscriber + for<'span> LookupSpan<'span>, -{ - layer_for_target_prefixes(provider, instrumentation_scope, [target_prefix]) -} - -/// Build a tracing layer that exports only spans from `target_prefixes`. -pub fn layer_for_target_prefixes( - provider: &SdkTracerProvider, - instrumentation_scope: &'static str, - target_prefixes: impl IntoIterator, -) -> TargetOtlpLayer -where - S: Subscriber + for<'span> LookupSpan<'span>, -{ - tracing_opentelemetry::layer() - .with_tracer(provider.tracer(instrumentation_scope)) - .with_filter(TargetPrefixFilter { - prefixes: target_prefixes.into_iter().collect(), - include: true, - }) -} - -/// Build a tracing layer that excludes spans from `target_prefix`. -/// -/// `None` excludes no application spans. -pub fn layer_excluding_target_prefix( - provider: &SdkTracerProvider, - instrumentation_scope: &'static str, - target_prefix: Option<&'static str>, -) -> TargetOtlpLayer -where - S: Subscriber + for<'span> LookupSpan<'span>, -{ - layer_excluding_target_prefixes(provider, instrumentation_scope, target_prefix) -} - -/// Build a tracing layer that excludes spans from `target_prefixes`. -/// -/// An empty iterator excludes no application spans. -pub fn layer_excluding_target_prefixes( - provider: &SdkTracerProvider, - instrumentation_scope: &'static str, - target_prefixes: impl IntoIterator, -) -> TargetOtlpLayer -where - S: Subscriber + for<'span> LookupSpan<'span>, -{ - tracing_opentelemetry::layer() - .with_tracer(provider.tracer(instrumentation_scope)) - .with_filter(TargetPrefixFilter { - prefixes: target_prefixes.into_iter().collect(), - include: false, - }) -} +#[cfg(feature = "export")] +pub use setup::*; #[cfg(test)] pub(crate) fn test_lock() -> std::sync::MutexGuard<'static, ()> { @@ -561,256 +82,3 @@ pub(crate) fn test_lock() -> std::sync::MutexGuard<'static, ()> { LOCK.lock() .unwrap_or_else(std::sync::PoisonError::into_inner) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn error_status_guard_marks_only_unfinished_results() { - use tracing_subscriber::layer::SubscriberExt as _; - - let _tracing_lock = test_lock(); - let exporter = opentelemetry_sdk::trace::InMemorySpanExporterBuilder::new().build(); - let provider = SdkTracerProvider::builder() - .with_simple_exporter(exporter.clone()) - .build(); - let subscriber = tracing_subscriber::registry().with(layer(&provider, "guard-test")); - - tracing::subscriber::with_default(subscriber, || { - let failed = - tracing::info_span!("failed-operation", otel.status_code = tracing::field::Empty); - { - let _entered = failed.enter(); - drop(ErrorStatusGuard::current()); - } - drop(failed); - - let succeeded = tracing::info_span!( - "successful-operation", - otel.status_code = tracing::field::Empty - ); - { - let _entered = succeeded.enter(); - ErrorStatusGuard::current().finish(Ok::<_, ()>(())).unwrap(); - } - drop(succeeded); - }); - - let spans = exporter.get_finished_spans().unwrap(); - let failed = spans - .iter() - .find(|span| span.name == "failed-operation") - .unwrap(); - assert!(matches!( - failed.status, - opentelemetry::trace::Status::Error { .. } - )); - let succeeded = spans - .iter() - .find(|span| span.name == "successful-operation") - .unwrap(); - assert_eq!(succeeded.status, opentelemetry::trace::Status::Unset); - } - - #[test] - fn target_scoped_layers_partition_in_process_driver_spans() { - use tracing_subscriber::layer::SubscriberExt as _; - - let _tracing_lock = test_lock(); - let gateway_exporter = opentelemetry_sdk::trace::InMemorySpanExporterBuilder::new().build(); - let gateway_provider = SdkTracerProvider::builder() - .with_simple_exporter(gateway_exporter.clone()) - .build(); - let driver_exporter = opentelemetry_sdk::trace::InMemorySpanExporterBuilder::new().build(); - let driver_provider = SdkTracerProvider::builder() - .with_simple_exporter(driver_exporter.clone()) - .build(); - let subscriber = tracing_subscriber::registry() - .with(layer_excluding_target_prefix( - &gateway_provider, - "gateway", - Some("driver_target"), - )) - .with(layer_for_target_prefix( - &driver_provider, - "driver", - "driver_target", - )); - - tracing::subscriber::with_default(subscriber, || { - let gateway = tracing::info_span!(target: "gateway_target", "gateway.span"); - let _entered = gateway.enter(); - drop(tracing::info_span!(target: "driver_target::operation", "driver.span")); - }); - - gateway_provider.force_flush().unwrap(); - driver_provider.force_flush().unwrap(); - let gateway_spans = gateway_exporter.get_finished_spans().unwrap(); - let driver_spans = driver_exporter.get_finished_spans().unwrap(); - assert_eq!( - gateway_spans - .iter() - .map(|span| span.name.as_ref()) - .collect::>(), - ["gateway.span"] - ); - assert_eq!( - driver_spans - .iter() - .map(|span| span.name.as_ref()) - .collect::>(), - ["driver.span"] - ); - assert_eq!( - driver_spans[0].span_context.trace_id(), - gateway_spans[0].span_context.trace_id() - ); - assert_eq!( - driver_spans[0].parent_span_id, - gateway_spans[0].span_context.span_id() - ); - } - - #[test] - fn excluding_no_target_prefix_exports_all_application_spans() { - use tracing_subscriber::layer::SubscriberExt as _; - - let _tracing_lock = test_lock(); - let exporter = opentelemetry_sdk::trace::InMemorySpanExporterBuilder::new().build(); - let provider = SdkTracerProvider::builder() - .with_simple_exporter(exporter.clone()) - .build(); - let subscriber = tracing_subscriber::registry() - .with(layer_excluding_target_prefix(&provider, "gateway", None)); - - tracing::subscriber::with_default(subscriber, || { - drop(tracing::info_span!(target: "\0driver_target", "nul-prefixed.span")); - drop(tracing::info_span!(target: "gateway_target", "gateway.span")); - }); - - provider.force_flush().unwrap(); - let spans = exporter.get_finished_spans().unwrap(); - assert_eq!(spans.len(), 2); - assert!(spans.iter().any(|span| span.name == "nul-prefixed.span")); - assert!(spans.iter().any(|span| span.name == "gateway.span")); - } - - #[test] - fn resource_uses_fixed_service_identity_and_custom_attributes() { - let resource = resource_for(&OtlpExportConfig { - endpoint: "http://127.0.0.1:4317", - service_name: ServiceName::Fixed("openshell-driver-vm"), - service_version: Some("1.2.3"), - resource_attributes: vec![KeyValue::new("openshell.gateway.name", "vm-dev")], - }); - - assert_eq!( - resource - .get(&opentelemetry::Key::from_static_str("service.name")) - .map(|value| value.to_string()), - Some("openshell-driver-vm".to_string()) - ); - assert_eq!( - resource - .get(&opentelemetry::Key::from_static_str("service.version")) - .map(|value| value.to_string()), - Some("1.2.3".to_string()) - ); - assert_eq!( - resource - .get(&opentelemetry::Key::from_static_str( - "openshell.gateway.name", - )) - .map(|value| value.to_string()), - Some("vm-dev".to_string()) - ); - } - - #[tokio::test] - async fn tls_settings_need_an_https_endpoint() { - let pki = openshell_otel_test_support::TestPki::generate(); - let tls = OtlpTls::from_pem(Some(pki.ca_pem.as_bytes()), None).unwrap(); - for protocol in [Protocol::Grpc, Protocol::HttpProtobuf] { - let target = |endpoint| { - OtlpTarget::new(&OtlpExportConfig { - endpoint, - service_name: ServiceName::Fixed("test-service"), - service_version: None, - resource_attributes: Vec::new(), - }) - .unwrap() - .with_protocol(protocol) - .with_tls(tls.clone()) - }; - assert!(matches!( - target("http://collector:4317").tracer_provider(), - Err(SetupError::TlsWithoutHttps { .. }) - )); - assert!(matches!( - target("http://collector:4317").log_exporter(), - Err(SetupError::TlsWithoutHttps { .. }) - )); - assert!(target("https://collector:4317").tracer_provider().is_ok()); - } - } - - #[test] - fn debug_output_names_headers_without_their_values() { - let target = OtlpTarget::new(&OtlpExportConfig { - endpoint: "http://collector:4317", - service_name: ServiceName::Fixed("test-service"), - service_version: None, - resource_attributes: Vec::new(), - }) - .unwrap() - .with_headers(parse_otlp_headers("authorization=Bearer%20secret-token")); - - let debug = format!("{target:?}"); - assert!(debug.contains("authorization"), "{debug}"); - assert!(!debug.contains("secret-token"), "{debug}"); - } - - #[tokio::test(flavor = "multi_thread")] - async fn trace_provider_sends_headers_as_grpc_metadata() { - test_environment::Environment::new() - .remove("OTEL_EXPORTER_OTLP_HEADERS") - .remove("OTEL_EXPORTER_OTLP_TRACES_HEADERS") - .remove("OTEL_EXPORTER_OTLP_LOGS_HEADERS") - .run_async(async { - use opentelemetry::trace::Tracer as _; - - let collector = openshell_otel_test_support::OtlpTestServer::start().await; - let provider = OtlpTarget::new(&OtlpExportConfig { - endpoint: collector.endpoint(), - service_name: ServiceName::Fixed("test-service"), - service_version: None, - resource_attributes: Vec::new(), - }) - .unwrap() - .with_headers(parse_otlp_headers("authorization=Bearer%20token")) - .tracer_provider() - .unwrap(); - provider.tracer("test").in_span("grpc-span", |_| {}); - let flush = tokio::task::spawn_blocking(move || provider.force_flush()); - - collector.wait_for_export().await; - flush.await.unwrap().expect("flush succeeds"); - assert_eq!(collector.received().trace_authorizations, ["Bearer token"]); - }) - .await; - } - - #[tokio::test] - async fn malformed_endpoint_disables_export_with_a_reportable_error() { - let (provider, error) = provider_for(Some(OtlpExportConfig { - endpoint: "definitely not a url", - service_name: ServiceName::Fixed("test-service"), - service_version: None, - resource_attributes: Vec::new(), - })); - - assert!(provider.is_none()); - assert!(matches!(error, Some(SetupError::InvalidEndpoint { .. }))); - } -} diff --git a/crates/openshell-otel/src/ocsf.rs b/crates/openshell-otel/src/ocsf.rs index 3ad4dd7173..63489fde1d 100644 --- a/crates/openshell-otel/src/ocsf.rs +++ b/crates/openshell-otel/src/ocsf.rs @@ -116,10 +116,11 @@ impl Layer for OcsfOtlpLayer { let Some(event) = clone_current_event() else { return; }; - let Ok(record) = log_record(&self.logger, &event, SystemTime::now()) else { + let Ok(mut record) = log_record(&self.logger, &event, SystemTime::now()) else { self.gap.record_loss("serialization", 1); return; }; + set_trace_context(&mut record); let bytes = record_bytes(&record); if bytes > BATCH_MAX_BYTES { self.gap.record_loss("oversized", 1); @@ -399,6 +400,18 @@ fn log_record( Ok(record) } +/// Link the record to the active sampled span through the OTLP record's own +/// trace fields, for every OCSF class. +fn set_trace_context(record: &mut SdkLogRecord) { + if let Some(span_context) = crate::correlation::sampled_span_context() { + record.set_trace_context( + span_context.trace_id(), + span_context.span_id(), + Some(span_context.trace_flags()), + ); + } +} + /// Map an OCSF severity onto an OTLP severity. /// /// OCSF ranks more levels than OTLP has bands. Low and Medium share the WARN @@ -418,7 +431,10 @@ fn severity(severity: SeverityId) -> Severity { #[cfg(test)] mod tests { use super::*; - use openshell_ocsf::{ConfigStateChangeBuilder, EventContext, EventOrigin, ocsf_emit}; + use openshell_ocsf::{ + ConfigStateChangeBuilder, EventContext, EventOrigin, HttpActivityBuilder, HttpRequest, Url, + ocsf_emit, + }; use opentelemetry::Key; use opentelemetry::logs::{AnyValue, Severity}; use opentelemetry_sdk::Resource; @@ -479,6 +495,41 @@ mod tests { assert_eq!(body(&record(&event)), native(&event)); } + #[test] + fn trace_body_preserves_native_correlation() { + let mut correlated = native( + &HttpActivityBuilder::new(&event_context("sandbox-1")) + .http_request(HttpRequest::new( + "GET", + Url::new("https", "example.com", "/", 443), + )) + .build(), + ); + correlated["trace"] = serde_json::json!({"uid": "0af7651916cd43dd8448eb211c80319c"}); + correlated["metadata"]["profiles"] + .as_array_mut() + .unwrap() + .push("trace".into()); + let event: OcsfEvent = serde_json::from_value(correlated).unwrap(); + let native_body = body(&record(&event)); + assert_eq!(native_body, native(&event)); + assert_eq!( + native_body["trace"]["uid"], + "0af7651916cd43dd8448eb211c80319c" + ); + assert!( + native_body["metadata"]["profiles"] + .as_array() + .unwrap() + .iter() + .any(|profile| profile == "trace") + ); + assert_eq!( + native_body["metadata"]["version"], + openshell_ocsf::OCSF_VERSION + ); + } + #[test] fn timestamps_are_event_time_and_observed_time() { let event = event("sandbox-1", SeverityId::Informational); @@ -719,6 +770,42 @@ mod tests { assert_eq!(exporter.service_name().as_deref(), Some("openshell-test")); } + #[tokio::test] + async fn records_carry_the_sampled_span_context_for_every_class() { + use opentelemetry::trace::TraceContextExt as _; + use opentelemetry_sdk::trace::{InMemorySpanExporter, Sampler, SdkTracerProvider}; + use tracing_opentelemetry::OpenTelemetrySpanExt as _; + + for sampler in [Sampler::AlwaysOn, Sampler::AlwaysOff] { + let exporter = RecordingExporter::default(); + let otlp = OcsfOtlp::start(exporter.clone(), exporter_resource()); + let tracer = SdkTracerProvider::builder() + .with_sampler(sampler) + .with_simple_exporter(InMemorySpanExporter::default()) + .build(); + let subscriber = tracing_subscriber::registry() + .with(crate::layer(&tracer, "ocsf-test")) + .with(otlp.layer()); + let span_context = tracing::subscriber::with_default(subscriber, || { + let span = tracing::info_span!("decision"); + span.in_scope(|| ocsf_emit!(event("sandbox-1", SeverityId::Medium))); + span.context().span().span_context().clone() + }); + otlp.shutdown().await; + + let records = exporter.exported(); + let trace = records[0].record.trace_context(); + if span_context.is_sampled() { + let trace = trace.expect("sampled span context is attached"); + assert_eq!(trace.trace_id, span_context.trace_id()); + assert_eq!(trace.span_id, span_context.span_id()); + assert_eq!(trace.trace_flags, Some(span_context.trace_flags())); + } else { + assert!(trace.is_none()); + } + } + } + #[tokio::test] async fn shutdown_exports_every_queued_record_in_bounded_batches() { let exporter = RecordingExporter::default(); diff --git a/crates/openshell-otel/src/propagation.rs b/crates/openshell-otel/src/propagation.rs index 02f2bdb120..5bb7ba5ecc 100644 --- a/crates/openshell-otel/src/propagation.rs +++ b/crates/openshell-otel/src/propagation.rs @@ -6,6 +6,7 @@ use std::collections::BTreeMap; use http::HeaderMap; +use opentelemetry::Context; use opentelemetry::propagation::{Extractor, Injector, TextMapPropagator}; use opentelemetry_sdk::propagation::TraceContextPropagator; use tracing_opentelemetry::OpenTelemetrySpanExt as _; @@ -112,12 +113,16 @@ pub fn current_trace_context_environment() -> Vec<(&'static str, String)> { /// Parent `span` under the trace context in `TRACEPARENT` and `TRACESTATE`. /// +/// Reads the environment the same way [`EnvTraceContextInterceptor`] does: +/// both spellings are accepted, whitespace is trimmed, and blank values are +/// treated as unset. +/// /// Leaves `span` unchanged when the environment carries no valid context. pub fn set_parent_from_environment(span: &tracing::Span) { set_parent_from_trace_context( span, - std::env::var(TRACEPARENT_ENV).ok().as_deref(), - std::env::var(TRACESTATE_ENV).ok().as_deref(), + read_env(TRACEPARENT_ENV).as_deref(), + read_env(TRACESTATE_ENV).as_deref(), ); } @@ -177,22 +182,142 @@ impl tonic::service::Interceptor for TraceContextInterceptor { } } +/// Reads W3C trace-context propagation fields from process environment +/// variables (`TRACEPARENT` and `TRACESTATE`). +/// +/// OpenTelemetry integrations in CI systems (for example the Jenkins +/// OpenTelemetry plugin or an OpenTelemetry GitHub Action) export these +/// variables to propagate the pipeline trace to child processes. +/// Both upper-case and lower-case spellings are accepted; blank values are +/// treated as unset. +#[derive(Debug, Clone, Default)] +struct EnvTraceContext { + traceparent: Option, + tracestate: Option, +} + +impl EnvTraceContext { + fn from_env() -> Self { + Self { + traceparent: read_env(TRACEPARENT_ENV), + tracestate: read_env(TRACESTATE_ENV), + } + } +} + +/// Read one environment variable, trimming whitespace and discarding blanks. +fn read_one(name: &str) -> Option { + std::env::var(name) + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) +} + +/// Read an environment variable by its upper-case name, falling back to the +/// lower-case spelling, trimming whitespace and discarding blank values. +/// +/// Each spelling is normalized before the fallback, so a blank upper-case +/// value never shadows a valid lower-case one. +fn read_env(upper: &str) -> Option { + read_one(upper).or_else(|| read_one(&upper.to_ascii_lowercase())) +} + +impl Extractor for EnvTraceContext { + fn get(&self, key: &str) -> Option<&str> { + match key { + "traceparent" => self.traceparent.as_deref(), + "tracestate" => self.tracestate.as_deref(), + _ => None, + } + } + + fn keys(&self) -> Vec<&str> { + let mut keys = Vec::new(); + if self.traceparent.is_some() { + keys.push("traceparent"); + } + if self.tracestate.is_some() { + keys.push("tracestate"); + } + keys + } +} + +/// Passively forwards a W3C trace context captured from the process +/// environment onto outbound tonic requests. +/// +/// Unlike [`TraceContextInterceptor`], this does not require an active +/// OpenTelemetry span or an `SdkTracerProvider`; it reads `TRACEPARENT` +/// (and optionally `TRACESTATE`) once at construction and injects them as +/// gRPC metadata. This lets short-lived clients (e.g. the CLI on a CI runner) +/// extend a pipeline trace to the gateway without any collector configuration. +/// +/// When `TRACEPARENT` is unset or invalid the captured context carries no +/// valid span, so injection is a no-op. +#[derive(Debug, Clone)] +pub struct EnvTraceContextInterceptor { + context: Context, +} + +impl EnvTraceContextInterceptor { + /// Capture the W3C trace context from the process environment. + #[must_use] + pub fn from_env() -> Self { + let env = EnvTraceContext::from_env(); + let context = context_from(&env); + if env.traceparent.is_some() && !span_context_is_valid(&context) { + tracing::debug!("TRACEPARENT is set but invalid; not forwarding trace context"); + } + Self { context } + } +} + +/// Extract a [`Context`] from an [`Extractor`] using a fresh base context, so +/// the result depends only on the extractor and not on any ambient context. +fn context_from(extractor: &impl Extractor) -> Context { + TraceContextPropagator::new().extract_with_context(&Context::new(), extractor) +} + +fn span_context_is_valid(context: &Context) -> bool { + use opentelemetry::trace::TraceContextExt as _; + + context.span().span_context().is_valid() +} + +impl tonic::service::Interceptor for EnvTraceContextInterceptor { + fn call( + &mut self, + mut request: tonic::Request<()>, + ) -> Result, tonic::Status> { + TraceContextPropagator::new().inject_context( + &self.context, + &mut MetadataMapInjector::new(request.metadata_mut()), + ); + Ok(request) + } +} + #[cfg(test)] mod tests { use super::*; + use tonic::service::Interceptor as _; fn env_test_exporter() -> ( opentelemetry_sdk::trace::SdkTracerProvider, opentelemetry_sdk::trace::InMemorySpanExporter, impl tracing::Subscriber + Send + Sync, ) { + use opentelemetry::trace::TracerProvider as _; use tracing_subscriber::layer::SubscriberExt as _; let exporter = opentelemetry_sdk::trace::InMemorySpanExporterBuilder::new().build(); let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder() .with_simple_exporter(exporter.clone()) .build(); - let subscriber = tracing_subscriber::registry().with(crate::layer(&provider, "env-test")); + // Built directly from `tracing_opentelemetry` so the test runs even + // without the `export` feature. + let subscriber = tracing_subscriber::registry() + .with(tracing_opentelemetry::layer().with_tracer(provider.tracer("env-test"))); (provider, exporter, subscriber) } @@ -263,6 +388,7 @@ mod tests { #[test] fn invalid_environment_carrier_leaves_the_span_unparented() { + use opentelemetry::trace::TracerProvider as _; use tracing_subscriber::layer::SubscriberExt as _; let _tracing_lock = crate::test_lock(); @@ -270,7 +396,8 @@ mod tests { let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder() .with_simple_exporter(exporter.clone()) .build(); - let subscriber = tracing_subscriber::registry().with(crate::layer(&provider, "env-test")); + let subscriber = tracing_subscriber::registry() + .with(tracing_opentelemetry::layer().with_tracer(provider.tracer("env-test"))); tracing::subscriber::with_default(subscriber, || { let span = tracing::info_span!("orphan"); @@ -285,12 +412,14 @@ mod tests { #[test] fn interceptor_adds_traceparent_only_inside_an_exported_span() { + use opentelemetry::trace::TracerProvider as _; use tonic::service::Interceptor as _; use tracing_subscriber::layer::SubscriberExt as _; let _tracing_lock = crate::test_lock(); let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder().build(); - let subscriber = tracing_subscriber::registry().with(crate::layer(&provider, "rpc-test")); + let subscriber = tracing_subscriber::registry() + .with(tracing_opentelemetry::layer().with_tracer(provider.tracer("rpc-test"))); let (outside, inside) = tracing::subscriber::with_default(subscriber, || { let outside = TraceContextInterceptor @@ -330,4 +459,147 @@ mod tests { Some("value") ); } + + fn interceptor_with(extractor: EnvTraceContext) -> EnvTraceContextInterceptor { + EnvTraceContextInterceptor { + context: context_from(&extractor), + } + } + + /// Restores snapshotted environment variables when dropped, so a failed + /// assertion cannot leak them into later tests sharing the process. + struct EnvRestore(Vec<(&'static str, Option)>); + + impl EnvRestore { + fn snapshot(names: &[&'static str]) -> Self { + Self( + names + .iter() + .map(|name| (*name, std::env::var(name).ok())) + .collect(), + ) + } + } + + impl Drop for EnvRestore { + #[allow(unsafe_code)] + fn drop(&mut self) { + for (name, value) in &self.0 { + unsafe { + match value { + Some(value) => std::env::set_var(name, value), + None => std::env::remove_var(name), + } + } + } + } + } + + #[test] + #[allow(unsafe_code)] + fn env_trace_context_from_env_trims_and_skips_blanks() { + let _tracing_lock = crate::test_lock(); + let _restore = EnvRestore::snapshot(&[TRACEPARENT_ENV, TRACESTATE_ENV]); + let traceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"; + + // Upper-case spelling with surrounding whitespace. + unsafe { + std::env::set_var(TRACEPARENT_ENV, format!(" {traceparent} ")); + std::env::set_var(TRACESTATE_ENV, " vendor=value "); + } + let padded = EnvTraceContext::from_env(); + assert_eq!(padded.traceparent.as_deref(), Some(traceparent)); + assert_eq!(padded.tracestate.as_deref(), Some("vendor=value")); + + // Blank values are treated as unset. + unsafe { + std::env::set_var(TRACEPARENT_ENV, " "); + std::env::set_var(TRACESTATE_ENV, ""); + } + let blank = EnvTraceContext::from_env(); + assert_eq!(blank.traceparent, None); + assert_eq!(blank.tracestate, None); + } + + /// Windows treats environment names case-insensitively, so the lower-case + /// fallback cannot be exercised there: `TRACEPARENT` and `traceparent` + /// are the same variable. + #[test] + #[cfg(not(windows))] + #[allow(unsafe_code)] + fn env_trace_context_from_env_reads_lower_case_spelling() { + let _tracing_lock = crate::test_lock(); + let _restore = + EnvRestore::snapshot(&[TRACEPARENT_ENV, "traceparent", TRACESTATE_ENV, "tracestate"]); + let traceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"; + + // Lower-case-only spelling. + unsafe { + std::env::remove_var(TRACEPARENT_ENV); + std::env::remove_var(TRACESTATE_ENV); + std::env::set_var("traceparent", traceparent); + std::env::set_var("tracestate", "vendor=value"); + } + let lower = EnvTraceContext::from_env(); + assert_eq!(lower.traceparent.as_deref(), Some(traceparent)); + assert_eq!(lower.tracestate.as_deref(), Some("vendor=value")); + + // A blank upper-case value must not shadow a valid lower-case one. + unsafe { + std::env::set_var(TRACEPARENT_ENV, " "); + std::env::remove_var("tracestate"); + } + let shadowed = EnvTraceContext::from_env(); + assert_eq!(shadowed.traceparent.as_deref(), Some(traceparent)); + assert_eq!(shadowed.tracestate, None); + } + + #[test] + fn env_interceptor_forwards_valid_traceparent() { + let traceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"; + let mut interceptor = interceptor_with(EnvTraceContext { + traceparent: Some(traceparent.to_string()), + tracestate: Some("vendor=value".to_string()), + }); + + let request = interceptor.call(tonic::Request::new(())).unwrap(); + + assert_eq!( + request + .metadata() + .get("traceparent") + .and_then(|value| value.to_str().ok()), + Some(traceparent) + ); + assert_eq!( + request + .metadata() + .get("tracestate") + .and_then(|value| value.to_str().ok()), + Some("vendor=value") + ); + } + + #[test] + fn env_interceptor_is_noop_without_traceparent() { + let mut interceptor = interceptor_with(EnvTraceContext::default()); + + let request = interceptor.call(tonic::Request::new(())).unwrap(); + + assert!(request.metadata().get("traceparent").is_none()); + assert!(request.metadata().get("tracestate").is_none()); + } + + #[test] + fn env_interceptor_is_noop_for_invalid_traceparent() { + let mut interceptor = interceptor_with(EnvTraceContext { + traceparent: Some("not-a-valid-traceparent".to_string()), + tracestate: Some("vendor=value".to_string()), + }); + + let request = interceptor.call(tonic::Request::new(())).unwrap(); + + assert!(request.metadata().get("traceparent").is_none()); + assert!(request.metadata().get("tracestate").is_none()); + } } diff --git a/crates/openshell-otel/src/setup.rs b/crates/openshell-otel/src/setup.rs new file mode 100644 index 0000000000..1bf8407e59 --- /dev/null +++ b/crates/openshell-otel/src/setup.rs @@ -0,0 +1,798 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OTLP export setup: resources, providers, and tracing layers. +//! +//! Compiled only with the `export` feature; see the crate-level documentation. + +use crate::tls::{OtlpTls, TlsError, TlsFileError}; +use crate::transport::{self, OtlpHttpClient, signal_url}; +use opentelemetry::KeyValue; +use opentelemetry::trace::TracerProvider as _; +pub use opentelemetry_otlp::LogExporter; +use opentelemetry_otlp::tonic_types::metadata::MetadataMap; +use opentelemetry_otlp::{SpanExporter, WithExportConfig, WithHttpConfig, WithTonicConfig}; +use opentelemetry_sdk::Resource; +use opentelemetry_sdk::trace::SdkTracer; +pub use opentelemetry_sdk::trace::SdkTracerProvider; +use tracing::Subscriber; +use tracing_opentelemetry::OpenTelemetryLayer; +use tracing_subscriber::Layer as _; +use tracing_subscriber::layer::{Context, Filter}; +use tracing_subscriber::registry::LookupSpan; + +/// Export supervisor spans at INFO by default, or DEBUG and TRACE when enabled. +/// Dependency spans remain at INFO. +#[must_use] +pub fn supervisor_span_filter(log_level: &str) -> tracing_subscriber::EnvFilter { + use tracing_subscriber::filter::LevelFilter; + let level = log_level + .parse::() + .unwrap_or(LevelFilter::INFO) + .max(LevelFilter::INFO); + tracing_subscriber::EnvFilter::new(format!("info,openshell={level}")) +} + +const SDK_UNKNOWN_SERVICE_PREFIX: &str = "unknown_service"; + +/// Build the resource attributes shared by gateway and compute-driver providers. +pub fn gateway_resource_attributes( + gateway_name: Option<&str>, + compute_driver: Option<&str>, +) -> Vec { + let mut attributes = Vec::new(); + if let Some(name) = gateway_name.map(str::trim).filter(|name| !name.is_empty()) { + attributes.push(KeyValue::new("openshell.gateway.name", name.to_string())); + } + if let Some(driver) = compute_driver + .map(str::trim) + .filter(|driver| !driver.is_empty()) + { + attributes.push(KeyValue::new( + "openshell.gateway.compute_driver", + driver.to_string(), + )); + } + attributes +} + +/// Mark `span` as failed. +/// +/// The field must be declared on the span at creation because `tracing` drops +/// records for fields a span does not have. +pub fn mark_error(span: &tracing::Span) { + span.record("otel.status_code", "ERROR"); +} + +/// Marks the current span when an instrumented operation returns an error. +pub fn record_error_result(result: Result) -> Result { + if result.is_err() { + mark_error(&tracing::Span::current()); + } + result +} + +/// Marks an instrumented function's span when it exits before returning success. +/// +/// Create the guard at the start of the instrumented function and return the +/// successful result through [`Self::finish`]. An early `?` or error return +/// drops the unfinished guard and marks the captured span as failed. +#[must_use] +pub struct ErrorStatusGuard { + span: tracing::Span, + finished: bool, +} + +impl ErrorStatusGuard { + /// Captures the current instrumented span. + pub fn current() -> Self { + Self { + span: tracing::Span::current(), + finished: false, + } + } + + /// Returns `result`, marking this guard complete when it is successful. + pub fn finish(mut self, result: Result) -> Result { + self.finished = result.is_ok(); + result + } +} + +impl Drop for ErrorStatusGuard { + fn drop(&mut self) { + if !self.finished { + mark_error(&self.span); + } + } +} + +/// How a process chooses its OpenTelemetry `service.name`. +#[derive(Debug, Clone, Copy)] +pub enum ServiceName<'a> { + /// Always use this name, overriding `OTEL_SERVICE_NAME`. + Fixed(&'a str), + /// Use `OTEL_SERVICE_NAME` when set, otherwise use this default. + EnvironmentOr(&'a str), +} + +/// OTLP transport to a collector. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum Protocol { + #[default] + Grpc, + HttpProtobuf, +} + +/// Inputs for OTLP export. +#[derive(Debug, Clone)] +pub struct OtlpExportConfig<'a> { + pub endpoint: &'a str, + pub service_name: ServiceName<'a>, + pub service_version: Option<&'a str>, + pub resource_attributes: Vec, +} + +/// How exports reach a collector: the transport, request headers, and TLS. +#[derive(Clone, Default, PartialEq, Eq)] +pub struct CollectorTransport { + pub protocol: Protocol, + pub headers: http::HeaderMap, + pub tls: OtlpTls, +} + +impl std::fmt::Debug for CollectorTransport { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("CollectorTransport") + .field("protocol", &self.protocol) + .field("header_names", &self.headers.keys().collect::>()) + .field("tls", &self.tls) + .finish() + } +} + +/// Failure to set up OTLP export. +#[derive(Debug, thiserror::Error)] +pub enum SetupError { + #[error("OTLP endpoint is empty")] + EmptyEndpoint, + + #[error("invalid OTLP endpoint {endpoint:?}: {source}")] + InvalidEndpoint { + endpoint: String, + source: http::uri::InvalidUri, + }, + + #[error("failed to build the OTLP exporter: {0}")] + Exporter(#[from] opentelemetry_otlp::ExporterBuildError), + + #[error("invalid OTLP endpoint {endpoint:?}: {source}")] + InvalidSignalUrl { + endpoint: String, + source: http::Error, + }, + + #[error("OTLP TLS settings need an https:// endpoint, not {endpoint:?}")] + TlsWithoutHttps { endpoint: String }, + + #[error("invalid OTLP TLS settings: {0}")] + Tls(#[from] TlsError), + + #[error("invalid OTLP TLS files: {0}")] + TlsFiles(#[from] TlsFileError), + + #[error("OTLP/HTTP export needs a tokio runtime")] + NoRuntime, +} + +fn resource_attributes(config: &OtlpExportConfig<'_>) -> Vec { + let mut attributes = config.resource_attributes.clone(); + if let Some(version) = config + .service_version + .map(str::trim) + .filter(|version| !version.is_empty()) + { + attributes.push(KeyValue::new("service.version", version.to_string())); + } + attributes +} + +/// Build the OpenTelemetry resource for an export configuration. +#[must_use] +pub fn resource_for(config: &OtlpExportConfig<'_>) -> Resource { + let attributes = resource_attributes(config); + match config.service_name { + ServiceName::Fixed(name) => Resource::builder() + .with_service_name(name.trim().to_string()) + .with_attributes(attributes) + .build(), + ServiceName::EnvironmentOr(default) => { + let detected = Resource::builder() + .with_attributes(attributes.clone()) + .build(); + if detected + .get(&opentelemetry::Key::from_static_str("service.name")) + .is_some_and(|value| !value.to_string().starts_with(SDK_UNKNOWN_SERVICE_PREFIX)) + { + detected + } else { + Resource::builder() + .with_service_name(default.trim().to_string()) + .with_attributes(attributes) + .build() + } + } + } +} + +/// Trim and validate an OTLP endpoint before handing it to an exporter. +/// +/// Every exporter that reads an operator-supplied endpoint uses this, so a +/// value accepted for one signal is accepted for all of them. +pub fn validated_endpoint(endpoint: &str) -> Result<(&str, http::Uri), SetupError> { + let endpoint = endpoint.trim(); + if endpoint.is_empty() { + return Err(SetupError::EmptyEndpoint); + } + let uri = endpoint + .parse::() + .map_err(|source| SetupError::InvalidEndpoint { + endpoint: endpoint.to_string(), + source, + })?; + Ok((endpoint, uri)) +} + +/// A validated OTLP collector endpoint and the resource that every signal +/// exported to it describes. +#[derive(Clone)] +pub struct OtlpTarget { + pub(crate) endpoint: String, + pub(crate) tls: bool, + pub(crate) protocol: Protocol, + pub(crate) headers: http::HeaderMap, + pub(crate) tls_settings: OtlpTls, + pub(crate) resource: Resource, +} + +impl std::fmt::Debug for OtlpTarget { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("OtlpTarget") + .field("endpoint", &self.endpoint) + .field("tls", &self.tls) + .field("protocol", &self.protocol) + .field("header_names", &self.headers.keys().collect::>()) + .field("tls_settings", &self.tls_settings) + .field("resource", &self.resource) + .finish() + } +} + +impl OtlpTarget { + /// Validate the endpoint and build the resource. + pub fn new(config: &OtlpExportConfig<'_>) -> Result { + let (endpoint, uri) = validated_endpoint(config.endpoint)?; + Ok(Self { + endpoint: endpoint.to_string(), + tls: uri.scheme() == Some(&http::uri::Scheme::HTTPS), + protocol: Protocol::Grpc, + headers: http::HeaderMap::new(), + tls_settings: OtlpTls::default(), + resource: resource_for(config), + }) + } + + /// Export over `protocol` instead of gRPC. + #[must_use] + pub fn with_protocol(mut self, protocol: Protocol) -> Self { + self.protocol = protocol; + self + } + + /// Send `headers` with every export request. Headers from the process + /// environment (`OTEL_EXPORTER_OTLP_*HEADERS`) still apply and replace + /// any of the same name. + #[must_use] + pub fn with_headers(mut self, headers: http::HeaderMap) -> Self { + self.headers = headers; + self + } + + /// Reach the collector the way `transport` describes. + #[must_use] + pub fn with_transport(self, transport: &CollectorTransport) -> Self { + self.with_protocol(transport.protocol) + .with_headers(transport.headers.clone()) + .with_tls(transport.tls.clone()) + } + + /// Verify an `https://` collector and authenticate to it with `tls`. + #[must_use] + pub fn with_tls(mut self, tls: OtlpTls) -> Self { + self.tls_settings = tls; + self + } + + /// The URL of `signal_path` under the endpoint, for OTLP/HTTP export. + pub(crate) fn http_signal_url(&self, signal_path: &str) -> Result { + signal_url(&self.endpoint, signal_path) + .map(|url| url.to_string()) + .map_err(|source| SetupError::InvalidSignalUrl { + endpoint: self.endpoint.clone(), + source, + }) + } + + /// Point an exporter builder at this target, over TLS for `https://`. + pub(crate) fn configure(&self, builder: B) -> B + where + B: WithExportConfig + WithTonicConfig, + { + let builder = builder + .with_endpoint(self.endpoint.clone()) + .with_metadata(MetadataMap::from_headers(self.headers.clone())); + if self.tls { + builder.with_tls_config(self.tls_settings.grpc_config()) + } else { + builder + } + } + + /// Point an OTLP/HTTP exporter builder at `signal_path` under this + /// target. + pub(crate) fn configure_http( + &self, + builder: B, + signal_path: &str, + timeout_var: &str, + ) -> Result + where + B: WithExportConfig + WithHttpConfig, + { + // The SDK percent-decodes these values again. + let headers = self + .headers + .iter() + .filter_map(|(name, value)| { + Some((name.to_string(), value.to_str().ok()?.replace('%', "%25"))) + }) + .collect(); + Ok(builder + .with_protocol(opentelemetry_otlp::Protocol::HttpBinary) + .with_endpoint(self.http_signal_url(signal_path)?) + .with_http_client(OtlpHttpClient::new( + self.tls.then_some(&self.tls_settings), + transport::export_timeout(timeout_var), + )?) + .with_headers(headers)) + } + + #[must_use] + pub fn endpoint(&self) -> &str { + &self.endpoint + } + + #[must_use] + pub fn resource(&self) -> &Resource { + &self.resource + } + + /// Fails when TLS settings are given for a plaintext endpoint. + pub(crate) fn check_tls(&self) -> Result<(), SetupError> { + if self.tls || self.tls_settings.is_default() { + Ok(()) + } else { + Err(SetupError::TlsWithoutHttps { + endpoint: self.endpoint.clone(), + }) + } + } + + /// Build a trace provider that exports spans to this target in batches. + pub fn tracer_provider(&self) -> Result { + self.check_tls()?; + let exporter = match self.protocol { + Protocol::Grpc => self + .configure(SpanExporter::builder().with_tonic()) + .build()?, + Protocol::HttpProtobuf => self + .configure_http( + SpanExporter::builder().with_http(), + "v1/traces", + "OTEL_EXPORTER_OTLP_TRACES_TIMEOUT", + )? + .build()?, + }; + Ok(SdkTracerProvider::builder() + .with_batch_exporter(exporter) + .with_resource(self.resource.clone()) + .build()) + } +} + +/// Build an OTLP trace provider. +pub fn build_provider(config: &OtlpExportConfig<'_>) -> Result { + OtlpTarget::new(config)?.tracer_provider() +} + +/// Build the provider for an optional OTLP configuration. +/// +/// Telemetry setup failures disable export and remain available for the caller +/// to report after its tracing subscriber is installed. +#[must_use] +pub fn provider_for( + config: Option>, +) -> (Option, Option) { + match config.as_ref().map(build_provider) { + None => (None, None), + Some(Ok(provider)) => (Some(provider), None), + Some(Err(error)) => (None, Some(error)), + } +} + +/// Filtered OpenTelemetry layer returned by [`layer`]. +pub type OtlpLayer = tracing_subscriber::filter::Filtered< + OpenTelemetryLayer, + tracing_subscriber::filter::FilterFn, + S, +>; + +pub type TargetOtlpLayer = + tracing_subscriber::filter::Filtered, TargetPrefixFilter, S>; + +#[derive(Debug, Clone)] +pub struct TargetPrefixFilter { + prefixes: Vec<&'static str>, + include: bool, +} + +impl Filter for TargetPrefixFilter { + fn enabled(&self, metadata: &tracing::Metadata<'_>, _ctx: &Context<'_, S>) -> bool { + let matches_prefix = self + .prefixes + .iter() + .any(|prefix| metadata.target().starts_with(prefix)); + metadata.is_span() + && !metadata.target().starts_with("opentelemetry") + && (matches_prefix == self.include) + } +} + +/// Build a tracing layer that exports spans and excludes exporter callsites. +pub fn layer(provider: &SdkTracerProvider, instrumentation_scope: &'static str) -> OtlpLayer +where + S: Subscriber + for<'span> LookupSpan<'span>, +{ + tracing_opentelemetry::layer() + .with_tracer(provider.tracer(instrumentation_scope)) + .with_filter(tracing_subscriber::filter::filter_fn(|metadata| { + metadata.is_span() && !metadata.target().starts_with("opentelemetry") + })) +} + +/// Build a tracing layer that exports only spans from `target_prefix`. +pub fn layer_for_target_prefix( + provider: &SdkTracerProvider, + instrumentation_scope: &'static str, + target_prefix: &'static str, +) -> TargetOtlpLayer +where + S: Subscriber + for<'span> LookupSpan<'span>, +{ + layer_for_target_prefixes(provider, instrumentation_scope, [target_prefix]) +} + +/// Build a tracing layer that exports only spans from `target_prefixes`. +pub fn layer_for_target_prefixes( + provider: &SdkTracerProvider, + instrumentation_scope: &'static str, + target_prefixes: impl IntoIterator, +) -> TargetOtlpLayer +where + S: Subscriber + for<'span> LookupSpan<'span>, +{ + tracing_opentelemetry::layer() + .with_tracer(provider.tracer(instrumentation_scope)) + .with_filter(TargetPrefixFilter { + prefixes: target_prefixes.into_iter().collect(), + include: true, + }) +} + +/// Build a tracing layer that excludes spans from `target_prefix`. +/// +/// `None` excludes no application spans. +pub fn layer_excluding_target_prefix( + provider: &SdkTracerProvider, + instrumentation_scope: &'static str, + target_prefix: Option<&'static str>, +) -> TargetOtlpLayer +where + S: Subscriber + for<'span> LookupSpan<'span>, +{ + layer_excluding_target_prefixes(provider, instrumentation_scope, target_prefix) +} + +/// Build a tracing layer that excludes spans from `target_prefixes`. +/// +/// An empty iterator excludes no application spans. +pub fn layer_excluding_target_prefixes( + provider: &SdkTracerProvider, + instrumentation_scope: &'static str, + target_prefixes: impl IntoIterator, +) -> TargetOtlpLayer +where + S: Subscriber + for<'span> LookupSpan<'span>, +{ + tracing_opentelemetry::layer() + .with_tracer(provider.tracer(instrumentation_scope)) + .with_filter(TargetPrefixFilter { + prefixes: target_prefixes.into_iter().collect(), + include: false, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::parse_otlp_headers; + + #[test] + fn supervisor_span_filter_raises_only_openshell_targets() { + let directives = |log_level: &str| supervisor_span_filter(log_level).to_string(); + assert_eq!(directives("warn"), "openshell=info,info"); + assert_eq!(directives("info"), "openshell=info,info"); + assert_eq!(directives("debug"), "openshell=debug,info"); + assert_eq!(directives("info,h2=debug"), "openshell=info,info"); + } + + #[test] + fn error_status_guard_marks_only_unfinished_results() { + use tracing_subscriber::layer::SubscriberExt as _; + + let _tracing_lock = crate::test_lock(); + let exporter = opentelemetry_sdk::trace::InMemorySpanExporterBuilder::new().build(); + let provider = SdkTracerProvider::builder() + .with_simple_exporter(exporter.clone()) + .build(); + let subscriber = tracing_subscriber::registry().with(layer(&provider, "guard-test")); + + tracing::subscriber::with_default(subscriber, || { + let failed = + tracing::info_span!("failed-operation", otel.status_code = tracing::field::Empty); + { + let _entered = failed.enter(); + drop(ErrorStatusGuard::current()); + } + drop(failed); + + let succeeded = tracing::info_span!( + "successful-operation", + otel.status_code = tracing::field::Empty + ); + { + let _entered = succeeded.enter(); + ErrorStatusGuard::current().finish(Ok::<_, ()>(())).unwrap(); + } + drop(succeeded); + }); + + let spans = exporter.get_finished_spans().unwrap(); + let failed = spans + .iter() + .find(|span| span.name == "failed-operation") + .unwrap(); + assert!(matches!( + failed.status, + opentelemetry::trace::Status::Error { .. } + )); + let succeeded = spans + .iter() + .find(|span| span.name == "successful-operation") + .unwrap(); + assert_eq!(succeeded.status, opentelemetry::trace::Status::Unset); + } + + #[test] + fn target_scoped_layers_partition_in_process_driver_spans() { + use tracing_subscriber::layer::SubscriberExt as _; + + let _tracing_lock = crate::test_lock(); + let gateway_exporter = opentelemetry_sdk::trace::InMemorySpanExporterBuilder::new().build(); + let gateway_provider = SdkTracerProvider::builder() + .with_simple_exporter(gateway_exporter.clone()) + .build(); + let driver_exporter = opentelemetry_sdk::trace::InMemorySpanExporterBuilder::new().build(); + let driver_provider = SdkTracerProvider::builder() + .with_simple_exporter(driver_exporter.clone()) + .build(); + let subscriber = tracing_subscriber::registry() + .with(layer_excluding_target_prefix( + &gateway_provider, + "gateway", + Some("driver_target"), + )) + .with(layer_for_target_prefix( + &driver_provider, + "driver", + "driver_target", + )); + + tracing::subscriber::with_default(subscriber, || { + let gateway = tracing::info_span!(target: "gateway_target", "gateway.span"); + let _entered = gateway.enter(); + drop(tracing::info_span!(target: "driver_target::operation", "driver.span")); + }); + + gateway_provider.force_flush().unwrap(); + driver_provider.force_flush().unwrap(); + let gateway_spans = gateway_exporter.get_finished_spans().unwrap(); + let driver_spans = driver_exporter.get_finished_spans().unwrap(); + assert_eq!( + gateway_spans + .iter() + .map(|span| span.name.as_ref()) + .collect::>(), + ["gateway.span"] + ); + assert_eq!( + driver_spans + .iter() + .map(|span| span.name.as_ref()) + .collect::>(), + ["driver.span"] + ); + assert_eq!( + driver_spans[0].span_context.trace_id(), + gateway_spans[0].span_context.trace_id() + ); + assert_eq!( + driver_spans[0].parent_span_id, + gateway_spans[0].span_context.span_id() + ); + } + + #[test] + fn excluding_no_target_prefix_exports_all_application_spans() { + use tracing_subscriber::layer::SubscriberExt as _; + + let _tracing_lock = crate::test_lock(); + let exporter = opentelemetry_sdk::trace::InMemorySpanExporterBuilder::new().build(); + let provider = SdkTracerProvider::builder() + .with_simple_exporter(exporter.clone()) + .build(); + let subscriber = tracing_subscriber::registry() + .with(layer_excluding_target_prefix(&provider, "gateway", None)); + + tracing::subscriber::with_default(subscriber, || { + drop(tracing::info_span!(target: "\0driver_target", "nul-prefixed.span")); + drop(tracing::info_span!(target: "gateway_target", "gateway.span")); + }); + + provider.force_flush().unwrap(); + let spans = exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 2); + assert!(spans.iter().any(|span| span.name == "nul-prefixed.span")); + assert!(spans.iter().any(|span| span.name == "gateway.span")); + } + + #[test] + fn resource_uses_fixed_service_identity_and_custom_attributes() { + let resource = resource_for(&OtlpExportConfig { + endpoint: "http://127.0.0.1:4317", + service_name: ServiceName::Fixed("openshell-driver-vm"), + service_version: Some("1.2.3"), + resource_attributes: vec![KeyValue::new("openshell.gateway.name", "vm-dev")], + }); + + assert_eq!( + resource + .get(&opentelemetry::Key::from_static_str("service.name")) + .map(|value| value.to_string()), + Some("openshell-driver-vm".to_string()) + ); + assert_eq!( + resource + .get(&opentelemetry::Key::from_static_str("service.version")) + .map(|value| value.to_string()), + Some("1.2.3".to_string()) + ); + assert_eq!( + resource + .get(&opentelemetry::Key::from_static_str( + "openshell.gateway.name", + )) + .map(|value| value.to_string()), + Some("vm-dev".to_string()) + ); + } + + #[tokio::test] + async fn tls_settings_need_an_https_endpoint() { + let pki = openshell_otel_test_support::TestPki::generate(); + let tls = OtlpTls::from_pem(Some(pki.ca_pem.as_bytes()), None).unwrap(); + for protocol in [Protocol::Grpc, Protocol::HttpProtobuf] { + let target = |endpoint| { + OtlpTarget::new(&OtlpExportConfig { + endpoint, + service_name: ServiceName::Fixed("test-service"), + service_version: None, + resource_attributes: Vec::new(), + }) + .unwrap() + .with_protocol(protocol) + .with_tls(tls.clone()) + }; + assert!(matches!( + target("http://collector:4317").tracer_provider(), + Err(SetupError::TlsWithoutHttps { .. }) + )); + assert!(matches!( + target("http://collector:4317").log_exporter(), + Err(SetupError::TlsWithoutHttps { .. }) + )); + assert!(target("https://collector:4317").tracer_provider().is_ok()); + } + } + + #[test] + fn debug_output_names_headers_without_their_values() { + let target = OtlpTarget::new(&OtlpExportConfig { + endpoint: "http://collector:4317", + service_name: ServiceName::Fixed("test-service"), + service_version: None, + resource_attributes: Vec::new(), + }) + .unwrap() + .with_headers(parse_otlp_headers("authorization=Bearer%20secret-token")); + + let debug = format!("{target:?}"); + assert!(debug.contains("authorization"), "{debug}"); + assert!(!debug.contains("secret-token"), "{debug}"); + } + + #[tokio::test(flavor = "multi_thread")] + async fn trace_provider_sends_headers_as_grpc_metadata() { + crate::test_environment::Environment::new() + .remove("OTEL_EXPORTER_OTLP_HEADERS") + .remove("OTEL_EXPORTER_OTLP_TRACES_HEADERS") + .remove("OTEL_EXPORTER_OTLP_LOGS_HEADERS") + .run_async(async { + use opentelemetry::trace::Tracer as _; + + let collector = openshell_otel_test_support::OtlpTestServer::start().await; + let provider = OtlpTarget::new(&OtlpExportConfig { + endpoint: collector.endpoint(), + service_name: ServiceName::Fixed("test-service"), + service_version: None, + resource_attributes: Vec::new(), + }) + .unwrap() + .with_headers(parse_otlp_headers("authorization=Bearer%20token")) + .tracer_provider() + .unwrap(); + provider.tracer("test").in_span("grpc-span", |_| {}); + let flush = tokio::task::spawn_blocking(move || provider.force_flush()); + + collector.wait_for_export().await; + flush.await.unwrap().expect("flush succeeds"); + assert_eq!(collector.received().trace_authorizations, ["Bearer token"]); + }) + .await; + } + + #[tokio::test] + async fn malformed_endpoint_disables_export_with_a_reportable_error() { + let (provider, error) = provider_for(Some(OtlpExportConfig { + endpoint: "definitely not a url", + service_name: ServiceName::Fixed("test-service"), + service_version: None, + resource_attributes: Vec::new(), + })); + + assert!(provider.is_none()); + assert!(matches!(error, Some(SetupError::InvalidEndpoint { .. }))); + } +} diff --git a/crates/openshell-otel/tests/ocsf_correlation.rs b/crates/openshell-otel/tests/ocsf_correlation.rs new file mode 100644 index 0000000000..acf3f1a334 --- /dev/null +++ b/crates/openshell-otel/tests/ocsf_correlation.rs @@ -0,0 +1,228 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::{Arc, LazyLock, Mutex}; + +use openshell_ocsf::{ + ApiActivityBuilder, AuthenticationBuilder, Endpoint, HttpActivityBuilder, HttpRequest, + NetworkActivityBuilder, OcsfEvent, OcsfJsonlLayer, Service, Url, User, ocsf_emit, +}; +use opentelemetry_sdk::trace::{InMemorySpanExporterBuilder, Sampler, SdkTracerProvider}; +use tracing_subscriber::prelude::*; + +static SETUP: LazyLock<()> = LazyLock::new(|| { + tracing::subscriber::set_global_default(tracing_subscriber::registry()).unwrap(); + assert!(openshell_ocsf::register_trace_correlation_extractor( + openshell_otel::current_ocsf_trace_correlation, + )); +}); + +#[derive(Clone)] +struct Writer(Arc>>); +impl std::io::Write for Writer { + fn write(&mut self, bytes: &[u8]) -> std::io::Result { + self.0.lock().unwrap().extend_from_slice(bytes); + Ok(bytes.len()) + } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +fn capture( + sampler: Sampler, + active: bool, +) -> (serde_json::Value, Vec) { + capture_route(sampler, active, false) +} + +fn http_event() -> OcsfEvent { + HttpActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .http_request(HttpRequest::new( + "GET", + Url::new("https", "example.com", "/", 443), + )) + .build() +} + +fn capture_event(build: fn() -> OcsfEvent) -> serde_json::Value { + capture_context(Sampler::AlwaysOn, true, false, None, build).0 +} + +fn capture_route( + sampler: Sampler, + active: bool, + routed: bool, +) -> (serde_json::Value, Vec) { + capture_context(sampler, active, routed, None, http_event) +} + +fn capture_context( + sampler: Sampler, + active: bool, + routed: bool, + remote_parent: Option, + build: fn() -> OcsfEvent, +) -> (serde_json::Value, Vec) { + LazyLock::force(&SETUP); + let exporter = InMemorySpanExporterBuilder::new().build(); + let provider = SdkTracerProvider::builder() + .with_sampler(sampler) + .with_simple_exporter(exporter.clone()) + .build(); + let bytes = Arc::new(Mutex::new(vec![])); + let subscriber = tracing_subscriber::registry() + .with(openshell_otel::layer(&provider, "ocsf-test")) + .with(OcsfJsonlLayer::new(Writer(bytes.clone()))); + tracing::subscriber::with_default(subscriber, || { + let emit = || { + if routed { + openshell_ocsf::emit_ocsf_event_routed("test-sandbox", build()); + } else { + ocsf_emit!(build()); + } + }; + if active { + let span = tracing::info_span!("sampled-operation"); + if let Some(parent) = remote_parent { + use opentelemetry::trace::TraceContextExt as _; + use tracing_opentelemetry::OpenTelemetrySpanExt as _; + span.set_parent(opentelemetry::Context::new().with_remote_span_context(parent)) + .unwrap(); + } + span.in_scope(emit); + } else { + emit(); + } + }); + let value = serde_json::from_slice(&bytes.lock().unwrap()).unwrap(); + provider.force_flush().unwrap(); + let spans = exporter.get_finished_spans().unwrap(); + provider.shutdown().unwrap(); + (value, spans) +} + +#[test] +fn api_activity_correlation_can_inherit_sampled_or_unsampled_caller_context() { + use opentelemetry::trace::{SpanContext, SpanId, TraceFlags, TraceId, TraceState}; + let uid = "0af7651916cd43dd8448eb211c80319c"; + for sampled in [true, false] { + let parent = SpanContext::new( + TraceId::from_hex(uid).unwrap(), + SpanId::from_hex("b7ad6b7169203331").unwrap(), + if sampled { + TraceFlags::SAMPLED + } else { + TraceFlags::default() + }, + true, + TraceState::default(), + ); + let (event, spans) = capture_context( + Sampler::ParentBased(Box::new(Sampler::AlwaysOn)), + true, + false, + Some(parent), + || ApiActivityBuilder::new(openshell_ocsf::ctx::ctx(), "CreateSandbox").build(), + ); + assert_eq!(event["class_uid"], 6003); + if sampled { + assert_eq!(event["trace"]["uid"], uid); + assert_eq!(spans.len(), 1); + assert_eq!(spans[0].span_context.trace_id().to_string(), uid); + } else { + assert_no_trace(&event); + assert!(spans.is_empty()); + } + } +} + +fn assert_no_trace(value: &serde_json::Value) { + assert!(value.get("trace").is_none()); + assert!( + !value["metadata"]["profiles"] + .as_array() + .unwrap() + .iter() + .any(|p| p == "trace") + ); +} + +#[test] +fn classes_without_the_trace_profile_stay_uncorrelated() { + assert_no_trace(&capture_event(|| { + NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .dst_endpoint(Endpoint::from_domain("example.com", 443)) + .build() + })); + assert_no_trace(&capture_event(|| { + AuthenticationBuilder::new( + openshell_ocsf::ctx::ctx(), + User::unidentified(), + Service::new("openshell-gateway"), + ) + .build() + })); +} + +#[test] +fn sampled_active_span_correlates_to_exported_w3c_trace() { + let (value, spans) = capture(Sampler::AlwaysOn, true); + let uid = value["trace"]["uid"].as_str().unwrap(); + assert_eq!(uid.len(), 32); + assert!( + uid.bytes() + .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) + ); + assert_eq!(uid, spans[0].span_context.trace_id().to_string()); + assert_eq!( + value["metadata"]["profiles"] + .as_array() + .unwrap() + .iter() + .filter(|p| *p == "trace") + .count(), + 1 + ); + assert_eq!(value["trace"].as_object().unwrap().len(), 1); +} + +#[test] +fn no_active_context_omits_trace_and_profile() { + let (value, spans) = capture(Sampler::AlwaysOn, false); + assert_no_trace(&value); + assert!(spans.is_empty()); +} + +#[test] +fn routed_emission_also_enriches_from_sampled_context() { + let (value, spans) = capture_route(Sampler::AlwaysOn, true, true); + assert_eq!( + value["trace"]["uid"], + spans[0].span_context.trace_id().to_string() + ); + assert!( + value["metadata"]["profiles"] + .as_array() + .unwrap() + .iter() + .any(|p| p == "trace") + ); +} + +#[test] +fn unsampled_active_context_omits_trace_and_profile() { + let (value, spans) = capture(Sampler::AlwaysOff, true); + assert_no_trace(&value); + assert!(spans.is_empty()); +} + +#[test] +fn active_tracing_span_without_otel_context_is_invalid() { + LazyLock::force(&SETUP); + tracing::subscriber::with_default(tracing_subscriber::registry(), || { + tracing::info_span!("no-otel-layer").in_scope(|| { + assert!(openshell_otel::current_ocsf_trace_correlation().is_none()); + }); + }); +} diff --git a/crates/openshell-policy-schema/src/lib.rs b/crates/openshell-policy-schema/src/lib.rs index 656433b794..422ac13440 100644 --- a/crates/openshell-policy-schema/src/lib.rs +++ b/crates/openshell-policy-schema/src/lib.rs @@ -285,7 +285,7 @@ pub struct NetworkEndpoint { /// Defaults to false (strict). #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub allow_encoded_slash: bool, - /// When true, client-to-server WebSocket text messages on this REST + /// When true, client-to-server WebSocket text messages on a `protocol: websocket` /// endpoint rewrite credential placeholders after an allowed 101 upgrade. /// Defaults to false. #[serde(default, skip_serializing_if = "std::ops::Not::not")] diff --git a/crates/openshell-policy/src/l7_validate.rs b/crates/openshell-policy/src/l7_validate.rs index ac995fa65b..74be95cc82 100644 --- a/crates/openshell-policy/src/l7_validate.rs +++ b/crates/openshell-policy/src/l7_validate.rs @@ -223,6 +223,9 @@ pub struct L7EndpointFields<'a> { /// `true` when the endpoint has a non-empty rules list. pub has_rules: bool, + /// Whether WebSocket credential rewriting was enabled. + pub websocket_credential_rewrite: bool, + /// `true` when the endpoint has a non-empty `deny_rules` list. pub has_deny_rules: bool, @@ -244,6 +247,9 @@ pub fn validate_l7_endpoint_semantics(ep: &L7EndpointFields<'_>) -> Vec let mut errors = Vec::new(); let protocol = ep.protocol; let l7_protocol = L7Protocol::parse(protocol); + if matches!(l7_protocol, Some(L7Protocol::Rest)) && ep.websocket_credential_rewrite { + errors.push("websocket_credential_rewrite is not supported for protocol rest; use protocol: websocket with explicit message rules".to_string()); + } let explicit_tcp = is_explicit_tcp_protocol(protocol); let jsonrpc_family = l7_protocol.is_some_and(L7Protocol::is_jsonrpc_family); let is_mcp = matches!(l7_protocol, Some(L7Protocol::Mcp)); @@ -335,6 +341,7 @@ mod tests { fn valid_rest_endpoint() -> L7EndpointFields<'static> { L7EndpointFields { + websocket_credential_rewrite: false, protocol: "rest", access: "read-only", has_rules: false, @@ -344,6 +351,27 @@ mod tests { } } + #[test] + fn rest_rejects_websocket_credential_rewrite_with_migration_hint() { + let mut endpoint = valid_rest_endpoint(); + endpoint.websocket_credential_rewrite = true; + for protocol in ["rest", "REST", "Rest"] { + endpoint.protocol = protocol; + let errors = validate_l7_endpoint_semantics(&endpoint); + assert!( + errors.iter().any(|e| e.contains("use protocol: websocket")), + "{protocol}: {errors:?}" + ); + } + for protocol in ["websocket", "WEBSOCKET", "WebSocket"] { + endpoint.protocol = protocol; + assert!( + validate_l7_endpoint_semantics(&endpoint).is_empty(), + "{protocol}" + ); + } + } + #[test] fn valid_endpoint_produces_no_errors() { let errors = validate_l7_endpoint_semantics(&valid_rest_endpoint()); @@ -383,6 +411,7 @@ mod tests { #[test] fn rejects_unknown_protocol() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "ftp", access: "", has_rules: false, @@ -397,6 +426,7 @@ mod tests { #[test] fn rejects_rules_and_access_together() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "rest", access: "full", has_rules: true, @@ -411,6 +441,7 @@ mod tests { #[test] fn rejects_jsonrpc_with_access_presets() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "json-rpc", access: "full", has_rules: false, @@ -429,6 +460,7 @@ mod tests { #[test] fn rejects_mcp_with_access_presets() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "mcp", access: "full", has_rules: false, @@ -447,6 +479,7 @@ mod tests { #[test] fn rejects_jsonrpc_without_rules() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "json-rpc", access: "", has_rules: false, @@ -461,6 +494,7 @@ mod tests { #[test] fn rejects_protocol_without_rules_or_access() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "rest", access: "", has_rules: false, @@ -479,6 +513,7 @@ mod tests { #[test] fn rejects_mcp_without_rules_when_allow_all_false() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "mcp", access: "", has_rules: false, @@ -493,6 +528,7 @@ mod tests { #[test] fn accepts_mcp_with_allow_all_true() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "mcp", access: "", has_rules: false, @@ -507,6 +543,7 @@ mod tests { #[test] fn rejects_rules_that_deny_all() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "rest", access: "", has_rules: true, @@ -521,6 +558,7 @@ mod tests { #[test] fn mcp_allow_all_with_empty_allow_not_rejected() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "mcp", access: "", has_rules: true, @@ -538,6 +576,7 @@ mod tests { #[test] fn rejects_deny_rules_without_protocol() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "", access: "", has_rules: false, @@ -556,6 +595,7 @@ mod tests { #[test] fn rejects_deny_rules_without_allow_base() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "rest", access: "", has_rules: false, @@ -574,6 +614,7 @@ mod tests { #[test] fn no_protocol_no_errors() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "", access: "", has_rules: false, @@ -588,6 +629,7 @@ mod tests { #[test] fn explicit_tcp_is_valid_without_l7_fields() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "tcp", access: "", has_rules: false, @@ -604,6 +646,7 @@ mod tests { #[test] fn explicit_tcp_rejects_l7_fields() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "tcp", access: "full", has_rules: false, @@ -648,6 +691,7 @@ mod tests { #[test] fn jsonrpc_with_access_emits_single_diagnostic() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "json-rpc", access: "full", has_rules: false, @@ -669,6 +713,7 @@ mod tests { #[test] fn jsonrpc_without_rules_or_access_emits_single_diagnostic() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "json-rpc", access: "", has_rules: false, diff --git a/crates/openshell-policy/src/lib.rs b/crates/openshell-policy/src/lib.rs index bad8532cdf..a84bca7868 100644 --- a/crates/openshell-policy/src/lib.rs +++ b/crates/openshell-policy/src/lib.rs @@ -1562,6 +1562,7 @@ fn validate_sandbox_policy_with_mcp_presence( }) }); let fields = L7EndpointFields { + websocket_credential_rewrite: ep.websocket_credential_rewrite, protocol: &ep.protocol, access, has_rules: !ep.rules.is_empty(), @@ -4786,6 +4787,18 @@ network_policies: ); } + #[test] + fn validate_rest_rejects_websocket_credential_rewrite() { + for protocol in ["rest", "REST", "Rest"] { + let yaml = "version: 1\nnetwork_policies:\n api:\n endpoints:\n - { host: api.example.com, port: 443, protocol: rest, access: read-only, websocket_credential_rewrite: true }\n"; + let yaml = yaml.replace("protocol: rest", &format!("protocol: {protocol}")); + let policy = parse_sandbox_policy(&yaml).unwrap(); + let errors = validate_sandbox_policy(&policy) + .expect_err("REST cannot rewrite WebSocket messages"); + assert!(format!("{errors:?}").contains("use protocol: websocket")); + } + } + #[test] fn round_trip_preserves_websocket_credential_rewrite() { let yaml = r" @@ -4796,7 +4809,7 @@ network_policies: endpoints: - host: gateway.example.com port: 443 - protocol: rest + protocol: websocket enforcement: enforce access: full websocket_credential_rewrite: true @@ -4808,7 +4821,7 @@ network_policies: let proto2 = parse_sandbox_policy(&yaml_out).expect("re-parse failed"); let ep = &proto2.network_policies["discord_gateway"].endpoints[0]; - assert_eq!(ep.protocol, "rest"); + assert_eq!(ep.protocol, "websocket"); assert!(ep.websocket_credential_rewrite); assert!(yaml_out.contains("websocket_credential_rewrite: true")); } diff --git a/crates/openshell-policy/src/merge.rs b/crates/openshell-policy/src/merge.rs index 4639af6123..c010e6a723 100644 --- a/crates/openshell-policy/src/merge.rs +++ b/crates/openshell-policy/src/merge.rs @@ -3818,8 +3818,8 @@ mod tests { /// agent is told its approved rule never landed. #[test] fn merged_policy_covers_the_rule_it_merged() { - // Every field `merge_endpoint` widens rather than replaces, set on the - // existing endpoint and absent from the proposal. + // REST-compatible fields that `merge_endpoint` widens rather than + // replaces, set on the existing endpoint and absent from the proposal. let existing_endpoint = NetworkEndpoint { protocol: "rest".to_string(), rules: vec![rest_rule("GET", "/health")], @@ -3830,7 +3830,6 @@ mod tests { }], allowed_ips: vec!["10.0.0.1".to_string()], allow_encoded_slash: true, - websocket_credential_rewrite: true, request_body_credential_rewrite: true, advisor_proposed: true, ..endpoint("api.example.com", 443) diff --git a/crates/openshell-providers/src/profiles.rs b/crates/openshell-providers/src/profiles.rs index 3adfc9c278..b56dc2c7e4 100644 --- a/crates/openshell-providers/src/profiles.rs +++ b/crates/openshell-providers/src/profiles.rs @@ -2759,6 +2759,7 @@ pub fn validate_profile_set( } let l7_fields = L7EndpointFields { + websocket_credential_rewrite: endpoint.websocket_credential_rewrite, protocol: &endpoint.protocol, access: &endpoint.access, has_rules: endpoint.rules.as_ref().is_some_and(|r| !r.is_empty()), @@ -6330,6 +6331,42 @@ binaries: ["", /usr/bin/broken] assert!(messages.contains(&"binary path must not be empty")); } + #[test] + fn validate_profile_rejects_rest_websocket_credential_rewrite() { + for protocol in ["rest", "REST", "Rest"] { + let profile = parse_profile_yaml( + r" +id: rest-rewrite +display_name: REST rewrite +credentials: + - name: api_key + env_vars: [API_KEY] + auth_style: bearer + header_name: authorization +discovery: + credentials: [api_key] +endpoints: + - host: api.example.com + port: 443 + protocol: rest + access: read-only + websocket_credential_rewrite: true +binaries: + - /usr/bin/curl +" + .replace("protocol: rest", &format!("protocol: {protocol}")) + .as_str(), + ) + .unwrap(); + let diagnostics = validate_profile_set(&[("profile.yaml".to_string(), profile)]); + assert!( + diagnostics + .iter() + .any(|d| d.message.contains("use protocol: websocket")) + ); + } + } + #[test] fn validate_profile_set_rejects_noncanonical_profile_ids() { let profiles = [ diff --git a/crates/openshell-sandbox/README.md b/crates/openshell-sandbox/README.md index 72d01b73c7..e365721b8e 100644 --- a/crates/openshell-sandbox/README.md +++ b/crates/openshell-sandbox/README.md @@ -10,6 +10,13 @@ the lower-level mechanisms used by that orchestration and the boundary server. Unsafe kernel ABI, process-launch, and descriptor-ownership operations stay in this runtime crate behind safe APIs where their contracts can be enforced. +Landlock policy anchors are resolved relative to open directory descriptors. +Symlinks are followed only through ancestors that the workload neither owns nor +can write, preserving system aliases while rejecting mutable workspace links. +The rule uses the resolved descriptor directly. Unsafe resolution and an empty +effective allowlist fail the launch independently of Landlock compatibility; +they cannot trigger the best-effort fallback to baseline-only restrictions. + The network broker retains source socket descriptors only while preparing a workload endpoint. A successful connection or `listen` releases that reference; the kernel then tracks the lifetime through workload-owned descriptors. Native diff --git a/crates/openshell-sandbox/src/boundary_server.rs b/crates/openshell-sandbox/src/boundary_server.rs index 2c7b957707..a01f3b797e 100644 --- a/crates/openshell-sandbox/src/boundary_server.rs +++ b/crates/openshell-sandbox/src/boundary_server.rs @@ -97,6 +97,10 @@ mod linux { /// Bound the wait for a flush before acknowledging termination. A timed-out /// blocking task cannot be cancelled and may outlive the acknowledgement. const FILESYSTEM_FLUSH_TIMEOUT: Duration = Duration::from_secs(3); + /// Bound the process runtime's shutdown after the boundary stops serving. + /// A blocking task such as a supervisor's agent `Wait` can outlive the + /// termination that would end it, and the shutdown cannot cancel it. + const PROCESS_RUNTIME_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(2); const MAX_PENDING_HANDSHAKES: usize = 32; const MAX_CONTROL_CONNECTIONS: usize = 128; /// Bounds one runtime-initiated dial, including finding the supervisor @@ -297,7 +301,17 @@ mod linux { launcher, qualification, )?); - serve(&config.listener, runtime) + serve_then_stop(process_runtime, &config.listener, runtime) + } + + fn serve_then_stop( + process_runtime: tokio::runtime::Runtime, + config: &BoundaryListenerConfig, + runtime: Arc, + ) -> Result<(), String> { + let result = serve(config, runtime); + process_runtime.shutdown_timeout(PROCESS_RUNTIME_SHUTDOWN_TIMEOUT); + result } fn make_boundary_nondumpable() -> Result<(), String> { @@ -6232,6 +6246,242 @@ mod linux { ); } + /// Send one logical request on its own exchange and return the reply. + async fn exchange_request( + channel: tonic::transport::Channel, + handle: &str, + request: Request, + token: &str, + ) -> Response { + let request = RequestEnvelope::new(request).expect("encode request"); + let frames = tokio_stream::iter([ + openshell_sandbox_backend::isolation_protocol::exchange_frame(handle), + openshell_sandbox_backend::isolation_protocol::request_chunk(&request) + .expect("typed logical request"), + ]); + let mut body = SandboxProtocolClient::new(channel) + .exchange(bearer_request(frames, token)) + .await + .expect("exchange logical request") + .into_inner(); + let mut frame = Vec::new(); + while let Some(chunk) = body.message().await.expect("read gRPC response") { + frame.extend_from_slice(&chunk_data(chunk).unwrap()); + } + openshell_sandbox_backend::boundary_protocol::decode_frame::(&frame) + .expect("decode logical response") + .response + } + + /// A shared supervisor outlives the sandboxes it serves and keeps + /// their exchanges open, so SIGTERM must end the process without + /// waiting for the supervisor to close them. + #[test] + fn sigterm_exits_while_the_supervisor_holds_an_exchange_open() { + const CHILD_MARKER: &str = "OPENSHELL_TEST_BOUNDARY_SIGTERM_CHILD"; + const EXIT_BUDGET: Duration = Duration::from_secs(10); + if std::env::var_os(CHILD_MARKER).is_none() { + run_isolated( + "boundary_server::linux::tests::sigterm_exits_while_the_supervisor_holds_an_exchange_open", + CHILD_MARKER, + Duration::from_mins(1), + ); + return; + } + + let directory = tempfile::tempdir().expect("temporary directory"); + let (server_tls, client_tls) = stage_test_tls(directory.path(), "sigterm"); + let socket_path = directory.path().join("supervisor.sock"); + let listener = BoundaryListenerConfig::RuntimeInitiatedUnix { + socket_path: socket_path.clone(), + tls: server_tls, + }; + let supervisor_runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(1) + .enable_all() + .build() + .expect("supervisor runtime"); + let supervisor_listener = { + let _entered = supervisor_runtime.enter(); + tokio::net::UnixListener::bind(&socket_path).expect("bind supervisor socket") + }; + let process_runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("test process runtime"); + let (network_broker, workload_launcher) = test_network_broker(); + let (verification_key, token) = test_auth_material("sigterm"); + let boundary = { + let _entered = process_runtime.enter(); + Arc::new( + BoundaryRuntime::new( + BoundaryConfig { + boundary_id: "sigterm".to_string(), + generation: "generation-1".to_string(), + session_id: test_session_id(), + session_rotation: openshell_core::jwt::SessionRotation::new(1) + .expect("session rotation"), + auth_epoch: CredentialEpoch::new(1).expect("auth epoch"), + gateway_id: "test-gateway".to_string(), + verification_keys: vec![verification_key], + listener: listener.clone(), + resource_claims: std::collections::BTreeMap::new(), + resource_claim_files: std::collections::BTreeMap::new(), + workload_identity: test_workload_identity(), + provisioning_evidence: test_provisioning_evidence(), + child_env: std::collections::HashMap::new(), + }, + process_runtime.handle().clone(), + network_broker, + workload_launcher, + test_runtime_qualification(), + ) + .expect("test boundary runtime"), + ) + }; + let opening = test_open_request(&boundary.config); + let (signalled_sender, signalled) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let supervisor = std::panic::AssertUnwindSafe(|| { + supervisor_runtime.block_on(async move { + let (stream, _) = supervisor_listener.accept().await.expect("runtime dial"); + let server_name = + rustls::pki_types::ServerName::try_from(client_tls.server_name.clone()) + .expect("valid server name"); + let stream = tokio_rustls::TlsConnector::from(Arc::new( + test_client_config(&client_tls), + )) + .connect(server_name, stream) + .await + .expect("verify runtime TLS"); + let mut stream = Some(stream); + let channel = tonic::transport::Endpoint::from_static( + "http://boundary.openshell.internal", + ) + .connect_with_connector(tower::service_fn( + move |_: tonic::transport::Uri| { + let stream = stream.take(); + async move { + stream.map(hyper_util::rt::TokioIo::new).ok_or_else(|| { + io::Error::other("the runtime connection is in use") + }) + } + }, + )) + .await + .expect("gRPC over the runtime connection"); + let handle = test_open_boundary(channel.clone(), opening, &token).await; + let attach = Request::Attach { + supervisor_instance_id: test_supervisor_instance_id(), + policy: Box::new(takeover_test_policy()), + supervisor_takeover: None, + supervisor_recovery_timeout_secs: None, + }; + assert!(matches!( + exchange_request(channel.clone(), &handle, attach, &token).await, + Response::Attached { .. } + )); + assert!(matches!( + exchange_request(channel.clone(), &handle, Request::Confirm, &token) + .await, + Response::Confirmed { .. } + )); + let start = Request::StartAgent { + sandbox_id: "sigterm".to_string(), + spec: AgentSpecWire { + program: "/bin/sleep".to_string(), + args: vec!["300".to_string()], + workdir: None, + timeout_secs: 0, + interactive: false, + otlp_relay_active: false, + }, + ca_cert: None, + ca_bundle: None, + provider_env_revision: 0, + provider_env: std::collections::HashMap::new(), + provider_files: std::collections::HashMap::new(), + }; + let process_id = + match exchange_request(channel.clone(), &handle, start, &token).await { + Response::Started { process_id, .. } => process_id, + response => panic!("the agent did not start: {response:?}"), + }; + // The supervisor waits for the agent and for workload + // connections for as long as the sandbox runs. + let mut parked = Vec::new(); + for request in [Request::Wait { process_id }, Request::AcceptNetwork] { + let (sender, receiver) = tokio::sync::mpsc::channel(4); + sender + .send( + openshell_sandbox_backend::isolation_protocol::exchange_frame( + &handle, + ), + ) + .await + .expect("bind exchange"); + sender + .send( + openshell_sandbox_backend::isolation_protocol::request_chunk( + &RequestEnvelope::new(request).expect("encode request"), + ) + .expect("typed request"), + ) + .await + .expect("send request"); + let pending = SandboxProtocolClient::new(channel.clone()) + .exchange(bearer_request(ReceiverStream::new(receiver), &token)) + .await + .expect("open exchange") + .into_inner(); + parked.push((sender, pending)); + } + for (_, pending) in &mut parked { + assert!( + tokio::time::timeout(Duration::from_millis(500), pending.message()) + .await + .is_err(), + "a parked exchange answered before termination" + ); + } + signalled_sender + .send(std::time::Instant::now()) + .expect("report SIGTERM time"); + nix::sys::signal::kill( + nix::unistd::Pid::this(), + nix::sys::signal::Signal::SIGTERM, + ) + .expect("send SIGTERM"); + std::future::pending::<()>().await; + drop(parked); + }); + }); + if std::panic::catch_unwind(supervisor).is_err() { + std::process::exit(1); + } + }); + + // The parked `Wait` blocks shutdown only when the shutdown cancels + // the agent's reaper before it records the exit, so a task that + // never ends stands in for that loss. + process_runtime.spawn_blocking(|| { + loop { + std::thread::park(); + } + }); + install_boundary_signal_handlers().expect("install termination handlers"); + serve_then_stop(process_runtime, &listener, boundary) + .expect("terminate after supervisor confirmation"); + let elapsed = signalled + .recv() + .expect("the supervisor sent SIGTERM") + .elapsed(); + assert!( + elapsed < EXIT_BUDGET, + "the boundary took {elapsed:?} to stop after SIGTERM" + ); + } + #[tokio::test(flavor = "multi_thread")] async fn grpc_blackhole_expires_connection_and_releases_mediation_lease() { let (runtime, token) = availability_test_runtime(); diff --git a/crates/openshell-sandbox/src/sandbox/linux/landlock.rs b/crates/openshell-sandbox/src/sandbox/linux/landlock.rs index 25b505f389..b695ccfc82 100644 --- a/crates/openshell-sandbox/src/sandbox/linux/landlock.rs +++ b/crates/openshell-sandbox/src/sandbox/linux/landlock.rs @@ -3,13 +3,19 @@ //! Landlock filesystem sandboxing. +#[cfg(test)] +use landlock::PathFd; use landlock::{ - ABI, Access, AccessFs, BitFlags, CompatLevel, Compatible, PathBeneath, PathFd, PathFdError, - Ruleset, RulesetAttr, RulesetCreatedAttr, + ABI, Access, AccessFs, BitFlags, CompatLevel, Compatible, PathBeneath, Ruleset, RulesetAttr, + RulesetCreatedAttr, }; use miette::{IntoDiagnostic, Result}; use openshell_core::policy::{LandlockCompatibility, SandboxPolicy}; +use std::collections::VecDeque; +use std::ffi::OsString; use std::os::fd::{AsFd, OwnedFd}; +use std::os::unix::ffi::OsStringExt; +use std::path::Component; use std::path::{Path, PathBuf}; use tracing::debug; @@ -318,6 +324,19 @@ fn prepare_with_path_open_mode( .build() ); + // Path admission is independent of kernel compatibility. In particular, an + // unsafe policy anchor must never reach the best-effort ruleset fallback. + let read_only = open_policy_paths(&read_only, compatibility, path_open_mode)?; + let read_write = open_policy_paths(&read_write, compatibility, path_open_mode)?; + let rules_applied = read_only.len() + read_write.len(); + if rules_applied == 0 { + return Err(miette::miette!( + "Landlock ruleset has zero valid paths — all {} path(s) failed to open. \ + Refusing to continue without filesystem restrictions.", + total_paths, + )); + } + let result: Result = (|| { let access_all = AccessFs::from_all(abi); let access_read = AccessFs::from_read(abi); @@ -329,36 +348,20 @@ fn prepare_with_path_open_mode( .into_diagnostic()?; let mut ruleset = ruleset.create().into_diagnostic()?; - let mut rules_applied: usize = 0; - - for path in &read_only { - if let Some(path_fd) = try_open_path(path, compatibility, path_open_mode)? { - let allowed_access = access_for_path_fd(&path_fd, access_read, abi)?; - debug!(path = %path.display(), "Landlock allow read-only"); - ruleset = ruleset - .add_rule(PathBeneath::new(path_fd, allowed_access)) - .into_diagnostic()?; - rules_applied += 1; - } - } - - for path in &read_write { - if let Some(path_fd) = try_open_path(path, compatibility, path_open_mode)? { - let allowed_access = access_for_path_fd(&path_fd, access_all, abi)?; - debug!(path = %path.display(), "Landlock allow read-write"); - ruleset = ruleset - .add_rule(PathBeneath::new(path_fd, allowed_access)) - .into_diagnostic()?; - rules_applied += 1; - } + for (path, path_fd) in read_only { + let allowed_access = access_for_path_fd(&path_fd, access_read, abi)?; + debug!(path = %path.display(), "Landlock allow read-only"); + ruleset = ruleset + .add_rule(PathBeneath::new(path_fd, allowed_access)) + .into_diagnostic()?; } - if rules_applied == 0 { - return Err(miette::miette!( - "Landlock ruleset has zero valid paths — all {} path(s) failed to open. \ - Refusing to apply an empty ruleset that would block all filesystem access.", - total_paths, - )); + for (path, path_fd) in read_write { + let allowed_access = access_for_path_fd(&path_fd, access_all, abi)?; + debug!(path = %path.display(), "Landlock allow read-write"); + ruleset = ruleset + .add_rule(PathBeneath::new(path_fd, allowed_access)) + .into_diagnostic()?; } let skipped = total_paths - rules_applied; @@ -469,11 +472,119 @@ fn access_for_path_fd( }) } +/// A path-opening failure that must not be confused with compatibility errors. +#[derive(Debug)] +enum PolicyPathError { + Io(std::io::Error), + UnsafeSymlink, +} + +impl From for PolicyPathError { + fn from(error: rustix::io::Errno) -> Self { + Self::Io(error.into()) + } +} + +/// Whether a directory's entries can be changed by this workload identity. +/// Ownership counts even without current write permission: owners can chmod. +/// An unrecognized access-check failure is conservatively treated as mutable. +fn workload_can_modify_directory(directory: &OwnedFd) -> Result { + use rustix::fs::{Access, AtFlags, accessat, fstat}; + let stat = fstat(directory)?; + if stat.st_uid == rustix::process::geteuid().as_raw() { + return Ok(true); + } + // A read-only image mount is also immutable to the capability-free + // workload. Unknown failures remain untrusted rather than granting access. + Ok(!matches!( + accessat(directory, ".", Access::WRITE_OK, AtFlags::EACCESS), + Err(rustix::io::Errno::ACCESS | rustix::io::Errno::ROFS) + )) +} + +fn path_components(path: &Path) -> VecDeque { + path.components() + .filter_map(|component| match component { + Component::RootDir | Component::CurDir => None, + Component::ParentDir => Some(OsString::from("..")), + Component::Normal(name) => Some(name.to_owned()), + Component::Prefix(_) => unreachable!("Linux paths have no prefix"), + }) + .collect() +} + +/// Resolve policy anchors through pinned descriptors, following only symlinks +/// beneath directories the workload cannot modify. This preserves immutable +/// image aliases while refusing mutable aliases at every path component. +fn open_policy_path(path: &Path) -> Result { + use rustix::fs::{FileType, Mode, OFlags, fstat, open, openat, readlinkat}; + if !path.is_absolute() { + return Err(PolicyPathError::Io(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Landlock policy paths must be absolute", + ))); + } + let flags = OFlags::PATH | OFlags::NOFOLLOW | OFlags::CLOEXEC; + let mut directory = open("/", flags | OFlags::DIRECTORY, Mode::empty())?; + let mut mutable_ancestor = workload_can_modify_directory(&directory)?; + let mut pending = path_components(path); + let mut symlinks = 0; + while let Some(component) = pending.pop_front() { + let fd = openat(&directory, &component, flags, Mode::empty())?; + match FileType::from_raw_mode(fstat(&fd)?.st_mode) { + FileType::Symlink => { + // A non-writable immediate parent is insufficient: an ancestor + // may allow renaming that parent or replacing its entire tree. + if mutable_ancestor { + return Err(PolicyPathError::UnsafeSymlink); + } + symlinks += 1; + if symlinks > 40 { + return Err(rustix::io::Errno::LOOP.into()); + } + // Empty-path readlinkat reads the opened link, not a possibly + // replaced directory entry. Magic links are never opened. + let target = PathBuf::from(OsString::from_vec( + readlinkat(&fd, "", Vec::new())?.into_bytes(), + )); + if target.is_absolute() { + directory = open("/", flags | OFlags::DIRECTORY, Mode::empty())?; + mutable_ancestor |= workload_can_modify_directory(&directory)?; + } + let mut target_components = path_components(&target); + target_components.append(&mut pending); + pending = target_components; + } + FileType::Directory => { + mutable_ancestor |= workload_can_modify_directory(&fd)?; + directory = fd; + } + _ if pending.is_empty() => return Ok(fd), + _ => return Err(rustix::io::Errno::NOTDIR.into()), + } + } + Ok(directory) +} + +fn open_policy_paths( + paths: &[PathBuf], + compatibility: &LandlockCompatibility, + path_open_mode: PathOpenMode, +) -> Result> { + let mut opened = Vec::new(); + for path in paths { + if let Some(fd) = try_open_path(path, compatibility, path_open_mode)? { + opened.push((path.clone(), fd)); + } + } + Ok(opened) +} + /// Attempt to open a path for Landlock rule creation. /// -/// In `BestEffort` mode, inaccessible paths (missing, permission denied, symlink -/// loops, etc.) are skipped with a warning and `Ok(None)` is returned so the -/// caller can continue building the ruleset from the remaining valid paths. +/// Mutable symlink traversal is always fatal. In `BestEffort` mode, ordinary +/// inaccessible paths are skipped with `Ok(None)` so the caller can continue +/// building the ruleset from the remaining valid paths. /// /// In `HardRequirement` mode, any failure is fatal — the caller propagates the /// error, which ultimately aborts sandbox startup. @@ -481,16 +592,16 @@ fn try_open_path( path: &Path, compatibility: &LandlockCompatibility, path_open_mode: PathOpenMode, -) -> Result> { - match PathFd::new(path) { +) -> Result> { + match open_policy_path(path) { Ok(fd) => Ok(Some(fd)), - Err(err) => { - let reason = classify_path_fd_error(&err); - let is_not_found = matches!( - &err, - PathFdError::OpenCall { source, .. } - if source.kind() == std::io::ErrorKind::NotFound - ); + Err(PolicyPathError::UnsafeSymlink) => Err(miette::miette!( + "Landlock policy path traverses a symlink beneath a workload-modifiable directory: {}", + path.display() + )), + Err(PolicyPathError::Io(err)) => { + let reason = classify_io_error(&err); + let is_not_found = err.kind() == std::io::ErrorKind::NotFound; if matches!(path_open_mode, PathOpenMode::CurrentUser) { if is_not_found { debug!( @@ -556,19 +667,6 @@ fn try_open_path( } } -/// Classify a [`PathFdError`] into a human-readable reason. -/// -/// `PathFd::new()` wraps `open(path, O_PATH | O_CLOEXEC)` which can fail for -/// several reasons beyond simple non-existence. The `PathFdError::OpenCall` -/// variant wraps the underlying `std::io::Error`. -fn classify_path_fd_error(err: &PathFdError) -> &'static str { - match err { - PathFdError::OpenCall { source, .. } => classify_io_error(source), - // PathFdError is #[non_exhaustive], handle future variants gracefully. - _ => "unexpected error", - } -} - /// Classify a `std::io::Error` into a human-readable reason string. fn classify_io_error(err: &std::io::Error) -> &'static str { match err.kind() { @@ -884,6 +982,171 @@ mod tests { assert!(result.unwrap().is_none()); } + #[test] + fn policy_path_rejects_mutable_symlink_in_every_compatibility_mode() { + let root = tempfile::tempdir().unwrap(); + let link = root.path().join("workspace"); + std::os::unix::fs::symlink("/", &link).unwrap(); + for compatibility in [ + LandlockCompatibility::BestEffort, + LandlockCompatibility::HardRequirement, + ] { + for mode in [PathOpenMode::CurrentUser, PathOpenMode::Privileged] { + let error = try_open_path(&link, &compatibility, mode).unwrap_err(); + assert!(error.to_string().contains("workload-modifiable")); + } + } + } + + #[test] + fn policy_path_rejects_mutable_ancestor_symlink() { + let root = tempfile::tempdir().unwrap(); + let outside = tempfile::tempdir().unwrap(); + std::fs::create_dir(outside.path().join("nested")).unwrap(); + let link = root.path().join("workspace"); + std::os::unix::fs::symlink(outside.path(), &link).unwrap(); + assert!(matches!( + open_policy_path(&link.join("nested")), + Err(PolicyPathError::UnsafeSymlink) + )); + } + + #[test] + fn policy_path_owned_readonly_directory_is_mutable() { + use std::os::unix::fs::PermissionsExt; + let root = tempfile::tempdir().unwrap(); + std::fs::set_permissions(root.path(), std::fs::Permissions::from_mode(0o555)).unwrap(); + let fd = rustix::fs::open( + root.path(), + rustix::fs::OFlags::PATH | rustix::fs::OFlags::DIRECTORY, + rustix::fs::Mode::empty(), + ) + .unwrap(); + assert!(workload_can_modify_directory(&fd).unwrap()); + // Restore permissions for TempDir cleanup. + std::fs::set_permissions(root.path(), std::fs::Permissions::from_mode(0o700)).unwrap(); + } + + #[test] + fn policy_path_descriptor_stays_on_admitted_inode_after_replacement() { + let root = tempfile::tempdir().unwrap(); + let workspace = root.path().join("workspace"); + std::fs::create_dir(&workspace).unwrap(); + let fd = open_policy_path(&workspace).unwrap(); + let admitted = rustix::fs::fstat(&fd).unwrap(); + std::fs::remove_dir(&workspace).unwrap(); + std::os::unix::fs::symlink("/", &workspace).unwrap(); + let retained = rustix::fs::fstat(&fd).unwrap(); + assert_eq!( + (retained.st_dev, retained.st_ino), + (admitted.st_dev, admitted.st_ino) + ); + assert!(matches!( + open_policy_path(&workspace), + Err(PolicyPathError::UnsafeSymlink) + )); + } + + #[test] + fn policy_path_rejects_relative_paths() { + assert!(matches!( + open_policy_path(Path::new("workspace")), + Err(PolicyPathError::Io(_)) + )); + } + + #[test] + fn policy_path_trusted_image_aliases_and_mutable_ancestry() { + use std::os::unix::fs::PermissionsExt; + use std::os::unix::process::CommandExt; + // Exercise the actual DAC check as an unprivileged workload. Root owns + // every containing system directory and is not an admitted workload UID. + if rustix::process::geteuid().is_root() { + let fixture = tempfile::tempdir().unwrap(); + std::fs::set_permissions(fixture.path(), std::fs::Permissions::from_mode(0o777)) + .unwrap(); + let immutable = fixture.path().join("immutable"); + std::fs::create_dir(&immutable).unwrap(); + std::os::unix::fs::symlink("/", immutable.join("alias")).unwrap(); + std::fs::set_permissions(&immutable, std::fs::Permissions::from_mode(0o555)).unwrap(); + let mut command = std::process::Command::new(std::env::current_exe().unwrap()); + command.args([ + "--exact", + "sandbox::linux::landlock::tests::policy_path_trusted_image_aliases_and_mutable_ancestry", + "--nocapture", + ]).env("OPENSHELL_LANDLOCK_MUTABLE_ANCESTRY", immutable.join("alias")); + // SAFETY: these credential operations execute only in the forked + // test child before exec and do not change the test runner identity. + #[allow(unsafe_code)] + unsafe { + command.pre_exec(|| { + if libc::setgroups(0, std::ptr::null()) != 0 + || libc::setgid(65534) != 0 + || libc::setuid(65534) != 0 + { + return Err(std::io::Error::last_os_error()); + } + Ok(()) + }); + } + let status = command.status().unwrap(); + std::fs::set_permissions(&immutable, std::fs::Permissions::from_mode(0o755)).unwrap(); + assert!(status.success()); + return; + } + for alias in ["/bin", "/lib"] { + if std::fs::symlink_metadata(alias).is_ok() { + open_policy_path(Path::new(alias)).unwrap(); + } + } + if let Some(path) = std::env::var_os("OPENSHELL_LANDLOCK_MUTABLE_ANCESTRY") { + assert!(matches!( + open_policy_path(Path::new(&path)), + Err(PolicyPathError::UnsafeSymlink) + )); + } + } + + #[test] + fn prepare_mutable_policy_anchor_never_uses_best_effort_fallback() { + if !matches!(probe_availability(), LandlockAvailability::Available { .. }) { + return; + } + let root = tempfile::tempdir().unwrap(); + let link = root.path().join("workspace"); + std::os::unix::fs::symlink("/", &link).unwrap(); + for compatibility in [ + LandlockCompatibility::BestEffort, + LandlockCompatibility::HardRequirement, + ] { + for mode in [PathOpenMode::CurrentUser, PathOpenMode::Privileged] { + let mut policy = + hard_requirement_policy(vec![root.path().to_path_buf()], vec![link.clone()]); + policy.landlock.compatibility = compatibility.clone(); + let error = prepare_with_path_open_mode(&policy, None, mode) + .err() + .expect("unsafe anchor must abort preparation"); + assert!(error.to_string().contains("workload-modifiable")); + } + } + } + + #[test] + fn prepare_zero_usable_paths_never_uses_best_effort_fallback() { + if !matches!(probe_availability(), LandlockAvailability::Available { .. }) { + return; + } + let mut policy = hard_requirement_policy( + vec![], + vec![PathBuf::from("/nonexistent/openshell/test/path")], + ); + policy.landlock.compatibility = LandlockCompatibility::BestEffort; + let error = prepare_current_user(&policy, None) + .err() + .expect("empty allowlist must fail closed"); + assert!(error.to_string().contains("zero valid paths")); + } + #[test] fn classify_not_found() { let err = std::io::Error::from_raw_os_error(libc::ENOENT); @@ -920,14 +1183,6 @@ mod tests { assert_eq!(classify_io_error(&err), "unexpected error"); } - #[test] - fn classify_path_fd_error_extracts_io_error() { - // Use PathFd::new on a non-existent path to get a real PathFdError - // (the OpenCall variant is #[non_exhaustive] and can't be constructed directly). - let err = PathFd::new("/nonexistent/openshell/classify/test").unwrap_err(); - assert_eq!(classify_path_fd_error(&err), "path does not exist"); - } - #[test] fn probe_availability_returns_a_result() { // The probe should not panic regardless of whether Landlock is available. diff --git a/crates/openshell-sdk/src/client.rs b/crates/openshell-sdk/src/client.rs index 1f03916ef2..8241ac49b5 100644 --- a/crates/openshell-sdk/src/client.rs +++ b/crates/openshell-sdk/src/client.rs @@ -89,6 +89,16 @@ impl OpenShellClient { self.channel.clone() } + /// Clone of the auth interceptor backing [`OpenShellClient::raw_grpc`]. + /// + /// tonic allows a single interceptor per channel, so callers that need + /// to stack another concern (e.g. trace-context forwarding) on top of + /// authentication can compose around this clone — it shares the same + /// live bearer slot, so OIDC refresh still reaches the composed stack. + pub fn auth_interceptor(&self) -> EdgeAuthInterceptor { + self.interceptor.clone() + } + /// Authenticated gRPC client for the main `OpenShell` service. /// /// Use this when the curated surface below doesn't expose the RPC or @@ -1770,6 +1780,31 @@ mod tests { ); } + #[tokio::test] + async fn auth_interceptor_clone_sees_refreshed_token() { + use tonic::service::Interceptor as _; + + // The clone handed out by `auth_interceptor()` must share the live + // bearer slot: callers compose other concerns (e.g. the CLI's + // trace-forwarding gateway interceptor) around it, and a forced OIDC + // refresh must reach those already-built stacks. + let calls = Arc::new(AtomicUsize::new(0)); + let client = oidc_client_with_refresher(Arc::clone(&calls)); + let mut interceptor = client.auth_interceptor(); + + assert!(client.force_refresh().await.unwrap()); + + let request = interceptor.call(tonic::Request::new(())).unwrap(); + assert_eq!( + request + .metadata() + .get("authorization") + .and_then(|value| value.to_str().ok()), + Some("Bearer token-1"), + "refreshed token must reach the cloned interceptor" + ); + } + #[test] fn store_bearer_rejects_malformed_token_and_keeps_previous() { let slot: BearerSlot = Arc::new(RwLock::new(None)); diff --git a/crates/openshell-server/README.md b/crates/openshell-server/README.md index a1053f582c..3c52ebb127 100644 --- a/crates/openshell-server/README.md +++ b/crates/openshell-server/README.md @@ -167,6 +167,25 @@ A provisioning timeout can be repaired by Stop after its original driver work settles and cleanup completes. Stop exposes Stopped without discarding the timeout diagnosis or storage. This lifecycle behavior also applies when updates are disabled. +## Static credential delivery + +Existing polling and configuration-push environments keep their original wire +shape, delivery behavior, identities, and stable-handle derivation. The gateway +adds placement-only `ProviderProfileCredential` entries to the existing runtime +credential map under `static-env:`, for actually delivered static keys. +Entries are matched to their owning provider binding and contain no token grant, +refresh configuration, or credential values. The supervisor infers enforcement +from the auth metadata; the gateway does not enforce placement. + +Older supervisors ignore entries without token grants. An older gateway omits +these entries, so newer supervisors permit static substitution only in +`Authorization` or explicitly enabled body/WebSocket rewrite contexts. Custom +placements require a supporting gateway; upgrade it before the supervisor. +Explicit authored placement is forwarded even when invalid so the supervisor +can reject it rather than silently apply the default. Metadata travels in the +same captured environment snapshot; no separate service, capability gate, or +additional lookup is needed. + ## Operator credential export `GetProviderCredentials` exports selected profile-declared runtime environment diff --git a/crates/openshell-server/src/auth/workspace_authz.rs b/crates/openshell-server/src/auth/workspace_authz.rs index bc5cac8778..035d8aa354 100644 --- a/crates/openshell-server/src/auth/workspace_authz.rs +++ b/crates/openshell-server/src/auth/workspace_authz.rs @@ -156,7 +156,7 @@ pub async fn authorize_workspace( &user.identity.subject, ) .await - .map_err(|e| Status::internal(format!("membership lookup failed: {e}")))?; + .map_err(|e| crate::grpc::persistence_error_to_status(e, "membership lookup"))?; let Some(member) = member else { let workspace_arg = shell_quote_for_hint(&workspace); diff --git a/crates/openshell-server/src/cli.rs b/crates/openshell-server/src/cli.rs index 1f0f215881..55ad3091ea 100644 --- a/crates/openshell-server/src/cli.rs +++ b/crates/openshell-server/src/cli.rs @@ -799,6 +799,10 @@ async fn run_from_args( let prepared = prepare_server_config_with_drivers(&mut args, &matches, &compute_drivers)?; validate_user_auth(&prepared.config)?; + openshell_ocsf::register_trace_correlation_extractor( + openshell_otel::current_ocsf_trace_correlation, + ); + // Initialize OCSF identity before tracing can emit gateway events. let gateway_identity = crate::gateway_ocsf::GatewayIdentity { name: prepared.config.name.clone(), diff --git a/crates/openshell-server/src/grpc/mod.rs b/crates/openshell-server/src/grpc/mod.rs index 2efcb9873e..69208348de 100644 --- a/crates/openshell-server/src/grpc/mod.rs +++ b/crates/openshell-server/src/grpc/mod.rs @@ -84,7 +84,8 @@ use crate::ServerState; /// CAS conflicts (optimistic concurrency failures) are mapped to `ABORTED` /// to signal that the client should retry with fresh data. Mutation lock /// timeouts are mapped to `UNAVAILABLE` with a retry delay: the mutation lock -/// was not acquired, so this request's guarded writes did not run. Other +/// was not acquired, so this request's guarded writes did not run. So is a +/// database without a free connection, which ran no statement. Other /// persistence errors are mapped to `INTERNAL`. pub fn persistence_error_to_status( err: crate::persistence::PersistenceError, @@ -108,6 +109,11 @@ pub fn persistence_error_to_status( format!("{operation} timed out waiting for a concurrent mutation; retry the request"), std::time::Duration::from_secs(1), ), + PersistenceError::Unavailable(_) => openshell_core::rpc_error::unavailable( + "DATABASE_UNAVAILABLE", + format!("{operation} failed: the database is unavailable; retry the request"), + std::time::Duration::from_secs(1), + ), other => Status::internal(format!("{operation} failed: {other}")), } } @@ -1367,6 +1373,40 @@ mod tests { ); } + #[test] + fn persistence_error_to_status_maps_database_unavailable_to_unavailable() { + let status = persistence_error_to_status( + crate::persistence::PersistenceError::Unavailable( + "pool timed out while waiting for an open connection".into(), + ), + "workspace lookup", + ); + + assert_eq!(status.code(), tonic::Code::Unavailable); + assert_eq!( + status.message(), + "workspace lookup failed: the database is unavailable; retry the request" + ); + let details = openshell_core::rpc_error::decode_details(&status).expect("error details"); + assert_eq!( + details.error_info().expect("error info").reason, + "DATABASE_UNAVAILABLE" + ); + assert_eq!( + details.retry_info().expect("retry info").retry_delay, + Some(std::time::Duration::from_secs(1)) + ); + assert!( + !openshell_core::replica_routing::retry_unrouted( + Some(&tonic::metadata::AsciiMetadataValue::from_static( + "openshell-0" + )), + &status, + ), + "a gateway that answered must not look like an unrouted edge failure" + ); + } + #[test] fn public_resource_capabilities_preserves_absence() { let absent: Option = None; diff --git a/crates/openshell-server/src/grpc/policy.rs b/crates/openshell-server/src/grpc/policy.rs index be95843f6b..cc83789417 100644 --- a/crates/openshell-server/src/grpc/policy.rs +++ b/crates/openshell-server/src/grpc/policy.rs @@ -4061,6 +4061,18 @@ pub async fn build_provider_environment_snapshot_from_inputs( .clear(); } + // Add request metadata only after token-grant owner composition and all + // static value filtering, so it describes this exact atomic snapshot. + let static_request_metadata = super::provider::static_credential_request_metadata( + &sandbox_id, + provider_profile_catalog, + provider_records, + &provider_environment, + )?; + provider_environment + .dynamic_credentials + .extend(static_request_metadata); + info!( sandbox_id = %sandbox_id, provider_count = provider_names.len(), @@ -12376,13 +12388,27 @@ mod tests { environment.provider_env_revision, second.provider_env_revision ); - assert_eq!(environment.dynamic_credentials.len(), 2); - assert!(environment.dynamic_credentials.values().all(|credential| { - credential - .token_grant - .as_ref() - .is_some_and(|grant| grant.token_endpoint.contains("revision-b")) - })); + let grants = environment + .dynamic_credentials + .values() + .filter_map(|credential| credential.token_grant.as_ref()) + .collect::>(); + assert_eq!(grants.len(), 2); + assert!( + grants + .iter() + .all(|grant| grant.token_endpoint.contains("revision-b")) + ); + assert!( + environment + .dynamic_credentials + .iter() + .filter(|(key, _)| key.starts_with( + openshell_core::provider_credentials::STATIC_CREDENTIAL_METADATA_PREFIX + )) + .all(|(_, credential)| credential.token_grant.is_none() + && credential.token_grant_owners.is_empty()) + ); handle_submit_policy_analysis( &state, @@ -13114,6 +13140,95 @@ mod tests { assert!(error.message().contains("already defines endpoints")); } + #[tokio::test] + async fn provider_attachment_credential_inspection_uses_latest_policy() { + for (creation_has_rule, updated_has_rule) in [ + (true, Some(false)), + (false, Some(true)), + (true, None), + (false, None), + ] { + let state = test_server_state().await; + state + .store + .put_message(&test_provider("work-github", "github")) + .await + .unwrap(); + let policy = |has_rule| { + if has_rule { + test_policy_with_rule("uninspected", "api.github.com") + } else { + ProtoSandboxPolicy::default() + } + }; + let creation_policy = policy(creation_has_rule); + let sandbox = test_sandbox( + "sb-attachment-inspection", + "attach-inspection", + creation_policy.clone(), + Vec::new(), + ); + state.store.put_message(&sandbox).await.unwrap(); + + if let Some(has_rule) = updated_has_rule { + handle_update_config( + &state, + with_user(Request::new(UpdateConfigRequest { + sandbox: "attach-inspection".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + policy: Some(policy(has_rule)), + ..Default::default() + })), + ) + .await + .expect("policy update without an attached provider must succeed"); + } + + let result = super::super::sandbox::handle_attach_sandbox_provider( + &state, + authed_request(openshell_core::proto::AttachSandboxProviderRequest { + sandbox: "attach-inspection".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + provider: "work-github".to_string(), + ..Default::default() + }), + ) + .await; + let should_reject = updated_has_rule.unwrap_or(creation_has_rule); + if should_reject { + let error = result.expect_err("current uninspected rule must prevent attachment"); + assert_eq!(error.code(), Code::FailedPrecondition); + assert!(error.message().contains("L4-only")); + assert!(error.message().contains("uninspected")); + } else { + assert!( + result + .expect("no current uninspected rule") + .into_inner() + .attached + ); + } + + let stored = state + .store + .get_message::("sb-attachment-inspection") + .await + .unwrap() + .unwrap(); + let spec = stored.spec.unwrap(); + assert_eq!(spec.policy, Some(creation_policy)); + assert_eq!( + spec.providers, + if should_reject { + Vec::::new() + } else { + vec!["work-github".to_string()] + } + ); + state.store.close_for_test().await; + } + } + #[tokio::test] async fn update_config_gates_uninspected_endpointless_credential_binding() { use openshell_core::proto::{ProviderProfile, ProviderProfileCategory}; diff --git a/crates/openshell-server/src/grpc/policy/endpoint_status.rs b/crates/openshell-server/src/grpc/policy/endpoint_status.rs index 88efafada1..6d35131cc5 100644 --- a/crates/openshell-server/src/grpc/policy/endpoint_status.rs +++ b/crates/openshell-server/src/grpc/policy/endpoint_status.rs @@ -738,9 +738,11 @@ async fn invalidate_sandbox_endpoint_status_once( // Lifecycle writers on other replicas take no distributed guard, and a // delete between the re-read and the write surfaces as a database // error. Retry with a fresh read after this guard drops. - Err(error @ (PersistenceError::Conflict { .. } | PersistenceError::Database(_))) => { - Ok(StartupAttempt::Retry(error)) - } + Err( + error @ (PersistenceError::Conflict { .. } + | PersistenceError::Database(_) + | PersistenceError::Unavailable(_)), + ) => Ok(StartupAttempt::Retry(error)), Err(error) => Err(super::super::persistence_error_to_status( error, "invalidate tool server endpoint status during gateway startup", diff --git a/crates/openshell-server/src/grpc/provider.rs b/crates/openshell-server/src/grpc/provider.rs index 0f09d5dc2f..6ef3c3b15d 100644 --- a/crates/openshell-server/src/grpc/provider.rs +++ b/crates/openshell-server/src/grpc/provider.rs @@ -1566,6 +1566,78 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin }) } +/// Copy authored request metadata for static values that survived snapshot +/// filtering. The reserved namespace keeps it separate from endpoint token grants; +/// older supervisors ignore entries without `token_grant` before parsing selectors. +pub(super) fn static_credential_request_metadata( + sandbox_id: &str, + catalog: &EffectiveProviderProfileCatalog, + records: &[ProviderEnvironmentRecord], + environment: &ProviderEnvironment, +) -> Result, Status> { + use openshell_core::provider_credentials::STATIC_CREDENTIAL_METADATA_PREFIX; + let mut metadata = HashMap::new(); + for record in records { + let provider = &record.provider; + let Some(profile) = get_provider_type_profile_for_scope( + catalog, + &provider.r#type, + &provider.profile_workspace, + ) else { + continue; + }; + let refresh_epochs = refresh_authorization_epochs_by_key(record)?; + for credential in &profile.credentials { + // Keep malformed explicit metadata distinguishable from a wholly + // absent declaration. The supervisor validates authored fields and + // fails closed; absence selects its default request placement. + if credential.auth_style.is_empty() + && credential.header_name.is_empty() + && credential.query_param.is_empty() + && credential.path_template.is_empty() + { + continue; + } + for key in credential.accepted_stored_keys() { + if !environment.environment.contains_key(key) + || !environment.static_credential_keys.contains(key) + { + continue; + } + let Some(baseline) = environment.static_credential_bindings.get(key) else { + continue; + }; + // A later optional declaration with the same environment key + // must not override metadata belonging to the actual owner. + let expected = static_credential_binding( + Some(sandbox_id), + record, + key, + &baseline.endpoints, + refresh_epochs.get(key).map(String::as_str), + credential.stable_placeholder, + ); + if expected != *baseline { + continue; + } + metadata.insert( + format!("{STATIC_CREDENTIAL_METADATA_PREFIX}{key}"), + ProviderProfileCredential { + name: credential.name.clone(), + env_vars: vec![key.to_string()], + auth_style: credential.auth_style.clone(), + header_name: credential.header_name.clone(), + query_param: credential.query_param.clone(), + path_template: credential.path_template.clone(), + ..Default::default() + }, + ); + } + } + } + Ok(metadata) +} + fn refresh_authorization_epochs_by_key( record: &ProviderEnvironmentRecord, ) -> Result, Status> { @@ -12223,6 +12295,142 @@ mod tests { ); } + #[tokio::test] + async fn static_request_metadata_is_minimal_owner_bound_and_tracks_delivered_values() { + use openshell_core::provider_credentials::STATIC_CREDENTIAL_METADATA_PREFIX; + let store = test_store().await; + let mut owned = custom_profile("metadata-owner"); + owned.credentials = vec![static_credential("token", "TOKEN", true)]; + owned.endpoints = vec![NetworkEndpoint { + host: "api.example.com".into(), + port: 443, + protocol: "rest".into(), + access: openshell_core::proto::NetworkAccessPreset::Full as i32, + ..Default::default() + }]; + let mut optional = owned.clone(); + optional.id = "metadata-optional".into(); + optional.credentials[0].required = false; + optional.credentials[0].header_name = "x-wrong-owner".into(); + let sources = ProviderProfileSources::from_test_profiles(vec![owned.clone(), optional]); + let catalog = sources.snapshot_catalog(&store, "default").await.unwrap(); + let record = |name: &str, id: &str, profile: &str| ProviderEnvironmentRecord { + name: name.into(), + object_id: id.into(), + resource_version: 1, + provider: Provider { + r#type: profile.into(), + profile_workspace: "default".into(), + ..Default::default() + }, + refresh_states: Vec::new(), + }; + let mut records = [ + record("owner", "owner-id", "metadata-owner"), + record("optional", "optional-id", "metadata-optional"), + ]; + records[0] + .provider + .credentials + .insert("TOKEN".into(), "synthetic-sensitive-token".into()); + let endpoints = vec![StaticCredentialEndpointBinding { + host: "api.example.com".into(), + port: 443, + path: "/**".into(), + }]; + let baseline = static_credential_binding( + Some("sandbox"), + &records[0], + "TOKEN", + &endpoints, + None, + false, + ); + let mut environment = ProviderEnvironment { + environment: HashMap::from([("TOKEN".into(), "synthetic-sensitive-token".into())]), + static_credential_keys: HashSet::from(["TOKEN".into()]), + static_credential_bindings: HashMap::from([("TOKEN".into(), baseline.clone())]), + ..Default::default() + }; + let metadata = + static_credential_request_metadata("sandbox", &catalog, &records, &environment) + .unwrap(); + let key = format!("{STATIC_CREDENTIAL_METADATA_PREFIX}TOKEN"); + assert_eq!(metadata.len(), 1); + assert_eq!( + metadata[&key], + ProviderProfileCredential { + name: "token".into(), + env_vars: vec!["TOKEN".into()], + auth_style: "bearer".into(), + header_name: "authorization".into(), + ..Default::default() + } + ); + assert_eq!( + environment.static_credential_bindings["TOKEN"], baseline, + "legacy handles and identities must not change" + ); + let encoded = metadata[&key].encode_to_vec(); + assert!( + !encoded + .windows(b"synthetic-sensitive-token".len()) + .any(|window| window == b"synthetic-sensitive-token") + ); + // Preserve malformed authored fields for the supervisor to reject; + // silently dropping them would incorrectly select its default location. + for (style, header, query, path, explicit, valid) in [ + ("", "", "", "", false, true), + (" ", "", "", "", true, false), + ("", "x-api-key", "", "", true, false), + ("header", "Bad Header", "", "", true, false), + ("bearer", "authorization", "api_key", "", true, true), + ( + "bearer", + "authorization", + "api_key", + "/v1/{credential}/resources", + true, + true, + ), + ("query", "", "api?key", "", true, false), + ("basic", "x-custom", "", "", true, true), + ("basic", "Authorization", "", "", true, true), + ] { + let mut declared = owned.clone(); + declared.credentials[0].auth_style = style.into(); + declared.credentials[0].header_name = header.into(); + declared.credentials[0].query_param = query.into(); + declared.credentials[0].path_template = path.into(); + let sources = ProviderProfileSources::from_test_profiles(vec![declared]); + let catalog = sources.snapshot_catalog(&store, "default").await.unwrap(); + let metadata = + static_credential_request_metadata("sandbox", &catalog, &records, &environment) + .unwrap(); + assert_eq!( + metadata.contains_key(&key), + explicit, + "style {style:?}, header {header:?}" + ); + let installed = openshell_core::provider_credentials::ProviderCredentialState::from_bound_environment( + 1, environment.environment.clone(), HashMap::new(), metadata, + environment.static_credential_bindings.clone(), vec![], + ); + assert_eq!( + installed.is_ok(), + valid, + "style {style:?}, header {header:?}, query {query:?}" + ); + } + environment.environment.remove("TOKEN"); + assert!( + static_credential_request_metadata("sandbox", &catalog, &records, &environment) + .unwrap() + .is_empty(), + "expired or withheld material must not leave request metadata behind" + ); + } + #[test] fn external_stable_placeholder_binding_preserves_only_value_changes() { let mut record = ProviderEnvironmentRecord { @@ -12548,6 +12756,9 @@ mod tests { credential_state .resolver_for_endpoint("api.example.com", 443, "/v1/messages") .unwrap() + .with_request_location(openshell_core::secrets::RequestCredentialLocation::Header( + "authorization".into() + )) .resolve_placeholder(&original_placeholder), Some("token-1") ); @@ -12608,6 +12819,9 @@ mod tests { credential_state .resolver_for_endpoint("api.example.com", 443, "/v1/messages") .unwrap() + .with_request_location(openshell_core::secrets::RequestCredentialLocation::Header( + "authorization".into() + )) .resolve_placeholder(&original_placeholder), Some("token-2") ); @@ -12625,6 +12839,9 @@ mod tests { reconstructed .resolver_for_endpoint("api.example.com", 443, "/v1/messages") .unwrap() + .with_request_location(openshell_core::secrets::RequestCredentialLocation::Header( + "authorization".into() + )) .resolve_placeholder(&original_placeholder), Some("token-2") ); @@ -12676,6 +12893,9 @@ mod tests { credential_state .resolver_for_endpoint("api.example.com", 443, "/v1/messages") .unwrap() + .with_request_location(openshell_core::secrets::RequestCredentialLocation::Header( + "authorization".into() + )) .resolve_placeholder(&original_placeholder), None ); diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index c998372edb..dc496fa306 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -20,7 +20,7 @@ use crate::pagination::Pagination; use crate::persistence::{ ObjectLabels, ObjectListQuery, ObjectType, WriteCondition, generate_name, }; -use crate::tracing_bus::CursoredEvent; +use crate::tracing_bus::{CursoredEvent, ResumeSnapshot}; use crate::watch_cursor::WatchCursor; use futures::future; use openshell_core::net::set_tcp_nodelay_best_effort; @@ -99,19 +99,6 @@ const RESUME_SPACE_GONE: &str = "resume_after_cursor belongs to a cursor space t const RESUME_CURSOR_AHEAD: &str = "resume_after_cursor is ahead of every cursor this sandbox has \ issued. Restart the watch with an empty resume_after_cursor."; -/// Whether `sandbox_id`'s cursor space is still the one that issued `epoch`. -/// -/// A teardown retires the space and the next publish mints a replacement that -/// renumbers from 1, so a surviving epoch is the only proof that a seq validated -/// earlier still addresses the same numbering. Absent counts as changed: there -/// is nothing left for the cursor to point into. -fn cursor_space_is(state: &ServerState, sandbox_id: &str, epoch: uuid::Uuid) -> bool { - state - .tracing_log_bus - .cursor_space(sandbox_id) - .is_some_and(|space| space.epoch == epoch) -} - #[derive(Debug)] pub struct WatchSandboxStream { receiver: ReceiverStream>, @@ -1566,11 +1553,13 @@ pub(super) async fn handle_attach_sandbox_provider( &candidate_spec.providers, ) .await?; + let current_policy = + super::policy::current_base_policy_for_sandbox(state.store.as_ref(), &sandbox).await?; super::policy::validate_candidate_sandbox_credential_policy( state, &workspace, &candidate_spec.providers, - candidate_spec.policy.as_ref(), + Some(¤t_policy), ) .await?; @@ -2201,21 +2190,19 @@ pub(super) async fn handle_watch_sandbox( } else { None }; - let mut log_rx = if follow_logs { - Some(state.tracing_log_bus.subscribe(&sandbox_id)) - } else { - None - }; - let mut platform_rx = if follow_events { - Some( - state - .tracing_log_bus - .platform_event_bus - .subscribe(&sandbox_id), - ) - } else { - None - }; + // + // The resumable sources subscribe together with a high-water mark + // taken under the cursor-space lock, so the snapshot below can + // tell history (at or below the mark, buffered only) from events + // published while the stream opens (above it, buffered *and* + // queued on these receivers). + let crate::tracing_bus::ResumableSubscription { + mut log_rx, + mut platform_rx, + subscribed_at, + } = state + .tracing_log_bus + .subscribe_resumable(&sandbox_id, follow_logs, follow_events); // Re-read the snapshot now that we have subscriptions active. match state.store.get_message::(&sandbox_id).await { @@ -2255,28 +2242,26 @@ pub(super) async fn handle_watch_sandbox( // Highest seq the tail/replay phase already handled, tracked per // source. The broadcast receivers were subscribed before replay ran, - // so an event published during initialization can sit in both the - // replay buffer and a live receiver; the live loop suppresses events - // at or below its source's mark so each is delivered exactly once. + // so an event published during initialization (above + // `subscribed_at`) sits in both the replay buffer and a live + // receiver; the live loop suppresses events at or below its + // source's mark so each is delivered exactly once. // - // The two marks must stay separate. Both buses number from one - // shared cursor space, but they are read at different instants and - // bounded independently (`log_tail_lines` vs `event_tail`, which has - // no default and so replays nothing unless the client asks). A - // single shared mark therefore lets the deeper source censor the - // shallower one: with the default `event_tail` of 0 the mark rises - // to the newest buffered log while no platform event was replayed at - // all, and every platform event published in the initialization - // window is dropped as a duplicate of something never sent. Keyed by - // source, an event is suppressed only if its own source's replay - // actually covered it. + // Both reads happen under one cursor-space lock hold, so every + // event at or below the snapshot's high-water mark was buffered + // when they ran. A resume replays everything after the cursor from + // each followed source, so each source's mark is the last seq its + // own replay returned. The initial tail is depth-bounded, so both + // marks move to the snapshot's high-water mark instead. That is + // safe only because the depth bound never applies above + // `subscribed_at`: every event published while the stream opened + // is in the batch (or, if its buffer already evicted it, reported), + // and everything else at or below the mark is history the live + // receivers never saw. // - // No unit test pins this. The only reachable window is between the - // subscribe above and the log tail read below -- an event published - // earlier is replayed rather than live, and one published later - // outranks the mark -- and the producer crosses that window with no - // await a test can wedge open. Reproducing it needs a seam in the - // producer, which is not worth adding to production code. + // Tests force both windows through the bus's test-only snapshot + // hooks: one publishes after subscribe and before the snapshot, the + // other publishes between the snapshot's two reads. let resume_seq = resume_after.map_or(0, |resume| resume.seq); let mut log_cutoff: u64 = resume_seq; let mut platform_cutoff: u64 = resume_seq; @@ -2296,62 +2281,39 @@ pub(super) async fn handle_watch_sandbox( // and silently swallow every live event beneath it. Comparing // epochs answers "did this cursor come from *this* space?", // which no numeric bound can. - match state.tracing_log_bus.cursor_space(&sandbox_id) { - None => { - let _ = tx.send(Err(Status::out_of_range(RESUME_SPACE_GONE))).await; - return; - } - Some(space) if space.epoch != resume.epoch => { + // + // Validation and both bus reads happen inside one call, under + // one lock hold (see `TracingLogBus::snapshot_after`). A + // separate validate-then-read pair -- even with the two reads + // themselves atomic against each other -- still leaves a + // window between the validation's lock and the reads' lock + // for a teardown plus a republish to retire the validated + // space and install a replacement in between; the reads would + // then apply the old space's seq to the replacement's + // buffers and find no gap, since `tail_after` only compares + // numbers. Folding validation into the same hold as the reads + // closes that window entirely. + let (log_replay, platform_replay) = match state.tracing_log_bus.snapshot_after( + &sandbox_id, + resume.epoch, + resume.seq, + resume.seq, + follow_logs, + follow_events, + ) { + ResumeSnapshot::SpaceGone => { let _ = tx.send(Err(Status::out_of_range(RESUME_SPACE_GONE))).await; return; } - // Right space, but ahead of anything it issued: only a - // fabricated token gets here. Reject rather than accept a - // cutoff no event can ever exceed. - Some(space) if resume.seq > space.highest_seq => { + ResumeSnapshot::CursorAhead => { let _ = tx .send(Err(Status::out_of_range(RESUME_CURSOR_AHEAD))) .await; return; } - Some(_) => {} - } - - let log_replay = if follow_logs { - Some(state.tracing_log_bus.tail_after(&sandbox_id, resume.seq)) - } else { - None - }; - - let platform_replay = if follow_events { - Some( - state - .tracing_log_bus - .platform_event_bus - .tail_after(&sandbox_id, resume.seq), - ) - } else { - None + ResumeSnapshot::Read(log, platform) => (log, platform), }; - // Re-check the epoch now that both tails are in hand. The check - // above and each `tail_after` take their locks independently, so - // a teardown plus a republish can retire the validated space and - // install a replacement in between. The reads would then have - // applied the old space's seq to the new space's buffers, and - // `tail_after` -- which only knows numbers -- would report no gap - // while skipping every replacement event at or below it. The - // second look is cheap and runs before anything is emitted, so a - // space that moved under us ends the stream instead of serving a - // truncated replay. - // - // Ordering is unchanged: this takes only the allocator lock and - // releases it, never held across a bus lock. - if !cursor_space_is(&state, &sandbox_id, resume.epoch) { - let _ = tx.send(Err(Status::out_of_range(RESUME_SPACE_GONE))).await; - return; - } - // Gap check FIRST (borrows), before the merge moves the vecs. for replay in [&log_replay, &platform_replay] { if let Some(Err(gap)) = replay { @@ -2424,29 +2386,125 @@ pub(super) async fn handle_watch_sandbox( // cursors expects. // // The two windows are truncated independently (log_tail vs - // event_tail), so this merges whatever each bus retained; it - // does not align their depths. + // event_tail). A client tracks one scalar cursor across both + // sources -- whatever the highest one it saw was -- so a + // cursor this batch hands out is only safe to resume from if + // *every* followed source can vouch it covered everything up + // to that point. + // + // `tail_with_floor` reports each source's own coverage floor: + // the *newest* event its window excluded. The source's + // excluded set is a prefix of its own tail, so it delivered + // everything it retains above the floor. The batch as a whole + // is therefore safe above the highest floor across followed + // sources: every event from either source past that point is + // in the batch, and everything at or below it is outside the + // window, the same as ordinary tail truncation. A source never + // withholds its own window this way; only a sibling's floor + // can cut a source's window back. + // + // That can mean fewer events than a source's depth asked for + // whenever a followed sibling left out an event newer than + // part of its window. This depends on where the histories + // fall in the cursor order, not on the depths being unequal: + // logs at 1-2 and platform at 3-4 with both depths at 1 + // leaves out platform 3, so log 2 is withheld. + // That's the trade-off of a single shared scalar cursor: an + // event handed out below the sibling's floor would let a + // resume skip the sibling's withheld events past it. + // + // Both windows and the space's high-water mark come from one + // lock hold (see `TracingLogBus::snapshot_tail`), so no + // publish can land between the two reads. Read separately, a + // log published after the log read and a platform event + // published after it could let the platform window hand out + // the higher cursor while the log arrives live afterwards. + // + // Depths bound history only. Events published after the + // receivers subscribed are always in their source's window, so + // the batch can exceed `log_tail_lines`/`event_tail` by them. + // Each floor therefore stays at or below `subscribed_at`, and + // none of those events is ever withheld by a sibling's floor. + let crate::tracing_bus::TailSnapshot { + log, + platform, + highest_seq, + } = state.tracing_log_bus.snapshot_tail( + &sandbox_id, + follow_logs.then_some(log_tail as usize), + follow_events.then_some(event_tail as usize), + subscribed_at, + ); + let crate::tracing_bus::SourceTail { + events: mut logs, + floor: log_floor, + evicted_while_opening: logs_evicted, + } = log; + let crate::tracing_bus::SourceTail { + mut events, + floor: platform_floor, + evicted_while_opening: events_evicted, + } = platform; + + // 0 means "nothing excluded" for a source, so a fully-covered + // source never raises the maximum. + let critical_floor = log_floor.max(platform_floor); + + let before = logs.len(); + logs.retain(|cursored| cursored.seq > critical_floor); + let log_withheld = before - logs.len(); + let before = events.len(); + events.retain(|cursored| cursored.seq > critical_floor); + let platform_withheld = before - events.len(); + + // Both cutoffs move to the snapshot's high-water mark. The + // receivers were subscribed before the snapshot, so an event + // published in between is both buffered and queued live; the + // snapshot kept it in the batch, so its live copy is a + // duplicate, and delivering it would emit a lower cursor after + // a higher one from the batch. Events at or below + // `subscribed_at` were never queued live. Above the mark, an + // event can only arrive live. + log_cutoff = log_cutoff.max(highest_seq); + platform_cutoff = platform_cutoff.max(highest_seq); + let mut tail: Vec = Vec::new(); - if follow_logs { - let logs = state.tracing_log_bus.tail(&sandbox_id, log_tail as usize); - if let Some(last) = logs.last() { - log_cutoff = log_cutoff.max(last.seq); + tail.extend(logs); + tail.extend(events); + + tail.sort_by_key(|cursored| cursored.seq); + + // Disclose the gap before anything else in this batch. The + // withheld events sit at or below the snapshot's mark, so the + // cutoffs above keep their live copies (if any) from being + // delivered out of order, while later live events past the + // mark still reach the client. The client has to learn the + // gap exists from this warning; a resume from any cursor this + // batch hands out cannot recover it, and `tail_after` reports + // no gap when asked, since nothing was evicted, only never + // sent. + if log_withheld > 0 || platform_withheld > 0 { + let warning = crate::sandbox_watch::coverage_gap_warning_event( + log_withheld, + platform_withheld, + ); + if tx.send(Ok(warning)).await.is_err() { + return; } - tail.extend(logs); } - if follow_events { - let events = state - .tracing_log_bus - .platform_event_bus - .tail(&sandbox_id, event_tail as usize); - if let Some(last) = events.last() { - platform_cutoff = platform_cutoff.max(last.seq); + // An event published while the stream opened, then trimmed + // from its buffer before the snapshot read it, is in neither + // the batch nor (past the cutoff) the live stream. + if logs_evicted || events_evicted { + let warning = crate::sandbox_watch::opening_eviction_warning_event( + logs_evicted, + events_evicted, + ); + if tx.send(Ok(warning)).await.is_err() { + return; } - tail.extend(events); } - tail.sort_by_key(|cursored| cursored.seq); - for cursored in tail { // Log filters; platform events carry no log fields and pass. if let Some(openshell_core::proto::sandbox_stream_event::Payload::Log( @@ -5470,6 +5528,593 @@ mod tests { assert_eq!(got, vec![1, 2, 3, 4]); } + /// Regression for the inverted coverage-floor clamp: a single followed + /// source with more buffered events than `log_tail_lines` must still + /// deliver its newest window. A source never withholds its own window, + /// and the older lines it excluded are ordinary tail truncation, so no + /// coverage-gap warning is due either. + #[tokio::test] + async fn initial_tail_delivers_a_truncated_single_source_window() { + use tokio_stream::StreamExt as _; + + let state = test_server_state().await; + let sandbox = test_sandbox("truncated", Vec::new()); + state.store.put_message(&sandbox).await.unwrap(); + let id = sandbox.object_id().to_string(); + + seed_log_lines(&state, &id, 5); // cursors 1..=5 + + let response = handle_watch_sandbox( + &state, + authed_request(WatchSandboxRequest { + sandbox: sandbox.object_name().to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + follow_logs: true, + log_tail_lines: 2, + ..Default::default() + }), + ) + .await + .unwrap(); + + let mut stream = response.into_inner(); + let snap = stream.next().await.unwrap().unwrap(); + assert!(snap.cursor.is_empty()); + + let mut got = Vec::new(); + for _ in 0..2 { + got.push(seq_of(&stream.next().await.unwrap().unwrap())); + } + assert_eq!(got, vec![4, 5]); + + let next = tokio::time::timeout(std::time::Duration::from_millis(200), stream.next()).await; + assert!(next.is_err(), "expected nothing further, got {next:?}"); + } + + /// A sibling's excluded backlog that is older than everything the deeper + /// source delivers imposes nothing: `event_tail: 0` leaves out platform + /// seq 1, but logs 2 and 3 both sit above it, so a resume from 3 skips + /// only an event outside the requested window. + #[tokio::test] + async fn initial_tail_delivers_logs_newer_than_a_sibling_sources_unreplayed_backlog() { + use tokio_stream::StreamExt as _; + + let state = test_server_state().await; + let sandbox = test_sandbox("floor", Vec::new()); + state.store.put_message(&sandbox).await.unwrap(); + let id = sandbox.object_id().to_string(); + + seed_platform_event(&state, &id, "e1"); // cursor 1, never replayed (event_tail: 0) + seed_log_lines(&state, &id, 2); // cursors 2, 3 + + let response = handle_watch_sandbox( + &state, + authed_request(WatchSandboxRequest { + sandbox: sandbox.object_name().to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + follow_logs: true, + follow_events: true, + // Default: no backlog requested from the platform bus. + event_tail: 0, + ..Default::default() + }), + ) + .await + .unwrap(); + + let mut stream = response.into_inner(); + let snap = stream.next().await.unwrap().unwrap(); + assert!(snap.cursor.is_empty()); + + let mut got = Vec::new(); + for _ in 0..2 { + got.push(seq_of(&stream.next().await.unwrap().unwrap())); + } + assert_eq!(got, vec![2, 3]); + + let next = tokio::time::timeout(std::time::Duration::from_millis(200), stream.next()).await; + assert!( + next.is_err(), + "expected no warning or further events, got {next:?}" + ); + } + + /// The A2 repro: a platform event (seq 2) that `event_tail: 0` leaves + /// out sits between two logs. Handing out log 3 would let the client + /// remember cursor 3, and a resume from 3 would exclude the platform + /// event forever -- `tail_after` reports no gap, since nothing was + /// evicted, it was just never sent. Platform's floor (2) instead cuts the + /// log window back to seq 3 and withholds log 1 with a warning. + #[tokio::test] + async fn initial_tail_withholds_logs_behind_a_sibling_sources_unreplayed_backlog() { + use openshell_core::proto::sandbox_stream_event::Payload; + use tokio_stream::StreamExt as _; + + let state = test_server_state().await; + let sandbox = test_sandbox("floorcut", Vec::new()); + state.store.put_message(&sandbox).await.unwrap(); + let id = sandbox.object_id().to_string(); + + seed_log_lines(&state, &id, 1); // cursor 1, withheld (at or below floor) + seed_platform_event(&state, &id, "e2"); // cursor 2, never replayed (event_tail: 0) + seed_log_lines(&state, &id, 1); // cursor 3, delivered + + let response = handle_watch_sandbox( + &state, + authed_request(WatchSandboxRequest { + sandbox: sandbox.object_name().to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + follow_logs: true, + follow_events: true, + event_tail: 0, + ..Default::default() + }), + ) + .await + .unwrap(); + + let mut stream = response.into_inner(); + let snap = stream.next().await.unwrap().unwrap(); + assert!(snap.cursor.is_empty()); + + // The client learns about the withheld log from an explicit warning + // rather than discovering it silently on a later reconnect. + let warning = stream.next().await.unwrap().unwrap(); + match warning.payload { + Some(Payload::Warning(w)) => { + assert!( + w.message + .contains("withheld 1 log line(s) and 0 platform event(s)"), + "message: {}", + w.message + ); + } + other => panic!("expected a coverage-gap warning, got {other:?}"), + } + assert!(warning.cursor.is_empty()); + + let delivered = stream.next().await.unwrap().unwrap(); + assert_eq!(seq_of(&delivered), 3); + + let next = tokio::time::timeout(std::time::Duration::from_millis(200), stream.next()).await; + assert!(next.is_err(), "expected nothing further, got {next:?}"); + } + + /// The coverage-gap warning discloses the withheld backlog once, up + /// front, rather than gating every later event: a live event published + /// after connect still reaches the client, even though its cursor sits + /// past the withheld platform backlog. The withheld events were + /// published before subscribe and can never arrive live, so refusing to + /// deliver anything past them would starve the stream indefinitely + /// instead of just disclosing the one, bounded gap. + #[tokio::test] + async fn live_delivery_proceeds_normally_after_the_coverage_gap_warning() { + use openshell_core::proto::sandbox_stream_event::Payload; + use tokio_stream::StreamExt as _; + + let state = test_server_state().await; + let sandbox = test_sandbox("floorlive", Vec::new()); + state.store.put_message(&sandbox).await.unwrap(); + let id = sandbox.object_id().to_string(); + + seed_log_lines(&state, &id, 1); // cursor 1, withheld (below critical_floor) + seed_platform_event(&state, &id, "e2"); // cursor 2, withheld (event_tail: 0) + + let response = handle_watch_sandbox( + &state, + authed_request(WatchSandboxRequest { + sandbox: sandbox.object_name().to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + follow_logs: true, + follow_events: true, + event_tail: 0, + ..Default::default() + }), + ) + .await + .unwrap(); + + let mut stream = response.into_inner(); + let snap = stream.next().await.unwrap().unwrap(); + assert!(snap.cursor.is_empty()); + + let warning = stream.next().await.unwrap().unwrap(); + assert!(matches!(warning.payload, Some(Payload::Warning(_)))); + + // Published after connect: a genuinely new live event, not part of + // the withheld backlog. It must still be delivered. + seed_log_lines(&state, &id, 1); // cursor 3, live + + let live = tokio::time::timeout(std::time::Duration::from_secs(2), stream.next()) + .await + .expect("live event should arrive") + .unwrap() + .unwrap(); + assert_eq!(seq_of(&live), 3); + } + + /// `event_tail: 0` excludes *both* of the platform bus's own events + /// (seqs 1 and 3); a log event (seq 2) sits interleaved between them and + /// is otherwise fully covered by `log_tail_lines`. Delivering it would let + /// the client remember cursor 2 while platform's seq 3 was never sent, so + /// the floor (3, the newest excluded platform seq) withholds it too. + #[tokio::test] + async fn initial_tail_withholds_an_interleaved_sibling_event_between_two_excluded_seqs() { + use tokio_stream::StreamExt as _; + + let state = test_server_state().await; + let sandbox = test_sandbox("interleavedfloor", Vec::new()); + state.store.put_message(&sandbox).await.unwrap(); + let id = sandbox.object_id().to_string(); + + seed_platform_event(&state, &id, "e1"); // cursor 1, withheld + seed_log_lines(&state, &id, 1); // cursor 2, must also be withheld + seed_platform_event(&state, &id, "e3"); // cursor 3, withheld + + let response = handle_watch_sandbox( + &state, + authed_request(WatchSandboxRequest { + sandbox: sandbox.object_name().to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + follow_logs: true, + follow_events: true, + event_tail: 0, + ..Default::default() + }), + ) + .await + .unwrap(); + + let mut stream = response.into_inner(); + let snap = stream.next().await.unwrap().unwrap(); + assert!(snap.cursor.is_empty()); + + let warning = stream.next().await.unwrap().unwrap(); + match warning.payload { + Some(openshell_core::proto::sandbox_stream_event::Payload::Warning(w)) => { + assert!(w.message.contains('1'), "message: {}", w.message); + } + other => panic!("expected a coverage-gap warning, got {other:?}"), + } + + // Nothing else: the log event must not slip through between + // platform's two excluded seqs. + let next = tokio::time::timeout(std::time::Duration::from_millis(200), stream.next()).await; + assert!( + next.is_err(), + "expected the interleaved log event to be withheld too, got {next:?}" + ); + } + + /// Windows that leave nothing out impose no floor: every buffered event + /// is delivered, as before A2. + #[tokio::test] + async fn initial_tail_unclamped_when_no_source_has_unreplayed_backlog() { + use tokio_stream::StreamExt as _; + + let state = test_server_state().await; + let sandbox = test_sandbox("nofloor", Vec::new()); + state.store.put_message(&sandbox).await.unwrap(); + let id = sandbox.object_id().to_string(); + + seed_platform_event(&state, &id, "e1"); // cursor 1 + seed_log_lines(&state, &id, 2); // cursors 2, 3 + + let response = handle_watch_sandbox( + &state, + authed_request(WatchSandboxRequest { + sandbox: sandbox.object_name().to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + follow_logs: true, + follow_events: true, + // Deep enough to cover the one platform event: no floor. + event_tail: 10, + ..Default::default() + }), + ) + .await + .unwrap(); + + let mut stream = response.into_inner(); + let snap = stream.next().await.unwrap().unwrap(); + assert!(snap.cursor.is_empty()); + + let mut got = Vec::new(); + for _ in 0..3 { + got.push(seq_of(&stream.next().await.unwrap().unwrap())); + } + assert_eq!(got, vec![1, 2, 3]); + } + + /// Open an initial watch following both sources with the given depths and + /// collect what the initial batch delivers: the coverage-gap warning + /// message, if any, and the delivered cursors in order. + async fn initial_tail_following_both( + state: &Arc, + sandbox: &Sandbox, + log_tail_lines: u32, + event_tail: u32, + ) -> (Option, Vec) { + use openshell_core::proto::sandbox_stream_event::Payload; + use tokio_stream::StreamExt as _; + + let response = handle_watch_sandbox( + state, + authed_request(WatchSandboxRequest { + sandbox: sandbox.object_name().to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + follow_logs: true, + follow_events: true, + log_tail_lines, + event_tail, + ..Default::default() + }), + ) + .await + .unwrap(); + + let mut stream = response.into_inner(); + let snap = stream.next().await.unwrap().unwrap(); + assert!(snap.cursor.is_empty()); + + let mut warning = None; + let mut delivered = Vec::new(); + while let Ok(Some(item)) = + tokio::time::timeout(std::time::Duration::from_millis(200), stream.next()).await + { + let event = item.unwrap(); + match event.payload { + Some(Payload::Warning(w)) => { + assert!(delivered.is_empty(), "warning must precede the batch"); + assert!(warning.replace(w.message).is_none(), "one warning at most"); + } + _ => delivered.push(seq_of(&event)), + } + } + (warning, delivered) + } + + /// Equal depths do not rule out trimming. Logs sit at cursors 1-2 and + /// platform events at 3-4; with both depths at 1 the platform window + /// leaves out 3, which is newer than log 2, so log 2 is withheld even + /// though `log_tail_lines` alone would have delivered it. + #[tokio::test] + async fn initial_tail_equal_depths_withhold_an_older_log_window() { + let state = test_server_state().await; + let sandbox = test_sandbox("equallogsfirst", Vec::new()); + state.store.put_message(&sandbox).await.unwrap(); + let id = sandbox.object_id().to_string(); + + seed_log_lines(&state, &id, 2); // cursors 1, 2 + seed_platform_event(&state, &id, "e3"); // cursor 3, left out by event_tail + seed_platform_event(&state, &id, "e4"); // cursor 4 + + let (warning, delivered) = initial_tail_following_both(&state, &sandbox, 1, 1).await; + let warning = warning.expect("withheld log 2 must be disclosed"); + assert!( + warning.contains("withheld 1 log line(s) and 0 platform event(s)"), + "message: {warning}" + ); + assert_eq!(delivered, vec![4]); + } + + /// The mirror case: platform events at cursors 1-2 and logs at 3-4. The + /// log window leaves out 3, which is newer than platform 2, so this time + /// the platform event is withheld. + #[tokio::test] + async fn initial_tail_equal_depths_withhold_an_older_platform_window() { + let state = test_server_state().await; + let sandbox = test_sandbox("equalplatformfirst", Vec::new()); + state.store.put_message(&sandbox).await.unwrap(); + let id = sandbox.object_id().to_string(); + + seed_platform_event(&state, &id, "e1"); // cursor 1 + seed_platform_event(&state, &id, "e2"); // cursor 2 + seed_log_lines(&state, &id, 2); // cursors 3, 4; 3 left out by log_tail_lines + + let (warning, delivered) = initial_tail_following_both(&state, &sandbox, 1, 1).await; + let warning = warning.expect("withheld platform event 2 must be disclosed"); + assert!( + warning.contains("withheld 0 log line(s) and 1 platform event(s)"), + "message: {warning}" + ); + assert_eq!(delivered, vec![4]); + } + + /// Equal depths over interleaved histories withhold nothing when neither + /// window leaves out an event newer than part of the other: logs at 1 and + /// 3, platform at 2 and 4, both depths 1. Each window's excluded event is + /// older than everything delivered, so both 3 and 4 arrive, no warning. + #[tokio::test] + async fn initial_tail_equal_depths_over_interleaved_histories_withhold_nothing() { + let state = test_server_state().await; + let sandbox = test_sandbox("equalinterleaved", Vec::new()); + state.store.put_message(&sandbox).await.unwrap(); + let id = sandbox.object_id().to_string(); + + seed_log_lines(&state, &id, 1); // cursor 1, left out by log_tail_lines + seed_platform_event(&state, &id, "e2"); // cursor 2, left out by event_tail + seed_log_lines(&state, &id, 1); // cursor 3 + seed_platform_event(&state, &id, "e4"); // cursor 4 + + let (warning, delivered) = initial_tail_following_both(&state, &sandbox, 1, 1).await; + assert_eq!(warning, None); + assert_eq!(delivered, vec![3, 4]); + } + + /// Open a watch following both sources and return the stream after its + /// status snapshot. + async fn watch_both( + state: &Arc, + sandbox: &Sandbox, + event_tail: u32, + resume_after_cursor: String, + ) -> WatchSandboxStream { + use tokio_stream::StreamExt as _; + + let response = handle_watch_sandbox( + state, + authed_request(WatchSandboxRequest { + sandbox: sandbox.object_name().to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + follow_logs: true, + follow_events: true, + event_tail, + resume_after_cursor, + ..Default::default() + }), + ) + .await + .unwrap(); + let mut stream = response.into_inner(); + let snap = stream.next().await.unwrap().unwrap(); + assert!(snap.cursor.is_empty(), "first item is the status snapshot"); + stream + } + + /// Collect what a stream yields until it goes quiet: warning messages and + /// resumable cursors, each in arrival order. + async fn drain_until_quiet(stream: &mut WatchSandboxStream) -> (Vec, Vec) { + use openshell_core::proto::sandbox_stream_event::Payload; + use tokio_stream::StreamExt as _; + + let mut warnings = Vec::new(); + let mut seqs = Vec::new(); + while let Ok(Some(item)) = + tokio::time::timeout(std::time::Duration::from_millis(200), stream.next()).await + { + let event = item.unwrap(); + match event.payload { + Some(Payload::Warning(w)) => warnings.push(w.message), + _ => seqs.push(seq_of(&event)), + } + } + (warnings, seqs) + } + + /// The reviewer's example on #3613. Both sources followed at default + /// depths; after the watch subscribes and before its snapshot, platform + /// event 1 and then log 2 are published. Both are queued live and the + /// snapshot's cutoff suppresses those copies, so the batch is their only + /// delivery: it must carry both, even though `event_tail` asked for no + /// platform history. Previously the depth bound left event 1 out and the + /// cutoff swallowed its live copy, with no warning, and a resume after 2 + /// could never recover it. + #[tokio::test] + async fn initial_tail_delivers_events_published_while_the_stream_opens() { + let state = test_server_state().await; + let sandbox = test_sandbox("opening", Vec::new()); + state.store.put_message(&sandbox).await.unwrap(); + let id = sandbox.object_id().to_string(); + + let hook_state = Arc::clone(&state); + let hook_id = id.clone(); + let mut fired = false; + state.tracing_log_bus.set_snapshot_hook(move |point| { + if point == crate::tracing_bus::SnapshotPoint::AfterSubscribe && !fired { + fired = true; + seed_platform_event(&hook_state, &hook_id, "e1"); // cursor 1 + seed_log_lines(&hook_state, &hook_id, 1); // cursor 2 + } + }); + + let mut stream = watch_both(&state, &sandbox, 0, String::new()).await; + let (warnings, delivered) = drain_until_quiet(&mut stream).await; + assert_eq!(delivered, vec![1, 2]); + assert!(warnings.is_empty(), "nothing was withheld: {warnings:?}"); + + // Live delivery continues past the batch. + seed_log_lines(&state, &id, 1); // cursor 3 + let (warnings, delivered) = drain_until_quiet(&mut stream).await; + assert_eq!(delivered, vec![3]); + assert!(warnings.is_empty()); + + // A reconnect from the highest cursor the batch handed out misses + // nothing that was retained below it. + let mut resumed = watch_both(&state, &sandbox, 0, cursor_token(&state, &id, 2)).await; + let (warnings, delivered) = drain_until_quiet(&mut resumed).await; + assert_eq!(delivered, vec![3]); + assert!(warnings.is_empty()); + } + + /// A publish forced between the initial snapshot's log read and platform + /// read. The snapshot holds the cursor-space lock across both, so the + /// publisher waits: the batch is exactly the seeded history, the raced + /// log and platform event arrive live in cursor order once each, and a + /// resume from the batch's highest cursor replays both. With separate + /// reads the batch would carry the raced platform event (4) without the + /// log published before it (3), which would then arrive after 4 or, after + /// a reconnect from 4, never. + #[tokio::test] + async fn initial_tail_publish_between_snapshot_reads_arrives_live_in_order() { + let state = test_server_state().await; + let sandbox = test_sandbox("racetail", Vec::new()); + state.store.put_message(&sandbox).await.unwrap(); + let id = sandbox.object_id().to_string(); + + seed_log_lines(&state, &id, 1); // cursor 1 + seed_platform_event(&state, &id, "e2"); // cursor 2 + let probe = crate::tracing_bus::race_publish_between_reads(&state.tracing_log_bus, &id); + + let mut stream = watch_both(&state, &sandbox, 10, String::new()).await; + let (warnings, delivered) = drain_until_quiet(&mut stream).await; + { + let mut probe = probe.lock().unwrap(); + probe.join_publisher(); + assert!(probe.fired, "the hook must run between the two reads"); + assert!(probe.lock_held_between_reads, "lock released between reads"); + assert!( + !probe.publisher_finished_between_reads, + "a publish landed between the log read and the platform read" + ); + } + assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}"); + assert_eq!(delivered, vec![1, 2, 3, 4], "batch, then 3 and 4 live"); + + let mut resumed = watch_both(&state, &sandbox, 10, cursor_token(&state, &id, 2)).await; + let (warnings, delivered) = drain_until_quiet(&mut resumed).await; + assert!(warnings.is_empty()); + assert_eq!(delivered, vec![3, 4]); + } + + /// The same forced interleaving on the resume path. The replay after + /// cursor 1 holds platform event 2 only; the raced log (3) and platform + /// event (4) arrive live, in order, after it. With separate reads the + /// replay would carry 4 ahead of 3, and a client that disconnected after 4 + /// would resume past 3 forever. + #[tokio::test] + async fn resume_publish_between_snapshot_reads_arrives_live_in_order() { + let state = test_server_state().await; + let sandbox = test_sandbox("raceresume", Vec::new()); + state.store.put_message(&sandbox).await.unwrap(); + let id = sandbox.object_id().to_string(); + + seed_log_lines(&state, &id, 1); // cursor 1 + seed_platform_event(&state, &id, "e2"); // cursor 2 + let probe = crate::tracing_bus::race_publish_between_reads(&state.tracing_log_bus, &id); + + let mut stream = watch_both(&state, &sandbox, 0, cursor_token(&state, &id, 1)).await; + let (warnings, delivered) = drain_until_quiet(&mut stream).await; + { + let mut probe = probe.lock().unwrap(); + probe.join_publisher(); + assert!(probe.fired, "the hook must run between the two reads"); + assert!(probe.lock_held_between_reads, "lock released between reads"); + assert!( + !probe.publisher_finished_between_reads, + "a publish landed between the log read and the platform read" + ); + } + assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}"); + assert_eq!(delivered, vec![2, 3, 4], "replay, then 3 and 4 live"); + + // Reconnecting from the replay's only cursor recovers both. + let mut resumed = watch_both(&state, &sandbox, 0, cursor_token(&state, &id, 2)).await; + let (warnings, delivered) = drain_until_quiet(&mut resumed).await; + assert!(warnings.is_empty()); + assert_eq!(delivered, vec![3, 4]); + } + #[tokio::test] async fn live_delivery_orders_events_across_sources_by_cursor() { use tokio_stream::StreamExt as _; @@ -5820,45 +6465,6 @@ mod tests { assert!(stream.next().await.is_none()); } - /// The guard behind the producer's post-replay epoch re-check. - /// - /// Validation and the two `tail_after` reads take their locks separately, so - /// a teardown plus a republish can swap the space in between and leave the - /// reads applying an old seq to a replacement's buffers -- `tail_after` only - /// compares numbers, so it reports no gap while skipping every replacement - /// event at or below that seq. The producer re-checks the epoch once both - /// tails are in hand and before emitting anything; this pins what that check - /// must answer. - /// - /// The interleaving itself is not reachable from a test: the producer runs - /// validation, both reads, and the re-check with no await in between, so - /// there is nothing to suspend it on. - #[tokio::test] - async fn cursor_space_is_rejects_a_replacement_space() { - let state = test_server_state().await; - let sandbox = test_sandbox("respace", Vec::new()); - state.store.put_message(&sandbox).await.unwrap(); - let id = sandbox.object_id().to_string(); - - seed_log_lines(&state, &id, 1); - let original = state.tracing_log_bus.cursor_space(&id).unwrap().epoch; - assert!(cursor_space_is(&state, &id, original)); - - // Teardown alone leaves no space to point into. - state.tracing_log_bus.remove(&id); - assert!(state.tracing_log_bus.cursor_space(&id).is_none()); - assert!(!cursor_space_is(&state, &id, original)); - - // The republish installs a replacement renumbered from 1. Its seqs - // overlap the retired space's, so only the epoch separates them. - seed_log_lines(&state, &id, 1); - let replacement = state.tracing_log_bus.cursor_space(&id).unwrap(); - assert_ne!(replacement.epoch, original); - assert_eq!(replacement.highest_seq, 1); - assert!(!cursor_space_is(&state, &id, original)); - assert!(cursor_space_is(&state, &id, replacement.epoch)); - } - #[tokio::test] async fn resume_from_reset_cursor_space_terminates_out_of_range() { use tokio_stream::StreamExt as _; diff --git a/crates/openshell-server/src/grpc/workspace.rs b/crates/openshell-server/src/grpc/workspace.rs index 56156f7d3f..9f3be64703 100644 --- a/crates/openshell-server/src/grpc/workspace.rs +++ b/crates/openshell-server/src/grpc/workspace.rs @@ -130,7 +130,7 @@ pub async fn resolve_workspace( let ws: Option = store .get_message_by_name("", &name) .await - .map_err(|e| Status::internal(format!("workspace lookup failed: {e}")))?; + .map_err(|e| super::persistence_error_to_status(e, "workspace lookup"))?; match ws { Some(ws) => { diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index 02c1ba16b3..140f372347 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -827,14 +827,21 @@ pub(crate) async fn run_server( .map_err(|error| Error::config(format!("middleware registration failed: {error}")))?, ); - let store = Arc::new( - Store::connect_with_pool_sizes( - database_url, - config.database_max_connections, - config.database_lock_max_connections, - ) - .await?, - ); + let shutdown = shutdown_signal(); + tokio::pin!(shutdown); + let mut health_listener = HealthListener::new(config.health_bind_address); + let Some(store) = connect_store( + &config, + database_url, + &mut health_listener, + shutdown.as_mut(), + ) + .await? + else { + info!("Shutdown signal received while waiting for the database; stopping gateway"); + return Ok(()); + }; + let store = Arc::new(store); let credentials = credentials::CredentialRuntime::from_config_file_with_store( &config, config_file.as_ref(), @@ -1153,26 +1160,7 @@ pub(crate) async fn run_server( // Create the multiplexed service let service = MultiplexService::new(state.clone()); - // Bind the unauthenticated health endpoint on a separate port when configured. - if let Some(health_bind_address) = config.health_bind_address { - let health_listener = TcpListener::bind(health_bind_address).await.map_err(|e| { - Error::transport(format!( - "failed to bind health port {health_bind_address}: {e}" - )) - })?; - info!(address = %health_bind_address, "Health server listening"); - // `health_router` returns immediately; the listener serves - // `Initializing → 503` until the background monitor publishes the - // first real probe outcome, so the endpoint is always responsive. - let router = health_router(store.clone()); - tokio::spawn(async move { - if let Err(e) = axum::serve(health_listener, router.into_make_service()).await { - error!("Health server error: {e}"); - } - }); - } else { - info!("Health server disabled"); - } + health_listener.serve(store.clone()).await?; // Bind the Prometheus metrics endpoint on a dedicated port when configured. if let Some((metrics_bind_address, prometheus_handle)) = metrics_endpoint { @@ -1311,7 +1299,7 @@ pub(crate) async fn run_server( provider_profile_sources::spawn_refresh(state.clone()); provider_refresh::spawn_refresh_worker(state.clone(), Duration::from_mins(1)); - shutdown_signal().await; + shutdown.await; info!("Shutdown signal received; stopping gateway"); state.gateway_shutting_down.store(true, Ordering::Release); state.supervisor_sessions.close_admission(); @@ -1607,6 +1595,112 @@ fn spawn_gateway_connection( } } +/// First delay between attempts to reach an unreachable database at startup. +const DATABASE_CONNECT_RETRY_INITIAL_DELAY: Duration = Duration::from_secs(1); + +/// Longest delay between attempts to reach an unreachable database at startup. +const DATABASE_CONNECT_RETRY_MAX_DELAY: Duration = Duration::from_secs(2); + +/// Connect to the database, retrying while it is unreachable. +/// +/// The first failed attempt binds the health endpoint and starts listening +/// for `shutdown`; a gateway running as PID 1 ignores SIGTERM until then. +/// Returns `None` if `shutdown` completes first. +async fn connect_store( + config: &Config, + database_url: &str, + health_listener: &mut HealthListener, + mut shutdown: std::pin::Pin<&mut impl Future>, +) -> Result> { + let mut delay = DATABASE_CONNECT_RETRY_INITIAL_DELAY; + let mut waiting = false; + loop { + let attempt = Store::try_connect_with_pool_sizes( + database_url, + config.database_max_connections, + config.database_lock_max_connections, + ); + let result = if waiting { + tokio::select! { + () = &mut shutdown => return Ok(None), + result = attempt => result?, + } + } else { + attempt.await? + }; + let error = match result { + Ok(store) => return Ok(Some(store)), + Err(error @ persistence::PersistenceError::Unavailable(_)) => error, + Err(error) => return Err(Error::execution(error.to_string())), + }; + if !waiting { + health_listener.bind().await?; + waiting = true; + } + warn!( + error = %error, + retry_in_ms = u64::try_from(delay.as_millis()).unwrap_or(u64::MAX), + "database unreachable; retrying" + ); + tokio::select! { + () = &mut shutdown => return Ok(None), + () = tokio::time::sleep(delay) => {} + } + delay = (delay * 2).min(DATABASE_CONNECT_RETRY_MAX_DELAY); + } +} + +/// The unauthenticated health endpoint, served on its own port when +/// configured. +struct HealthListener { + address: Option, + state: Option>, +} + +impl HealthListener { + const fn new(address: Option) -> Self { + Self { + address, + state: None, + } + } + + /// Start serving probes, with `/readyz` reporting `Initializing` until + /// [`Self::serve`] publishes the database's health. + async fn bind(&mut self) -> Result<()> { + let Some(address) = self.address else { + return Ok(()); + }; + if self.state.is_some() { + return Ok(()); + } + let listener = TcpListener::bind(address) + .await + .map_err(|e| Error::transport(format!("failed to bind health port {address}: {e}")))?; + info!(address = %address, "Health server listening"); + let (sender, receiver) = watch::channel(readiness::HealthState::Initializing); + let router = http::health_router_from_receiver(receiver); + tokio::spawn(async move { + if let Err(e) = axum::serve(listener, router.into_make_service()).await { + error!("Health server error: {e}"); + } + }); + self.state = Some(sender); + Ok(()) + } + + /// Bind if not yet serving and publish `store`'s health from now on. + async fn serve(mut self, store: Arc) -> Result<()> { + self.bind().await?; + if let Some(sender) = self.state { + readiness::DatabaseHealthMonitor::spawn_into(store, sender); + } else { + info!("Health server disabled"); + } + Ok(()) + } +} + async fn shutdown_signal() { #[cfg(unix)] { @@ -3978,6 +4072,157 @@ mod tests { "persisted sandbox start must not run before the gateway listener is bound" ); } + + /// A loopback address where nothing listens until a test binds it. + async fn reserved_loopback_address() -> SocketAddr { + TcpListener::bind("127.0.0.1:0") + .await + .expect("reserve a loopback port") + .local_addr() + .expect("loopback address") + } + + /// Status code of `GET path`, or `None` when nothing answers. + async fn http_status(address: SocketAddr, path: &str) -> Option { + let mut stream = openshell_core::net::connect_tcp_nodelay_best_effort(&[address]) + .await + .ok()?; + stream + .write_all( + format!("GET {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n") + .as_bytes(), + ) + .await + .ok()?; + let mut response = String::new(); + stream.read_to_string(&mut response).await.ok()?; + response.split_whitespace().nth(1)?.parse().ok() + } + + async fn wait_for_http_status(address: SocketAddr, path: &str, expected: u16) { + let deadline = tokio::time::Instant::now() + Duration::from_secs(20); + while http_status(address, path).await != Some(expected) { + assert!( + tokio::time::Instant::now() < deadline, + "{path} never returned {expected}" + ); + tokio::time::sleep(Duration::from_millis(100)).await; + } + } + + #[tokio::test] + async fn connect_store_serves_probes_while_the_database_is_unreachable() { + let url = crate::persistence::PostgresStore::refusing_url_for_tests().await; + let health_address = reserved_loopback_address().await; + let mut health = super::HealthListener::new(Some(health_address)); + let (stop, stopped) = tokio::sync::oneshot::channel::<()>(); + let shutdown = async { + let _ = stopped.await; + }; + tokio::pin!(shutdown); + let config = Config::new(None); + + let probe = async { + wait_for_http_status(health_address, "/healthz", 200).await; + assert_eq!(http_status(health_address, "/readyz").await, Some(503)); + stop.send(()).expect("connect_store is still waiting"); + }; + let (result, ()) = tokio::join!( + super::connect_store(&config, &url, &mut health, shutdown.as_mut()), + probe + ); + + assert!( + result.expect("shutdown is not an error").is_none(), + "shutdown ends the wait without a store" + ); + } + + #[tokio::test] + async fn connect_store_returns_a_configuration_error_without_retrying() { + let health_address = reserved_loopback_address().await; + let mut health = super::HealthListener::new(Some(health_address)); + let shutdown = std::future::pending::<()>(); + tokio::pin!(shutdown); + + let result = tokio::time::timeout( + Duration::from_secs(5), + super::connect_store( + &Config::new(None), + "postgres://openshell@127.0.0.1:1/openshell?sslmode=invalid", + &mut health, + shutdown.as_mut(), + ), + ) + .await + .expect("a configuration error is not retried"); + + assert!(result.is_err()); + assert_eq!(http_status(health_address, "/healthz").await, None); + } + + #[tokio::test] + #[ignore = "requires a disposable PostgreSQL database in OPENSHELL_TEST_POSTGRES_URL; run mise run test:rust:postgres"] + async fn postgres_connect_store_retries_until_the_database_answers() { + let schema = crate::persistence::test_postgres::TestSchema::create("startup").await; + let upstream = url::Url::parse(schema.url()).expect("parse the PostgreSQL URL"); + let upstream_addresses: Vec = tokio::net::lookup_host(( + upstream.host_str().expect("TCP PostgreSQL host"), + upstream.port().unwrap_or(5432), + )) + .await + .expect("resolve PostgreSQL") + .collect(); + let late_address = reserved_loopback_address().await; + let mut url = upstream.clone(); + url.set_host(Some("127.0.0.1")).expect("set host"); + url.set_port(Some(late_address.port())).expect("set port"); + let health_address = reserved_loopback_address().await; + let mut health = super::HealthListener::new(Some(health_address)); + let shutdown = std::future::pending::<()>(); + tokio::pin!(shutdown); + let config = Config::new(None); + + // PostgreSQL becomes reachable only after the gateway reports that it + // is waiting. + let late_database = async { + wait_for_http_status(health_address, "/healthz", 200).await; + assert_eq!(http_status(health_address, "/readyz").await, Some(503)); + let listener = TcpListener::bind(late_address) + .await + .expect("bind the late PostgreSQL address"); + loop { + let (mut client, _) = listener.accept().await.expect("accept"); + openshell_core::net::set_tcp_nodelay_best_effort(&client); + let mut upstream = + openshell_core::net::connect_tcp_nodelay_best_effort(&upstream_addresses) + .await + .expect("connect to PostgreSQL"); + tokio::spawn(async move { + let _ = tokio::io::copy_bidirectional(&mut client, &mut upstream).await; + }); + } + }; + let store = tokio::select! { + result = tokio::time::timeout( + Duration::from_mins(1), + super::connect_store(&config, url.as_str(), &mut health, shutdown.as_mut()), + ) => result + .expect("connect_store finishes once PostgreSQL answers") + .expect("connect") + .expect("no shutdown"), + () = late_database => unreachable!("the forwarder runs until the store connects"), + }; + store.ping().await.expect("ping the connected store"); + + health + .serve(Arc::new(store)) + .await + .expect("keep serving the bound health listener"); + wait_for_http_status(health_address, "/readyz", 200).await; + + schema.drop_schema().await; + } } #[cfg(test)] diff --git a/crates/openshell-server/src/persistence/mod.rs b/crates/openshell-server/src/persistence/mod.rs index 30280b96bb..d61451604d 100644 --- a/crates/openshell-server/src/persistence/mod.rs +++ b/crates/openshell-server/src/persistence/mod.rs @@ -69,6 +69,11 @@ pub enum PersistenceError { /// request's guarded writes did not run; the operation is safe to retry. #[error("mutation lock timeout: {0}")] LockTimeout(String), + /// The database is not serving. At request time, no connection was + /// available before the pool's acquire timeout, so the failed statement + /// was never sent. + #[error("database unavailable: {0}")] + Unavailable(String), } impl PersistenceError { @@ -475,6 +480,22 @@ impl Store { max_connections: Option, lock_max_connections: Option, ) -> CoreResult { + Self::try_connect_with_pool_sizes(url, max_connections, lock_max_connections) + .await? + .map_err(|e| CoreError::execution(e.to_string())) + } + + /// One attempt of [`Self::connect_with_pool_sizes`]. + /// + /// Configuration errors fail the outer result. Connection and migration + /// errors keep their [`PersistenceError`] kind in the inner result, so a + /// caller can retry [`PersistenceError::Unavailable`] until the database + /// is reachable. + pub(crate) async fn try_connect_with_pool_sizes( + url: &str, + max_connections: Option, + lock_max_connections: Option, + ) -> CoreResult> { if url.starts_with("postgres://") || url.starts_with("postgresql://") { if let Some(configured) = max_connections && configured < postgres::MIN_MAX_CONNECTIONS @@ -486,28 +507,25 @@ impl Store { postgres::MIN_MAX_CONNECTIONS ))); } - let store = PostgresStore::connect(url, max_connections, lock_max_connections) - .await - .map_err(|e| CoreError::execution(e.to_string()))?; - store - .migrate() - .await - .map_err(|e| CoreError::execution(e.to_string()))?; - Ok(Self::Postgres(store)) + Ok(async { + let store = + PostgresStore::connect(url, max_connections, lock_max_connections).await?; + store.migrate().await?; + Ok(Self::Postgres(store)) + } + .await) } else if url.starts_with("sqlite:") { if lock_max_connections.is_some() { tracing::warn!( "ignoring the configured mutation lock pool ceiling: SQLite has no lock pool" ); } - let store = SqliteStore::connect(url, max_connections) - .await - .map_err(|e| CoreError::execution(e.to_string()))?; - store - .migrate() - .await - .map_err(|e| CoreError::execution(e.to_string()))?; - Ok(Self::Sqlite(store)) + Ok(async { + let store = SqliteStore::connect(url, max_connections).await?; + store.migrate().await?; + Ok(Self::Sqlite(store)) + } + .await) } else { Err(CoreError::config(format!( "unsupported database URL scheme: {url}" @@ -1446,6 +1464,9 @@ pub fn current_time_ms() -> i64 { } fn map_db_error(error: &sqlx::Error) -> PersistenceError { + if matches!(error, sqlx::Error::PoolTimedOut) { + return PersistenceError::Unavailable(error.to_string()); + } if let sqlx::Error::Database(db) = error && db.is_unique_violation() { diff --git a/crates/openshell-server/src/persistence/mutation_lock_pg_tests.rs b/crates/openshell-server/src/persistence/mutation_lock_pg_tests.rs index 3bf37d1a33..4873925972 100644 --- a/crates/openshell-server/src/persistence/mutation_lock_pg_tests.rs +++ b/crates/openshell-server/src/persistence/mutation_lock_pg_tests.rs @@ -17,7 +17,7 @@ use super::mutation_lock::{ GLOBAL_MUTATION_LOCK_KEY, LOCK_CONNECTION_MIN_BUDGET, MUTATION_LOCK_POOL_MAX_CONNECTIONS, MUTATION_LOCK_TIMEOUT, MutationLockKey, MutationLockSet, }; -use super::postgres::LOCK_CONNECTION_RELEASE_TIMEOUT; +use super::postgres::{DATA_POOL_ACQUIRE_TIMEOUT, LOCK_CONNECTION_RELEASE_TIMEOUT}; use super::test_postgres::TestSchema; use super::{ DistributedMutationGuard, PersistenceError, PersistenceResult, PostgresStore, Store, @@ -1261,6 +1261,77 @@ async fn postgres_mutation_lock_connection_failure_is_not_a_lock_timeout() { fixture.finish(vec![saturated, unanswered]).await; } +/// A data-pool caller whose `PostgreSQL` stops answering fails as retryable +/// within the acquire timeout, and the next caller after it answers again +/// gets a fresh connection. +#[tokio::test] +#[ignore = "requires a disposable PostgreSQL database in OPENSHELL_TEST_POSTGRES_URL; run mise run test:rust:postgres"] +async fn postgres_data_pool_fails_unavailable_while_postgres_does_not_answer() { + let schema = TestSchema::create("unavailable").await; + let proxy = StallingProxy::start(schema.url()).await; + let store = Store::Postgres( + PostgresStore::connect_with_lock_pool_size(&proxy.url, None, 1) + .await + .expect("connect a store through the proxy"), + ); + store.ping().await.expect("ping before the network stalls"); + + proxy.stalled.send_replace(true); + let started = Instant::now(); + let result = store.ping().await; + let waited = started.elapsed(); + assert!( + matches!(result, Err(PersistenceError::Unavailable(_))), + "{result:?}" + ); + assert!( + waited < DATA_POOL_ACQUIRE_TIMEOUT + Duration::from_secs(2), + "ping waited {waited:?}" + ); + + proxy.stalled.send_replace(false); + tokio::time::timeout(Duration::from_secs(5), store.ping()) + .await + .expect("ping after the network recovers is bounded") + .expect("ping after the network recovers"); + + drop(store); + schema.drop_schema().await; +} + +/// Migrating against a read-only server, as a standby before promotion, +/// fails as retryable rather than as a fatal migration error. +#[tokio::test] +#[ignore = "requires a disposable PostgreSQL database in OPENSHELL_TEST_POSTGRES_URL; run mise run test:rust:postgres"] +async fn postgres_migrate_on_a_read_only_server_fails_unavailable() { + let schema = TestSchema::create("read_only").await; + let mut url = url::Url::parse(schema.url()).expect("parse the PostgreSQL URL"); + let pairs: Vec<(String, String)> = url + .query_pairs() + .map(|(key, value)| { + let value = if key == "options" { + format!("{value} -cdefault_transaction_read_only=on") + } else { + value.into_owned() + }; + (key.into_owned(), value) + }) + .collect(); + url.query_pairs_mut().clear().extend_pairs(pairs); + let store = PostgresStore::connect(url.as_str(), None, None) + .await + .expect("connect to the read-only server"); + + let result = store.migrate().await; + assert!( + matches!(result, Err(PersistenceError::Unavailable(_))), + "{result:?}" + ); + + drop(store); + schema.drop_schema().await; +} + #[tokio::test] #[ignore = "requires a disposable PostgreSQL database in OPENSHELL_TEST_POSTGRES_URL; run mise run test:rust:postgres"] async fn postgres_mutation_lock_unrelated_sandboxes_outpace_global_serialization() { diff --git a/crates/openshell-server/src/persistence/postgres.rs b/crates/openshell-server/src/persistence/postgres.rs index ca070bedd6..45511726fd 100644 --- a/crates/openshell-server/src/persistence/postgres.rs +++ b/crates/openshell-server/src/persistence/postgres.rs @@ -275,6 +275,46 @@ pub const DEFAULT_MAX_CONNECTIONS: u32 = 10; /// wait on itself until the acquire times out. pub const MIN_MAX_CONNECTIONS: u32 = 2; +/// How long a data-pool caller waits for a connection, including opening one. +pub(super) const DATA_POOL_ACQUIRE_TIMEOUT: Duration = Duration::from_secs(5); + +/// Whether a connect or migration failure means `PostgreSQL` is not serving +/// yet, so startup retries it. A bad host, credentials, or certificate stays +/// fatal. +fn is_unavailable_at_startup(error: &sqlx::Error) -> bool { + use std::io::ErrorKind; + match error { + sqlx::Error::PoolTimedOut => true, + sqlx::Error::Io(error) => matches!( + error.kind(), + ErrorKind::ConnectionRefused + | ErrorKind::ConnectionReset + | ErrorKind::ConnectionAborted + | ErrorKind::NotConnected + | ErrorKind::BrokenPipe + | ErrorKind::TimedOut + | ErrorKind::UnexpectedEof + | ErrorKind::HostUnreachable + | ErrorKind::NetworkUnreachable + ), + // read_only_sql_transaction, too_many_connections, admin_shutdown, + // crash_shutdown, cannot_connect_now. + sqlx::Error::Database(error) => matches!( + error.code().as_deref(), + Some("25006" | "53300" | "57P01" | "57P02" | "57P03") + ), + _ => false, + } +} + +fn map_startup_error(error: &sqlx::Error) -> PersistenceError { + if is_unavailable_at_startup(error) { + PersistenceError::Unavailable(error.to_string()) + } else { + map_db_error(error) + } +} + impl PostgresStore { /// Connect and size the data and lock pools, falling back to /// [`DEFAULT_MAX_CONNECTIONS`] and `MUTATION_LOCK_POOL_MAX_CONNECTIONS`. @@ -304,9 +344,10 @@ impl PostgresStore { ); let pool = PgPoolOptions::new() .max_connections(max_connections) + .acquire_timeout(DATA_POOL_ACQUIRE_TIMEOUT) .connect(url) .await - .map_err(|e| map_db_error(&e))?; + .map_err(|e| map_startup_error(&e))?; let lock_pool = PgPoolOptions::new() .max_connections(lock_pool_size) .min_connections(0) @@ -351,22 +392,30 @@ impl PostgresStore { POSTGRES_MIGRATOR .run(&self.pool) .await - .map_err(|e| map_migrate_error(&e))?; + .map_err(|e| match &e { + sqlx::migrate::MigrateError::Execute(error) + | sqlx::migrate::MigrateError::ExecuteMigration(error, _) + if is_unavailable_at_startup(error) => + { + PersistenceError::Unavailable(e.to_string()) + } + _ => map_migrate_error(&e), + })?; self.migrate_legacy_time_payloads().await } async fn migrate_legacy_time_payloads(&self) -> PersistenceResult<()> { - let mut transaction = self.pool.begin().await.map_err(|e| map_db_error(&e))?; + let mut transaction = self.pool.begin().await.map_err(|e| map_startup_error(&e))?; // Serialize this application-level data migration across gateway replicas. sqlx::query("SELECT pg_advisory_xact_lock(3052)") .execute(&mut *transaction) .await - .map_err(|e| map_db_error(&e))?; + .map_err(|e| map_startup_error(&e))?; let rows = sqlx::query("SELECT id, object_type, payload FROM objects ORDER BY id FOR UPDATE") .fetch_all(&mut *transaction) .await - .map_err(|e| map_db_error(&e))?; + .map_err(|e| map_startup_error(&e))?; for row in rows { let id: String = row.try_get("id").map_err(|e| map_db_error(&e))?; @@ -384,11 +433,14 @@ impl PostgresStore { .bind(id) .execute(&mut *transaction) .await - .map_err(|e| map_db_error(&e))?; + .map_err(|e| map_startup_error(&e))?; } } - transaction.commit().await.map_err(|e| map_db_error(&e)) + transaction + .commit() + .await + .map_err(|e| map_startup_error(&e)) } /// Verify the database is reachable by acquiring a pooled connection @@ -2283,6 +2335,55 @@ impl PostgresStore { mod tests { use super::*; + #[tokio::test] + async fn connect_to_a_refusing_server_fails_unavailable_within_the_acquire_timeout() { + let url = PostgresStore::refusing_url_for_tests().await; + let started = std::time::Instant::now(); + + let error = PostgresStore::connect(&url, None, None) + .await + .expect_err("nothing listens on the port"); + + assert!( + matches!(error, PersistenceError::Unavailable(_)), + "{error:?}" + ); + assert!( + started.elapsed() < DATA_POOL_ACQUIRE_TIMEOUT + Duration::from_secs(3), + "connect waited {:?}", + started.elapsed() + ); + } + + #[test] + fn startup_retries_io_errors_only_while_postgres_does_not_answer() { + use std::io::ErrorKind; + let io = |kind: ErrorKind| map_startup_error(&sqlx::Error::Io(kind.into())); + for kind in [ + ErrorKind::ConnectionRefused, + ErrorKind::ConnectionReset, + ErrorKind::TimedOut, + ErrorKind::HostUnreachable, + ErrorKind::UnexpectedEof, + ] { + assert!( + matches!(io(kind), PersistenceError::Unavailable(_)), + "{kind:?}" + ); + } + // An unresolvable host, a missing certificate file, a failed TLS handshake. + for kind in [ + ErrorKind::Other, + ErrorKind::NotFound, + ErrorKind::InvalidData, + ] { + assert!( + matches!(io(kind), PersistenceError::Database(_)), + "{kind:?}" + ); + } + } + #[tokio::test] async fn lock_connection_timeout_is_contention_only_if_the_pool_filled_during_the_wait() { let store = PostgresStore::connect_lazy_for_tests( diff --git a/crates/openshell-server/src/readiness.rs b/crates/openshell-server/src/readiness.rs index 7ca21605a8..fed1ffd102 100644 --- a/crates/openshell-server/src/readiness.rs +++ b/crates/openshell-server/src/readiness.rs @@ -129,6 +129,17 @@ impl DatabaseHealthMonitor { Self { receiver: rx } } + /// Spawn a monitor with the production defaults that publishes to a + /// channel whose receivers already serve probes. + pub fn spawn_into(store: Arc, sender: watch::Sender) { + tokio::spawn(monitor_loop( + store, + sender, + DEFAULT_CHECK_INTERVAL, + DEFAULT_CHECK_TIMEOUT, + )); + } + /// Subscribe to state updates. /// /// Returned receivers always observe the latest value with no lock diff --git a/crates/openshell-server/src/sandbox_watch.rs b/crates/openshell-server/src/sandbox_watch.rs index 679912d352..c79c81d399 100644 --- a/crates/openshell-server/src/sandbox_watch.rs +++ b/crates/openshell-server/src/sandbox_watch.rs @@ -154,6 +154,81 @@ pub fn lag_warning_event(n: u64) -> openshell_core::proto::SandboxStreamEvent { } } +/// Build the warning payload emitted when the cross-source coverage floor +/// withholds events on connect. +/// +/// Unlike broadcast lag, nothing was evicted here: the withheld events are +/// still sitting in a bus's tail, excluded only because a sibling source's +/// shallower replay depth means handing them out would let the client's +/// single shared cursor imply coverage that sibling can't back. They won't +/// be resent later in this stream -- a resume replays past the cursor this +/// connect settles on, and these are at or below it by construction -- so +/// the client needs to know now, not discover it silently after a reconnect. +pub fn coverage_gap_warning(log_withheld: usize, platform_withheld: usize) -> SandboxStreamWarning { + SandboxStreamWarning { + message: format!( + "resume cursor cannot cover every followed source's backlog; withheld {log_withheld} \ + log line(s) and {platform_withheld} platform event(s) that a shallower sibling source \ + had not replayed. Restart the watch with a deeper log_tail_lines/event_tail if you need them; \ + they will not be recoverable from this cursor." + ), + } +} + +/// Wrap [`coverage_gap_warning`] in a `SandboxStreamEvent` ready to send. +pub fn coverage_gap_warning_event( + log_withheld: usize, + platform_withheld: usize, +) -> openshell_core::proto::SandboxStreamEvent { + use openshell_core::proto::sandbox_stream_event::Payload; + openshell_core::proto::SandboxStreamEvent { + payload: Some(Payload::Warning(coverage_gap_warning( + log_withheld, + platform_withheld, + ))), + // Warnings are not part of the resumable log/platform sequence. + cursor: String::new(), + } +} + +/// Build the warning payload emitted when a bus evicted events published +/// while the watch was opening, before the initial snapshot could read them. +/// +/// Those events were queued on the live receivers too, but the snapshot's +/// cutoff suppresses live copies at or below its high-water mark, so they are +/// in neither the batch nor the live stream. The bus cannot say how many were +/// lost -- its seqs interleave with the sibling source's -- only which source. +pub fn opening_eviction_warning(logs: bool, platform_events: bool) -> SandboxStreamWarning { + let what = match (logs, platform_events) { + (true, true) => "log lines and platform events", + (true, false) => "log lines", + _ => "platform events", + }; + SandboxStreamWarning { + message: format!( + "some {what} published while this watch was opening were evicted from the \ + gateway buffer before they could be sent; they are not recoverable from any \ + cursor this stream hands out." + ), + } +} + +/// Wrap [`opening_eviction_warning`] in a `SandboxStreamEvent` ready to send. +pub fn opening_eviction_warning_event( + logs: bool, + platform_events: bool, +) -> openshell_core::proto::SandboxStreamEvent { + use openshell_core::proto::sandbox_stream_event::Payload; + openshell_core::proto::SandboxStreamEvent { + payload: Some(Payload::Warning(opening_eviction_warning( + logs, + platform_events, + ))), + // Warnings are not part of the resumable log/platform sequence. + cursor: String::new(), + } +} + #[cfg(test)] mod tests { use super::*; @@ -226,6 +301,59 @@ mod tests { } } + #[test] + fn coverage_gap_warning_reports_withheld_counts() { + let warning = coverage_gap_warning(2, 1); + assert!( + warning.message.contains('2'), + "message: {}", + warning.message + ); + assert!( + warning.message.contains('1'), + "message: {}", + warning.message + ); + } + + #[test] + fn opening_eviction_warning_names_the_affected_sources() { + assert!( + opening_eviction_warning(true, false) + .message + .contains("some log lines ") + ); + assert!( + opening_eviction_warning(false, true) + .message + .contains("some platform events ") + ); + assert!( + opening_eviction_warning(true, true) + .message + .contains("log lines and platform events") + ); + assert!( + opening_eviction_warning_event(true, false) + .cursor + .is_empty() + ); + } + + #[test] + fn coverage_gap_warning_event_wraps_warning_payload_with_empty_cursor() { + use openshell_core::proto::sandbox_stream_event::Payload; + let evt = coverage_gap_warning_event(2, 1); + assert!(evt.cursor.is_empty()); + match evt.payload { + Some(Payload::Warning(w)) => { + assert!(w.message.contains('2')); + assert!(w.message.contains('1')); + } + other => panic!("expected Warning payload, got {other:?}"), + } + } + // Broadcast lag is recoverable at the tokio layer: after `Lagged`, the same // receiver keeps yielding the oldest surviving messages instead of closing. #[tokio::test] diff --git a/crates/openshell-server/src/tracing_bus.rs b/crates/openshell-server/src/tracing_bus.rs index 92d30f5571..02f36dbe18 100644 --- a/crates/openshell-server/src/tracing_bus.rs +++ b/crates/openshell-server/src/tracing_bus.rs @@ -22,6 +22,8 @@ pub struct TracingLogBus { inner: Arc>, pub(crate) platform_event_bus: PlatformEventBus, seq: SeqAllocator, + #[cfg(test)] + hooks: SnapshotHooks, } #[derive(Debug, Clone)] @@ -29,6 +31,10 @@ struct Inner { per_id: HashMap, } +/// Result of a resumable-source replay read: the events after the requested +/// cursor, or the gap that made them unreplayable. +pub(crate) type ReplayResult = Result, ResumeGap>; + /// A buffered or broadcast stream event paired with its raw sequence number. /// /// The wire `SandboxStreamEvent.cursor` is an opaque token; ordering decisions @@ -182,6 +188,50 @@ fn tail_after_impl( Ok(res) } +/// Split a tail into what a bounded request returns and the coverage floor +/// that bound leaves behind. +/// +/// `max` may return fewer events than the tail currently retains; the ones +/// excluded are the oldest, at the front. The floor is the *newest* excluded +/// event's seq: this bus returned every event it retains above the floor and +/// none at or below it. `0` means `max` covered everything currently +/// retained, so there's nothing this call withheld. +/// +/// Because the excluded set is a prefix of this bus's retained deque, the +/// floor is the only boundary a caller needs: an event from any followed +/// source with a seq above the floor is safe to hand out as far as this bus +/// is concerned, and one at or below it may sit next to an event this bus +/// withheld. Excluding events below the delivered window is ordinary tail +/// truncation, the same as a single-source `log_tail_lines`. +/// +/// This is what lets a caller answer "is a cursor from this batch safe to +/// resume from," which `tail_after`'s eviction-only gap check can't: a +/// shallow request and a bus eviction both withhold events, but only the +/// eviction leaves a trace in `last_trimmed_seq`. +/// +/// `keep_after` exempts the newest events from `max`: every retained event +/// with a seq above it is returned regardless of depth. The watch handler +/// passes the high-water mark it recorded when it subscribed, so an event +/// published while the stream was opening -- one that is also queued on the +/// live receiver, where the snapshot's cutoff will suppress it -- is always +/// delivered in the batch rather than trimmed as history. The returned set is +/// still a suffix of the tail, so the floor keeps its meaning, and it never +/// exceeds `keep_after`. +fn tail_with_floor_impl( + tail: &VecDeque, + max: usize, + keep_after: u64, +) -> (Vec, u64) { + let first_unseen = tail.partition_point(|cursored| cursored.seq <= keep_after); + let excluded = tail.len().saturating_sub(max).min(first_unseen); + let floor = excluded + .checked_sub(1) + .and_then(|newest_excluded| tail.get(newest_excluded)) + .map_or(0, |cursored| cursored.seq); + let events = tail.iter().skip(excluded).cloned().collect(); + (events, floor) +} + impl Default for TracingLogBus { fn default() -> Self { Self::new() @@ -200,6 +250,8 @@ impl TracingLogBus { })), platform_event_bus: PlatformEventBus::new(seq.clone()), seq, + #[cfg(test)] + hooks: SnapshotHooks::default(), } } @@ -274,18 +326,6 @@ impl TracingLogBus { self.seq.space(sandbox_id) } - pub(crate) fn tail_after( - &self, - sandbox_id: &str, - after_seq: u64, - ) -> Result, ResumeGap> { - let inner = self.inner.lock().expect("tracing bus lock poisoned"); - inner.per_id.get(sandbox_id).map_or_else( - || Ok(Vec::new()), - |per| tail_after_impl(&per.tail, per.last_trimmed_seq, after_seq), - ) - } - /// Publish a log line from an external source (e.g., sandbox push). /// /// Injects the line into the same broadcast channel and tail buffer @@ -329,6 +369,364 @@ impl TracingLogBus { } } } + + /// Validate a resume cursor's epoch and read both resumable buses, all + /// under one lock hold. + /// + /// Folding validation into the same hold as the reads is what makes this + /// atomic against a teardown: checking the epoch first and reading after + /// -- even via a single call that itself locks once internally -- still + /// leaves a window between the two lock acquisitions for a `remove` plus + /// a republish to retire the validated space and install a replacement + /// numbered from 1. A read landing after that would apply the old + /// space's seq to the replacement's buffers and find no gap, since + /// `tail_after` only compares numbers. Locking the cursor space before + /// checking the epoch -- the same lock every publish path holds across + /// its own tail insert -- closes that window entirely, and also makes + /// the two bus reads atomic against any publish on either bus, for the + /// same reason. + pub(crate) fn snapshot_after( + &self, + sandbox_id: &str, + expected_epoch: Uuid, + log_after: u64, + platform_after: u64, + want_log: bool, + want_platform: bool, + ) -> ResumeSnapshot { + let spaces = self.seq.lock(); + let Some(space) = spaces.get(sandbox_id) else { + return ResumeSnapshot::SpaceGone; + }; + if space.epoch != expected_epoch { + return ResumeSnapshot::SpaceGone; + } + // Right space, but ahead of anything it has issued: only a + // fabricated cursor gets here. Reject rather than accept a cutoff no + // event can ever exceed. + if log_after.max(platform_after) > space.next.saturating_sub(1) { + return ResumeSnapshot::CursorAhead; + } + + let log = want_log.then(|| { + let inner = self.inner.lock().expect("tracing bus lock poisoned"); + inner.per_id.get(sandbox_id).map_or_else( + || Ok(Vec::new()), + |per| tail_after_impl(&per.tail, per.last_trimmed_seq, log_after), + ) + }); + #[cfg(test)] + self.hooks.run(SnapshotPoint::BetweenReads); + let platform = want_platform.then(|| { + let inner = self + .platform_event_bus + .inner + .lock() + .expect("platform event bus lock poisoned"); + inner.per_id.get(sandbox_id).map_or_else( + || Ok(Vec::new()), + |per| tail_after_impl(&per.tail, per.last_trimmed_seq, platform_after), + ) + }); + ResumeSnapshot::Read(log, platform) + } + + /// Read both bounded initial tails and the space's high-water mark, all + /// under one lock hold. + /// + /// `None` skips a source. Holding the cursor-space lock -- the one every + /// publish holds across its tail insert and broadcast send -- means no + /// publish can land between the two reads, and `highest_seq` is exactly + /// the boundary between what these tails could see and what can only + /// arrive live. Every event at or below it was already buffered when the + /// tails were read, so the caller can suppress live copies at or below it + /// without dropping anything the tails could not have covered. + /// + /// `subscribed_at` is the mark [`Self::subscribe_resumable`] recorded. + /// Events above it were published after the caller's live receivers + /// existed, so they are queued live as well as buffered here; the caller + /// suppresses those live copies, which means this batch is their only + /// delivery. Each source therefore returns all of them regardless of its + /// depth (see `tail_with_floor_impl`), and reports whether its buffer + /// already evicted any of them before the read, in which case they are + /// lost from both paths and the caller must say so. + /// + /// Reads never mint a space: with none, both tails are empty and + /// `highest_seq` is `0`. + pub(crate) fn snapshot_tail( + &self, + sandbox_id: &str, + log_max: Option, + platform_max: Option, + subscribed_at: u64, + ) -> TailSnapshot { + let spaces = self.seq.lock(); + let highest_seq = spaces + .get(sandbox_id) + .map_or(0, |space| space.next.saturating_sub(1)); + + let read = |per: Option<&PerSandbox>, max: usize| { + per.map_or_else(SourceTail::default, |per| { + let (events, floor) = tail_with_floor_impl(&per.tail, max, subscribed_at); + SourceTail { + events, + floor, + evicted_while_opening: per.last_trimmed_seq > subscribed_at, + } + }) + }; + + let log = log_max.map_or_else(SourceTail::default, |max| { + let inner = self.inner.lock().expect("tracing bus lock poisoned"); + read(inner.per_id.get(sandbox_id), max) + }); + #[cfg(test)] + self.hooks.run(SnapshotPoint::BetweenReads); + let platform = platform_max.map_or_else(SourceTail::default, |max| { + let inner = self + .platform_event_bus + .inner + .lock() + .expect("platform event bus lock poisoned"); + read(inner.per_id.get(sandbox_id), max) + }); + drop(spaces); + + TailSnapshot { + log, + platform, + highest_seq, + } + } + + /// Subscribe live receivers for the resumable sources and record the + /// space's high-water mark, all under one cursor-space lock hold. + /// + /// Every publish holds this lock across its broadcast send, so the mark + /// splits the space exactly: an event at or below it was sent before + /// these receivers existed and can only be found in a buffer, and one + /// above it is queued on them. [`Self::snapshot_tail`] uses the mark to + /// keep every queued-and-buffered event in the initial batch. + /// + /// The lock order is allocator -> bus map, the same as publish. Never + /// mints a space: with none, the mark is `0`. + pub(crate) fn subscribe_resumable( + &self, + sandbox_id: &str, + want_log: bool, + want_platform: bool, + ) -> ResumableSubscription { + let spaces = self.seq.lock(); + let subscription = ResumableSubscription { + log_rx: want_log.then(|| self.subscribe(sandbox_id)), + platform_rx: want_platform.then(|| self.platform_event_bus.subscribe(sandbox_id)), + subscribed_at: spaces + .get(sandbox_id) + .map_or(0, |space| space.next.saturating_sub(1)), + }; + drop(spaces); + #[cfg(test)] + self.hooks.run(SnapshotPoint::AfterSubscribe); + subscription + } + + /// Install a hook that runs at each [`SnapshotPoint`], replacing any + /// previous one. Shared by every clone of this bus. + #[cfg(test)] + pub(crate) fn set_snapshot_hook(&self, hook: impl FnMut(SnapshotPoint) + Send + 'static) { + *self.hooks.0.lock().expect("snapshot hook lock poisoned") = Some(Box::new(hook)); + } + + /// Whether the cursor-space lock is held right now, by anyone. + #[cfg(test)] + pub(crate) fn cursor_space_locked(&self) -> bool { + matches!( + self.seq.inner.try_lock(), + Err(std::sync::TryLockError::WouldBlock) + ) + } +} + +/// Receivers from [`TracingLogBus::subscribe_resumable`] and the mark that +/// separates buffered-only history from events queued on them. +pub(crate) struct ResumableSubscription { + pub(crate) log_rx: Option>, + pub(crate) platform_rx: Option>, + /// Highest seq the space had issued when the receivers subscribed; `0` + /// with no space. + pub(crate) subscribed_at: u64, +} + +/// Points inside the snapshot paths where a test hook runs. +#[cfg(test)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum SnapshotPoint { + /// After `subscribe_resumable` subscribed and released the lock. + AfterSubscribe, + /// Inside `snapshot_tail` or `snapshot_after`, after the log read and + /// before the platform read, with the cursor-space lock still held. + BetweenReads, +} + +/// Test-only hook slot. Production builds carry no field at all. +#[cfg(test)] +#[derive(Clone, Default)] +#[allow(clippy::type_complexity)] +struct SnapshotHooks(Arc>>>); + +#[cfg(test)] +impl SnapshotHooks { + fn run(&self, point: SnapshotPoint) { + if let Some(hook) = self.0.lock().expect("snapshot hook lock poisoned").as_mut() { + hook(point); + } + } +} + +#[cfg(test)] +impl std::fmt::Debug for SnapshotHooks { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("SnapshotHooks") + } +} + +/// One source's part of a [`TailSnapshot`]. An unread source has no events, +/// a floor of `0`, and nothing evicted. +#[derive(Debug, Default)] +pub(crate) struct SourceTail { + pub(crate) events: Vec, + /// What `tail_with_floor_impl` reports for this source. + pub(crate) floor: u64, + /// The buffer trimmed at least one event published after the receivers + /// subscribed, before the snapshot could read it. Its live copy is + /// suppressed with the rest of the batch's range, so it is lost. + pub(crate) evicted_while_opening: bool, +} + +/// What [`race_publish_between_reads`] observed from inside the snapshot. +#[cfg(test)] +#[derive(Debug, Default)] +pub(crate) struct RaceProbe { + /// The hook ran at all, i.e. a snapshot reached its second read. + pub(crate) fired: bool, + /// The cursor-space lock was held between the two reads. + pub(crate) lock_held_between_reads: bool, + /// The competing publisher completed while the snapshot sat between its + /// reads. With both reads under one lock hold it cannot. + pub(crate) publisher_finished_between_reads: bool, + publisher: Option>, +} + +#[cfg(test)] +impl RaceProbe { + /// Wait for the competing publisher to finish both publishes. + pub(crate) fn join_publisher(&mut self) { + if let Some(handle) = self.publisher.take() { + handle.join().expect("publisher thread panicked"); + } + } +} + +/// Force the interleaving a snapshot must be atomic against: when the next +/// snapshot on `bus` sits between its log read and its platform read, start a +/// publisher that publishes one log line and then one platform event for +/// `sandbox_id`, and give it time to finish before letting the snapshot read +/// the platform bus. +/// +/// With separate reads, the publisher completes in that pause and the platform +/// read picks up its event while the log read missed the log line published +/// just before it. With one lock hold, the publisher blocks until the snapshot +/// is done, and both events land after it. The hook records what it saw +/// rather than asserting, because it may run inside a gateway producer task +/// where a panic would surface only as a truncated stream. +/// +/// The hook fires once. +#[cfg(test)] +pub(crate) fn race_publish_between_reads( + bus: &TracingLogBus, + sandbox_id: &str, +) -> Arc> { + use std::sync::mpsc; + use std::time::Duration; + + let probe = Arc::new(Mutex::new(RaceProbe::default())); + let hook_probe = Arc::clone(&probe); + let publisher_bus = bus.clone(); + let lock_bus = bus.clone(); + let sandbox_id = sandbox_id.to_string(); + // Closure state, not the probe, decides whether the hook already fired: + // a later snapshot must not touch the probe, which the test may be + // holding while it inspects the result. + let mut fired = false; + bus.set_snapshot_hook(move |point| { + if point != SnapshotPoint::BetweenReads || fired { + return; + } + fired = true; + let mut probe = hook_probe.lock().expect("race probe lock poisoned"); + probe.fired = true; + probe.lock_held_between_reads = lock_bus.cursor_space_locked(); + + let (started_tx, started_rx) = mpsc::channel(); + let (done_tx, done_rx) = mpsc::channel(); + let bus = publisher_bus.clone(); + let id = sandbox_id.clone(); + probe.publisher = Some(std::thread::spawn(move || { + started_tx.send(()).expect("hook waits for start"); + bus.publish_external(SandboxLogLine { + sandbox_id: id.clone(), + level: "INFO".to_string(), + target: "race".to_string(), + message: "raced log".to_string(), + source: "gateway".to_string(), + ..Default::default() + }); + bus.platform_event_bus.publish( + &id, + SandboxStreamEvent { + payload: Some(openshell_core::proto::sandbox_stream_event::Payload::Event( + openshell_core::proto::PlatformEvent { + source: "race".to_string(), + r#type: "Normal".to_string(), + reason: "raced".to_string(), + message: "raced event".to_string(), + ..Default::default() + }, + )), + cursor: String::new(), + }, + ); + let _ = done_tx.send(()); + })); + started_rx.recv().expect("publisher thread starts"); + // Long enough for an unblocked publisher to finish two publishes many + // times over. A blocked one never does, so the correct implementation + // always pays the full wait; a broken one fails reliably. + probe.publisher_finished_between_reads = + done_rx.recv_timeout(Duration::from_millis(300)).is_ok(); + }); + probe +} + +/// Outcome of [`TracingLogBus::snapshot_tail`]. +pub(crate) struct TailSnapshot { + pub(crate) log: SourceTail, + pub(crate) platform: SourceTail, + /// Highest seq the space had issued at the lock hold; `0` with no space. + pub(crate) highest_seq: u64, +} + +/// Outcome of [`TracingLogBus::snapshot_after`]. +pub(crate) enum ResumeSnapshot { + /// No cursor space exists for this sandbox, or its epoch no longer + /// matches the resume cursor's -- the space that issued it is gone. + SpaceGone, + /// The right space, but the cursor is ahead of anything it has issued. + /// Only a fabricated cursor reaches this. + CursorAhead, + /// Both requested reads succeeded at the lock hold; each is + /// independently either the replay or the gap that made it unreplayable. + Read(Option, Option), } #[derive(Debug, Clone)] @@ -592,6 +990,324 @@ mod tests { assert!(tail_after_impl(&tail, 0, 99).expect("ok").is_empty()); } + #[test] + fn tail_with_floor_impl_empty_tail_returns_zero_floor() { + let tail = VecDeque::new(); + let (events, floor) = tail_with_floor_impl(&tail, 10, u64::MAX); + assert!(events.is_empty()); + assert_eq!(floor, 0); + } + + #[test] + fn tail_with_floor_impl_max_covers_everything_returns_zero_floor() { + let tail = tail_of(1, 5); + let (events, floor) = tail_with_floor_impl(&tail, 5, u64::MAX); + assert_eq!(cursors(&events), vec![1, 2, 3, 4, 5]); + assert_eq!(floor, 0); + + // Asking for more than exists is the same as asking for everything. + let (events, floor) = tail_with_floor_impl(&tail, 99, u64::MAX); + assert_eq!(cursors(&events), vec![1, 2, 3, 4, 5]); + assert_eq!(floor, 0); + } + + #[test] + fn tail_with_floor_impl_truncated_reports_newest_excluded_seq() { + let tail = tail_of(1, 5); + // Newest 2 returned (4, 5); 1..=3 excluded, newest of those is 3. + // Every returned event sits above the floor, so a bus never + // withholds its own delivered window. + let (events, floor) = tail_with_floor_impl(&tail, 2, u64::MAX); + assert_eq!(cursors(&events), vec![4, 5]); + assert_eq!(floor, 3); + assert!(events.iter().all(|cursored| cursored.seq > floor)); + } + + #[test] + fn tail_with_floor_impl_zero_max_excludes_everything() { + let tail = tail_of(1, 5); + // Nothing returned; every event is excluded, so the floor is the + // newest of them, 5. + let (events, floor) = tail_with_floor_impl(&tail, 0, u64::MAX); + assert!(events.is_empty()); + assert_eq!(floor, 5); + } + + /// The resume read is atomic against a publish on either bus. The forced + /// interleaving publishes a log line and then a platform event while + /// `snapshot_after` sits between its two reads. If the reads were + /// separate, the platform read would return the event while the log read + /// had already missed the line before it: a client handed that event's + /// cursor would resume past the line forever. Under one lock hold the + /// publisher waits, the replay holds neither, and both arrive live in + /// cursor order and are replayable from the replay's last cursor. + #[test] + fn snapshot_after_holds_the_lock_across_a_publish_between_reads() { + let bus = TracingLogBus::new(); + let sandbox_id = "sb-race-resume"; + bus.publish_external(make_log_event(sandbox_id, "l1")); // seq 1 + bus.platform_event_bus.publish(sandbox_id, stream_event(0)); // seq 2 + let epoch = bus.cursor_space(sandbox_id).expect("space exists").epoch; + let sub = bus.subscribe_resumable(sandbox_id, true, true); + let (mut log_rx, mut platform_rx) = (sub.log_rx.unwrap(), sub.platform_rx.unwrap()); + + let probe = race_publish_between_reads(&bus, sandbox_id); + let ResumeSnapshot::Read(log, platform) = + bus.snapshot_after(sandbox_id, epoch, 0, 0, true, true) + else { + panic!("expected a successful read"); + }; + { + let mut probe = probe.lock().unwrap(); + probe.join_publisher(); + assert!(probe.fired, "the hook must run between the two reads"); + assert!(probe.lock_held_between_reads, "lock released between reads"); + assert!( + !probe.publisher_finished_between_reads, + "a publish landed between the log read and the platform read" + ); + } + + let log = log.expect("followed").expect("no gap"); + let platform = platform.expect("followed").expect("no gap"); + assert_eq!(cursors(&log), vec![1]); + assert_eq!(cursors(&platform), vec![2]); + + // The raced events come live, in order, above the replay. + assert_eq!(log_rx.try_recv().expect("raced log is live").seq, 3); + assert_eq!(platform_rx.try_recv().expect("raced event is live").seq, 4); + + // Resuming from the replay's highest cursor recovers both. + let ResumeSnapshot::Read(log, platform) = + bus.snapshot_after(sandbox_id, epoch, 2, 2, true, true) + else { + panic!("expected a successful read"); + }; + assert_eq!(cursors(&log.unwrap().unwrap()), vec![3]); + assert_eq!(cursors(&platform.unwrap().unwrap()), vec![4]); + } + + /// The same forced interleaving on the initial path. Under one lock hold + /// the raced events are newer than the snapshot's high-water mark, so the + /// caller's cutoff lets their live copies through, in order. + #[test] + fn snapshot_tail_holds_the_lock_across_a_publish_between_reads() { + let bus = TracingLogBus::new(); + let sandbox_id = "sb-race-tail"; + bus.publish_external(make_log_event(sandbox_id, "l1")); // seq 1 + bus.platform_event_bus.publish(sandbox_id, stream_event(0)); // seq 2 + let sub = bus.subscribe_resumable(sandbox_id, true, true); + let (mut log_rx, mut platform_rx) = (sub.log_rx.unwrap(), sub.platform_rx.unwrap()); + + let probe = race_publish_between_reads(&bus, sandbox_id); + let snap = bus.snapshot_tail(sandbox_id, Some(10), Some(10), sub.subscribed_at); + let mut probe = probe.lock().unwrap(); + probe.join_publisher(); + assert!(probe.fired, "the hook must run between the two reads"); + assert!(probe.lock_held_between_reads, "lock released between reads"); + assert!( + !probe.publisher_finished_between_reads, + "a publish landed between the log read and the platform read" + ); + + assert_eq!(cursors(&snap.log.events), vec![1]); + assert_eq!(cursors(&snap.platform.events), vec![2]); + assert_eq!(snap.highest_seq, 2); + + let live_log = log_rx.try_recv().expect("raced log is live"); + let live_event = platform_rx.try_recv().expect("raced event is live"); + assert_eq!((live_log.seq, live_event.seq), (3, 4)); + assert!(live_log.seq > snap.highest_seq && live_event.seq > snap.highest_seq); + } + + /// `subscribe_resumable` records the mark under the lock every publish + /// holds across its send, so an event at or below it is never queued on + /// the new receivers and one above it always is. + #[test] + fn subscribe_resumable_splits_buffered_history_from_queued_events() { + let bus = TracingLogBus::new(); + let sandbox_id = "sb-sub"; + assert_eq!( + bus.subscribe_resumable(sandbox_id, true, true) + .subscribed_at, + 0 + ); + assert_eq!(bus.cursor_space(sandbox_id), None, "must not mint a space"); + + bus.publish_external(make_log_event(sandbox_id, "l1")); // seq 1 + let sub = bus.subscribe_resumable(sandbox_id, true, false); + assert_eq!(sub.subscribed_at, 1); + assert!(sub.platform_rx.is_none()); + let mut log_rx = sub.log_rx.unwrap(); + assert!(log_rx.try_recv().is_err(), "history is not queued live"); + + bus.publish_external(make_log_event(sandbox_id, "l2")); // seq 2 + assert_eq!(log_rx.try_recv().unwrap().seq, 2); + } + + /// The reviewer's example at bus level: subscribe, then platform event 1 + /// and log 2 are published before the snapshot. Both are queued live, and + /// the caller will suppress those copies, so the snapshot must return both + /// even though `event_tail` asked for no platform history -- and with + /// nothing excluded, neither source reports a floor. + #[test] + fn snapshot_tail_keeps_events_published_after_subscribe_regardless_of_depth() { + let bus = TracingLogBus::new(); + let sandbox_id = "sb-opening"; + let sub = bus.subscribe_resumable(sandbox_id, true, true); + bus.platform_event_bus.publish(sandbox_id, stream_event(0)); // seq 1 + bus.publish_external(make_log_event(sandbox_id, "l2")); // seq 2 + + let snap = bus.snapshot_tail(sandbox_id, Some(200), Some(0), sub.subscribed_at); + assert_eq!(cursors(&snap.log.events), vec![2]); + assert_eq!(cursors(&snap.platform.events), vec![1]); + assert_eq!((snap.log.floor, snap.platform.floor), (0, 0)); + assert!(!snap.log.evicted_while_opening && !snap.platform.evicted_while_opening); + assert_eq!(snap.highest_seq, 2); + } + + /// The exemption covers only events after the mark: older history is + /// still bounded by depth and still reports its floor. + #[test] + fn tail_with_floor_impl_exempts_only_events_after_keep_after() { + let tail = tail_of(1, 6); + // Depth 1 would return just 6; everything after 3 is exempt. + let (events, floor) = tail_with_floor_impl(&tail, 1, 3); + assert_eq!(cursors(&events), vec![4, 5, 6]); + assert_eq!(floor, 3); + + // A depth deeper than the exempt run still reaches into history. + let (events, floor) = tail_with_floor_impl(&tail, 4, 5); + assert_eq!(cursors(&events), vec![3, 4, 5, 6]); + assert_eq!(floor, 2); + + // Nothing after the mark: plain depth bound. + let (events, floor) = tail_with_floor_impl(&tail, 0, 6); + assert!(events.is_empty()); + assert_eq!(floor, 6); + } + + /// If the buffer trimmed an event published after subscribe before the + /// snapshot read it, that event is gone from both paths; the source says + /// so for the caller to report. + #[test] + fn snapshot_tail_reports_eviction_of_events_published_after_subscribe() { + let bus = TracingLogBus::new(); + let sandbox_id = "sb-opening-evict"; + bus.platform_event_bus.publish(sandbox_id, stream_event(0)); // seq 1 + let sub = bus.subscribe_resumable(sandbox_id, true, true); + for _ in 0..=PlatformEventBus::DEFAULT_TAIL { + bus.platform_event_bus.publish(sandbox_id, stream_event(0)); + } + + let snap = bus.snapshot_tail(sandbox_id, Some(10), Some(0), sub.subscribed_at); + assert!(snap.platform.evicted_while_opening); + assert!(!snap.log.evicted_while_opening); + assert_eq!(snap.platform.events.len(), PlatformEventBus::DEFAULT_TAIL); + + // History trimmed before subscribe is ordinary truncation. + let late = bus.subscribe_resumable(sandbox_id, true, true); + let snap = bus.snapshot_tail(sandbox_id, Some(10), Some(0), late.subscribed_at); + assert!(!snap.platform.evicted_while_opening); + assert!(snap.platform.events.is_empty()); + } + + /// Baseline correctness: interleaved log/platform publishes draw from the + /// same cursor space, and `snapshot_after` returns each source's own + /// events under its own key, not merged or cross-contaminated. + #[test] + fn snapshot_after_matches_independent_tail_after_calls() { + let bus = TracingLogBus::new(); + let sandbox_id = "sb-snap"; + bus.publish_external(make_log_event(sandbox_id, "l1")); // seq 1 + bus.platform_event_bus.publish( + sandbox_id, + SandboxStreamEvent { + payload: None, + cursor: String::new(), + }, + ); // seq 2 + bus.publish_external(make_log_event(sandbox_id, "l2")); // seq 3 + let epoch = bus.cursor_space(sandbox_id).expect("space exists").epoch; + + let ResumeSnapshot::Read(log, platform) = + bus.snapshot_after(sandbox_id, epoch, 0, 0, true, true) + else { + panic!("expected a successful read"); + }; + assert_eq!( + cursors(&log.expect("followed").expect("no gap")), + vec![1, 3] + ); + assert_eq!( + cursors(&platform.expect("followed").expect("no gap")), + vec![2] + ); + } + + /// A source that isn't followed isn't read at all. + #[test] + fn snapshot_after_skips_unfollowed_sources() { + let bus = TracingLogBus::new(); + let sandbox_id = "sb-skip"; + bus.publish_external(make_log_event(sandbox_id, "l1")); + let epoch = bus.cursor_space(sandbox_id).expect("space exists").epoch; + + let ResumeSnapshot::Read(log, platform) = + bus.snapshot_after(sandbox_id, epoch, 0, 0, true, false) + else { + panic!("expected a successful read"); + }; + assert_eq!(log.expect("followed").expect("no gap").len(), 1); + assert!(platform.is_none()); + } + + /// The epoch check and both bus reads happen under one lock hold, so a + /// teardown plus republish between them can't apply a stale epoch's + /// cursor to the replacement space's buffers. + #[test] + fn snapshot_after_rejects_a_cursor_from_a_retired_space() { + let bus = TracingLogBus::new(); + let sandbox_id = "sb-retired"; + bus.publish_external(make_log_event(sandbox_id, "l1")); + let original_epoch = bus.cursor_space(sandbox_id).expect("space exists").epoch; + + bus.remove(sandbox_id); + bus.publish_external(make_log_event(sandbox_id, "l1-again")); // seq 1 again, new epoch + + assert!(matches!( + bus.snapshot_after(sandbox_id, original_epoch, 0, 0, true, false), + ResumeSnapshot::SpaceGone + )); + } + + /// No space at all -- an unpublished or never-existent sandbox -- is the + /// same outcome as a retired one: there's nothing for the cursor to + /// address. + #[test] + fn snapshot_after_rejects_when_no_space_exists() { + let bus = TracingLogBus::new(); + assert!(matches!( + bus.snapshot_after("nope", Uuid::nil(), 0, 0, true, false), + ResumeSnapshot::SpaceGone + )); + } + + /// A cursor ahead of anything the space has issued -- only reachable with + /// a fabricated token -- is rejected rather than treated as caught up. + #[test] + fn snapshot_after_rejects_a_cursor_ahead_of_the_space() { + let bus = TracingLogBus::new(); + let sandbox_id = "sb-ahead"; + bus.publish_external(make_log_event(sandbox_id, "l1")); + let epoch = bus.cursor_space(sandbox_id).expect("space exists").epoch; + + assert!(matches!( + bus.snapshot_after(sandbox_id, epoch, 99, 0, true, false), + ResumeSnapshot::CursorAhead + )); + } + #[test] fn tail_after_impl_boundary_at_last_trimmed_is_serviceable() { // Bus trimmed up to seq 2, retains 3..=5. Client saw exactly 2, so @@ -627,56 +1343,132 @@ mod tests { } #[test] - fn tracing_log_bus_tail_after_serviceable_and_missing() { + fn tracing_log_bus_resume_replay_serviceable_and_missing() { let bus = TracingLogBus::new(); let sandbox_id = "sb-ta"; for _ in 0..3 { bus.publish_external(make_log_event(sandbox_id, "line")); } + let epoch = bus.cursor_space(sandbox_id).expect("space exists").epoch; + // Cursors start at 1, so three publishes are seqs 1,2,3. + let ResumeSnapshot::Read(log, _) = bus.snapshot_after(sandbox_id, epoch, 0, 0, true, false) + else { + panic!("expected a successful read"); + }; assert_eq!( - cursors(&bus.tail_after(sandbox_id, 0).unwrap()), + cursors(&log.expect("followed").expect("no gap")), vec![1, 2, 3] ); - assert_eq!(cursors(&bus.tail_after(sandbox_id, 2).unwrap()), vec![3]); - // Unknown sandbox: no entry, nothing buffered, no gap. - assert!(bus.tail_after("nope", 5).unwrap().is_empty()); + let ResumeSnapshot::Read(log, _) = bus.snapshot_after(sandbox_id, epoch, 2, 0, true, false) + else { + panic!("expected a successful read"); + }; + assert_eq!(cursors(&log.expect("followed").expect("no gap")), vec![3]); } #[test] - fn platform_event_bus_tail_after_serviceable() { + fn platform_event_bus_resume_replay_serviceable() { let bus = TracingLogBus::new(); let platform = &bus.platform_event_bus; let sandbox_id = "sb-pe"; for _ in 0..3 { platform.publish(sandbox_id, stream_event(0)); } + let epoch = bus.cursor_space(sandbox_id).expect("space exists").epoch; + // Shared allocator, but only the platform bus published here, so its // seqs are 1,2,3. + let ResumeSnapshot::Read(_, platform_replay) = + bus.snapshot_after(sandbox_id, epoch, 0, 0, false, true) + else { + panic!("expected a successful read"); + }; assert_eq!( - cursors(&platform.tail_after(sandbox_id, 0).unwrap()), + cursors(&platform_replay.expect("followed").expect("no gap")), vec![1, 2, 3] ); + let ResumeSnapshot::Read(_, platform_replay) = + bus.snapshot_after(sandbox_id, epoch, 0, 1, false, true) + else { + panic!("expected a successful read"); + }; assert_eq!( - cursors(&platform.tail_after(sandbox_id, 1).unwrap()), + cursors(&platform_replay.expect("followed").expect("no gap")), vec![2, 3] ); } #[test] - fn shared_allocator_interleaves_cursors_across_buses() { + fn snapshot_tail_reports_truncation() { let bus = TracingLogBus::new(); - let sandbox_id = "sb-mix"; - // Interleave log and platform publishes; the shared allocator gives - // each a unique, increasing cursor in one merged space. - bus.publish_external(make_log_event(sandbox_id, "a")); // seq 1 - bus.platform_event_bus.publish(sandbox_id, stream_event(0)); // seq 2 - bus.publish_external(make_log_event(sandbox_id, "b")); // seq 3 + let sandbox_id = "sb-floor"; + for _ in 0..5 { + bus.publish_external(make_log_event(sandbox_id, "line")); + } + let snap = bus.snapshot_tail(sandbox_id, Some(2), None, u64::MAX); + assert_eq!(cursors(&snap.log.events), vec![4, 5]); + assert_eq!(snap.log.floor, 3); + + // Wide enough to cover everything: no floor. + let snap = bus.snapshot_tail(sandbox_id, Some(10), None, u64::MAX); + assert_eq!(cursors(&snap.log.events), vec![1, 2, 3, 4, 5]); + assert_eq!(snap.log.floor, 0); + } + + /// `highest_seq` is the space's high-water mark at the lock hold, not the + /// newest event either window returned: events a depth excluded still + /// count, since they were buffered when the tails were read and can only + /// reach a live receiver as a duplicate of pre-snapshot history. + #[test] + fn snapshot_tail_reports_the_high_water_mark_past_excluded_events() { + let bus = TracingLogBus::new(); + let sandbox_id = "sb-hwm"; + bus.publish_external(make_log_event(sandbox_id, "l1")); // seq 1 + bus.publish_external(make_log_event(sandbox_id, "l2")); // seq 2 + bus.platform_event_bus.publish(sandbox_id, stream_event(0)); // seq 3 + + let snap = bus.snapshot_tail(sandbox_id, Some(10), Some(0), u64::MAX); + assert_eq!(cursors(&snap.log.events), vec![1, 2]); + assert_eq!(snap.log.floor, 0); + assert!(snap.platform.events.is_empty()); + assert_eq!(snap.platform.floor, 3); + assert_eq!(snap.highest_seq, 3); + } + + /// An unread source is skipped outright; a sandbox with nothing published + /// yields empty tails and a zero mark without minting a cursor space. + #[test] + fn snapshot_tail_skips_unread_sources_and_never_mints_a_space() { + let bus = TracingLogBus::new(); + let sandbox_id = "sb-tail-skip"; + bus.publish_external(make_log_event(sandbox_id, "l1")); + bus.platform_event_bus.publish(sandbox_id, stream_event(0)); + + let snap = bus.snapshot_tail(sandbox_id, Some(10), None, u64::MAX); + assert_eq!(cursors(&snap.log.events), vec![1]); + assert!(snap.platform.events.is_empty()); + assert_eq!(snap.platform.floor, 0); + assert_eq!(snap.highest_seq, 2); - let logs = cursors(&bus.tail_after(sandbox_id, 0).unwrap()); - let events = cursors(&bus.platform_event_bus.tail_after(sandbox_id, 0).unwrap()); - assert_eq!(logs, vec![1, 3]); - assert_eq!(events, vec![2]); + let snap = bus.snapshot_tail("nope", Some(10), Some(10), u64::MAX); + assert!(snap.log.events.is_empty() && snap.platform.events.is_empty()); + assert_eq!(snap.highest_seq, 0); + assert_eq!(bus.cursor_space("nope"), None); + } + + #[test] + fn platform_event_bus_tail_with_floor_reports_truncation() { + let bus = TracingLogBus::new(); + let platform = &bus.platform_event_bus; + let sandbox_id = "sb-pe-floor"; + for _ in 0..5 { + platform.publish(sandbox_id, stream_event(0)); + } + let (events, floor) = platform.tail_with_floor(sandbox_id, 0); + assert!(events.is_empty()); + // Nothing returned: floor is the newest of everything excluded, 5. + assert_eq!(floor, 5); } #[test] @@ -760,7 +1552,9 @@ mod tests { assert_eq!(after_platform.highest_seq, 2); let log_cursor = &bus.tail(sandbox_id, 10)[0].event.cursor; - let platform_cursor = &bus.platform_event_bus.tail(sandbox_id, 10)[0].event.cursor; + let platform_cursor = &bus.platform_event_bus.tail_with_floor(sandbox_id, 10).0[0] + .event + .cursor; assert_eq!( WatchCursor::parse(log_cursor).expect("valid").epoch, WatchCursor::parse(platform_cursor).expect("valid").epoch, @@ -947,8 +1741,9 @@ mod tests { } // Tail should return all events in order - let events = bus.tail(sandbox_id, 10); + let (events, floor) = bus.tail_with_floor(sandbox_id, 10); assert_eq!(events.len(), 5); + assert_eq!(floor, 0, "max covered everything retained"); // Verify order (oldest first) for (i, cursored) in events.iter().enumerate() { @@ -960,8 +1755,9 @@ mod tests { } // Tail with smaller max should return most recent events - let events = bus.tail(sandbox_id, 2); + let (events, floor) = bus.tail_with_floor(sandbox_id, 2); assert_eq!(events.len(), 2); + assert_eq!(floor, 3, "newest excluded event is Event2 (seq 3)"); if let Some(sandbox_stream_event::Payload::Event(ref e)) = events[0].event.payload { assert_eq!(e.reason, "Event3"); } @@ -973,8 +1769,9 @@ mod tests { #[test] fn platform_event_bus_tail_empty_sandbox() { let bus = PlatformEventBus::new(SeqAllocator::default()); - let events = bus.tail("nonexistent", 10); + let (events, floor) = bus.tail_with_floor("nonexistent", 10); assert!(events.is_empty()); + assert_eq!(floor, 0); } #[test] @@ -987,10 +1784,10 @@ mod tests { cursor: String::new(), }; bus.publish(sandbox_id, evt); - assert_eq!(bus.tail(sandbox_id, 10).len(), 1); + assert_eq!(bus.tail_with_floor(sandbox_id, 10).0.len(), 1); bus.remove(sandbox_id); - assert!(bus.tail(sandbox_id, 10).is_empty()); + assert!(bus.tail_with_floor(sandbox_id, 10).0.is_empty()); } } @@ -1057,28 +1854,19 @@ impl PlatformEventBus { } } - /// Return buffered platform events for replay to late subscribers. - pub(crate) fn tail(&self, sandbox_id: &str, max: usize) -> Vec { - let inner = self.inner.lock().expect("platform event bus lock poisoned"); - inner - .per_id - .get(sandbox_id) - .map(|d| d.tail.iter().rev().take(max).cloned().collect::>()) - .unwrap_or_default() - .into_iter() - .rev() - .collect() - } - - pub(crate) fn tail_after( + /// Return buffered platform events and the coverage floor `max` leaves + /// behind. See `tail_with_floor_impl`. Production reads go through + /// `TracingLogBus::snapshot_tail`, which holds the cursor-space lock. + #[cfg(test)] + pub(crate) fn tail_with_floor( &self, sandbox_id: &str, - after_seq: u64, - ) -> Result, ResumeGap> { + max: usize, + ) -> (Vec, u64) { let inner = self.inner.lock().expect("platform event bus lock poisoned"); inner.per_id.get(sandbox_id).map_or_else( - || Ok(Vec::new()), - |per| tail_after_impl(&per.tail, per.last_trimmed_seq, after_seq), + || (Vec::new(), 0), + |per| tail_with_floor_impl(&per.tail, max, u64::MAX), ) } diff --git a/crates/openshell-supervisor-network/Cargo.toml b/crates/openshell-supervisor-network/Cargo.toml index aa3ca35014..7043f30d24 100644 --- a/crates/openshell-supervisor-network/Cargo.toml +++ b/crates/openshell-supervisor-network/Cargo.toml @@ -67,10 +67,12 @@ bundled-ca-roots = ["dep:webpki-roots"] openshell-sandbox-backend = { path = "../openshell-sandbox-backend", features = ["test-support"] } openshell-driver-vm = { path = "../openshell-driver-vm", default-features = false, features = ["allocation-tracking"] } +openshell-otel-test-support = { path = "../openshell-otel-test-support" } openshell-ocsf = { path = "../openshell-ocsf", features = ["test-support"] } openshell-ocsf-validate = { path = "../openshell-ocsf-validate" } openshell-otel = { path = "../openshell-otel" } openshell-supervisor-middleware-builtins = { path = "../openshell-supervisor-middleware-builtins" } +opentelemetry-proto = { workspace = true } opentelemetry_sdk = { workspace = true, features = ["testing"] } temp-env = "0.3" tokio = { workspace = true, features = ["test-util"] } diff --git a/crates/openshell-supervisor-network/README.md b/crates/openshell-supervisor-network/README.md index cf842d5041..38a7fb6add 100644 --- a/crates/openshell-supervisor-network/README.md +++ b/crates/openshell-supervisor-network/README.md @@ -41,3 +41,13 @@ at debug, and emits no OCSF denial event: a standard OpenTelemetry SDK retries for the life of the agent, and one event per retry would flood the OCSF log. The supervisor installs its OTLP relay receiver as the handler while the agent trace relay is active. + +## Static credential placement + +Substitution checks the declared request location before writing upstream. +Credentials without placement metadata default to the Authorization header. HTTP placement violations return a safe 403 response; +WebSocket violations close the connection with policy denial. Both paths emit a +domain event and a detection finding without credential values or placeholders. +Explicit body and WebSocket +rewrite flags authorize those locations for ordinary static credentials. SigV4 +signing retains the endpoint-scoped resolver's existing trusted access. diff --git a/crates/openshell-supervisor-network/clippy.toml b/crates/openshell-supervisor-network/clippy.toml new file mode 100644 index 0000000000..0b7005afb4 --- /dev/null +++ b/crates/openshell-supervisor-network/clippy.toml @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +disallowed-macros = [ + { path = "openshell_ocsf::ocsf_emit", reason = "use crate::telemetry::egress_emit so denied HTTP decisions get a deny span" }, +] diff --git a/crates/openshell-supervisor-network/src/google_cloud_metadata.rs b/crates/openshell-supervisor-network/src/google_cloud_metadata.rs index aec80473a3..ea5d0c16bd 100644 --- a/crates/openshell-supervisor-network/src/google_cloud_metadata.rs +++ b/crates/openshell-supervisor-network/src/google_cloud_metadata.rs @@ -16,8 +16,10 @@ use http::StatusCode; use miette::{IntoDiagnostic, Result}; use openshell_core::provider_credentials::ProviderCredentialState; use openshell_ocsf::{ - ActivityId, HttpActivityBuilder, HttpRequest, SeverityId, StatusId, ocsf_emit, + ActionId, ActivityId, DispositionId, HttpActivityBuilder, HttpRequest, SeverityId, StatusId, }; + +use crate::telemetry::egress_emit; use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; type MetadataResponse = (u16, &'static str, String); @@ -311,7 +313,7 @@ fn emit_metadata_event( status: StatusId, message: &str, ) { - ocsf_emit!(build_metadata_event( + egress_emit!(build_metadata_event( method, response_code, severity, @@ -327,7 +329,7 @@ fn build_metadata_event( status: StatusId, message: &str, ) -> openshell_ocsf::OcsfEvent { - HttpActivityBuilder::new(openshell_ocsf::ctx::ctx()) + let mut builder = HttpActivityBuilder::new(openshell_ocsf::ctx::ctx()) .activity(ActivityId::for_http_method(method)) .http_request(HttpRequest { http_method: method.parse().expect("HTTP method parsing is infallible"), @@ -338,8 +340,13 @@ fn build_metadata_event( }) .severity(severity) .status(status) - .message(message.to_string()) - .build() + .message(message.to_string()); + if matches!(response_code, 403 | 405) { + builder = builder + .action(ActionId::Denied) + .disposition(DispositionId::Blocked); + } + builder.build() } #[cfg(test)] @@ -347,6 +354,26 @@ mod tests { use super::*; use std::collections::HashMap; + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn metadata_rejections_have_deny_traces_but_operational_failures_do_not() { + use crate::telemetry::tests::{assert_deny_resolves, exported}; + + let (events, received) = exported("warn", || { + let ctx = make_context(HashMap::new()); + route_request(&ctx, "GET", PATH_TOKEN, &[]); + route_request(&ctx, "POST", PATH_TOKEN, &flavor_headers()); + route_request(&ctx, "GET", "/unknown", &flavor_headers()); + route_request(&ctx, "GET", PATH_TOKEN, &flavor_headers()); + }) + .await; + assert_eq!(events.len(), 4); + assert_deny_resolves(&events[..2], &received, true); + for event in &events[2..] { + assert!(event.get("trace").is_none()); + } + assert_eq!(received.spans.len(), 2); + } + fn token_binding() -> openshell_core::proto::StaticCredentialBinding { openshell_core::proto::StaticCredentialBinding { endpoints: vec![openshell_core::proto::StaticCredentialEndpointBinding { diff --git a/crates/openshell-supervisor-network/src/host.rs b/crates/openshell-supervisor-network/src/host.rs index b5ada8b100..d6fbe2e54f 100644 --- a/crates/openshell-supervisor-network/src/host.rs +++ b/crates/openshell-supervisor-network/src/host.rs @@ -23,7 +23,7 @@ use openshell_core::proto::SandboxPolicy as ProtoSandboxPolicy; use openshell_core::provider_credentials::ProviderCredentialState; use openshell_isolation_interface::contract::{BinaryIdentity, ExecutableIdentity, Sha256Digest}; use openshell_ocsf::{ - ConfigStateChangeBuilder, SeverityId, StateId, StatusId, ctx::ctx as ocsf_ctx, ocsf_emit, + ConfigStateChangeBuilder, SeverityId, StateId, StatusId, ctx::ctx as ocsf_ctx, }; use tokio::sync::mpsc::UnboundedSender; @@ -125,7 +125,7 @@ pub async fn start_host_proxy(config: HostProxyConfig) -> Result { let cert_cache = CertCache::new(ca); let state = Arc::new(ProxyTlsState::new(cert_cache, upstream_config)); - ocsf_emit!( + crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(ocsf_ctx()) .severity(SeverityId::Informational) .status(StatusId::Success) @@ -138,7 +138,7 @@ pub async fn start_host_proxy(config: HostProxyConfig) -> Result { - ocsf_emit!( + crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(ocsf_ctx()) .severity(SeverityId::High) .status(StatusId::Failure) @@ -152,7 +152,7 @@ pub async fn start_host_proxy(config: HostProxyConfig) -> Result { - ocsf_emit!( + crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(ocsf_ctx()) .severity(SeverityId::High) .status(StatusId::Failure) @@ -167,7 +167,7 @@ pub async fn start_host_proxy(config: HostProxyConfig) -> Result { - ocsf_emit!( + crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(ocsf_ctx()) .severity(SeverityId::High) .status(StatusId::Failure) @@ -181,7 +181,7 @@ pub async fn start_host_proxy(config: HostProxyConfig) -> Result { - ocsf_emit!( + crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(ocsf_ctx()) .severity(SeverityId::High) .status(StatusId::Failure) diff --git a/crates/openshell-supervisor-network/src/l7/middleware.rs b/crates/openshell-supervisor-network/src/l7/middleware.rs index de2aa20613..73c317be6f 100644 --- a/crates/openshell-supervisor-network/src/l7/middleware.rs +++ b/crates/openshell-supervisor-network/src/l7/middleware.rs @@ -5,10 +5,11 @@ use crate::l7::relay::L7EvalContext; use crate::opa::PolicyGenerationGuard; +use crate::telemetry::egress_emit; use miette::{Result, miette}; use openshell_ocsf::{ ActionId, ActivityId, DetectionFindingBuilder, DispositionId, Endpoint, FindingInfo, - HttpActivityBuilder, HttpRequest, NetworkActivityBuilder, SeverityId, StatusId, ocsf_emit, + HttpActivityBuilder, HttpRequest, NetworkActivityBuilder, SeverityId, StatusId, }; use std::path::PathBuf; use tokio::io::{AsyncRead, AsyncWrite}; @@ -146,7 +147,7 @@ pub fn uninspectable_traffic_gate( /// Emit the detection finding for traffic a matching middleware chain cannot /// inspect: denied under a fail-closed chain, bypassed under fail-open. pub fn emit_middleware_uninspectable(ctx: &L7EvalContext, detail: &str, denied: bool) { - let event = DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) + let mut event = DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) .severity(if denied { SeverityId::High } else { @@ -166,9 +167,11 @@ pub fn emit_middleware_uninspectable(ctx: &L7EvalContext, detail: &str, denied: "Uninspectable traffic to host with required middleware; denied" } else { "Uninspectable traffic bypassed middleware (fail_open)" - }) - .build(); - ocsf_emit!(event); + }); + if denied { + event = event.action(ActionId::Denied); + } + egress_emit!(event.build()); } pub fn emit_websocket_preflight_events( @@ -178,13 +181,13 @@ pub fn emit_websocket_preflight_events( emit_websocket_invocations(ctx, &outcome.invocations); emit_websocket_coverage(ctx, &outcome.coverage); for event in websocket_preflight_finding_events(outcome) { - ocsf_emit!(event); + egress_emit!(event); } if outcome.saturated { emit_websocket_saturation(ctx); } if outcome.session_capacity_exhausted { - emit_middleware_session_capacity_exhausted(ctx); + emit_middleware_session_capacity_exhausted(ctx, !outcome.allowed); } } @@ -193,7 +196,7 @@ pub(super) fn emit_websocket_coverage( coverage: &[openshell_supervisor_middleware::WebSocketCoverage], ) { for event in websocket_coverage_events(ctx, coverage) { - ocsf_emit!(event); + egress_emit!(event); } } @@ -263,7 +266,7 @@ pub(super) fn emit_websocket_message_events( emit_websocket_saturation(ctx); } for event in websocket_message_finding_events(outcome) { - ocsf_emit!(event); + egress_emit!(event); } } @@ -385,10 +388,15 @@ fn emit_websocket_invocations( invocation.transformed, )) .build(); - ocsf_emit!(event); + egress_emit!(event); if invocation.failed { - let event = DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) - .severity(if invocation.outcome == Outcome::FailClosed { + let fail_closed = invocation.outcome == Outcome::FailClosed; + let mut finding = DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()); + if fail_closed { + finding = finding.action(ActionId::Denied); + } + let event = finding + .severity(if fail_closed { SeverityId::High } else { SeverityId::Medium @@ -406,7 +414,7 @@ fn emit_websocket_invocations( ]) .message("WebSocket middleware stage failed") .build(); - ocsf_emit!(event); + egress_emit!(event); } if invocation.stage_disabled { let event = DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) @@ -426,7 +434,7 @@ fn emit_websocket_invocations( "WebSocket middleware stage stream became unusable and was disabled for this session", ) .build(); - ocsf_emit!(event); + egress_emit!(event); } } } @@ -445,11 +453,11 @@ fn emit_websocket_saturation(ctx: &L7EvalContext) { ]) .message("WebSocket middleware work waited for admission capacity") .build(); - ocsf_emit!(event); + egress_emit!(event); } -fn emit_middleware_session_capacity_exhausted(ctx: &L7EvalContext) { - let event = DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) +fn emit_middleware_session_capacity_exhausted(ctx: &L7EvalContext, denied: bool) { + let mut event = DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) .severity(SeverityId::Medium) .finding_info(FindingInfo::new( "openshell.middleware.session_capacity_exhausted", @@ -460,13 +468,16 @@ fn emit_middleware_session_capacity_exhausted(ctx: &L7EvalContext) { ("host", ctx.host.as_str()), ("operation", "websocket_message"), ]) - .message("Persistent middleware session admission was refused at process capacity") - .build(); - ocsf_emit!(event); + .message("Persistent middleware session admission was refused at process capacity"); + if denied { + event = event.action(ActionId::Denied); + } + egress_emit!(event.build()); } fn middleware_admission_exhausted_event(ctx: &L7EvalContext) -> openshell_ocsf::OcsfEvent { DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) + .action(ActionId::Denied) .severity(SeverityId::Medium) .finding_info(FindingInfo::new( "openshell.middleware.admission_exhausted", @@ -483,7 +494,7 @@ fn middleware_admission_exhausted_event(ctx: &L7EvalContext) -> openshell_ocsf:: } fn emit_middleware_admission_exhausted(ctx: &L7EvalContext) { - ocsf_emit!(middleware_admission_exhausted_event(ctx)); + egress_emit!(middleware_admission_exhausted_event(ctx)); } /// Largest body-buffering limit across the entries that actually resolved to a @@ -790,7 +801,7 @@ pub(super) fn resolve_unbuffered_body( } fn emit_middleware_body_unavailable(ctx: &L7EvalContext, denied: bool) { - let event = DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) + let mut event = DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) .severity(if denied { SeverityId::High } else { @@ -809,9 +820,11 @@ fn emit_middleware_body_unavailable(ctx: &L7EvalContext, denied: bool) { "Request body exceeded middleware inspection cap; denied" } else { "Request body exceeded middleware inspection cap; passed through (fail_open)" - }) - .build(); - ocsf_emit!(event); + }); + if denied { + event = event.action(ActionId::Denied); + } + egress_emit!(event.build()); } /// Parse the raw header block into middleware-visible headers, preserving @@ -962,6 +975,7 @@ pub(super) fn middleware_events( "openshell.middleware.failure", "Supervisor middleware failure", )) + .action(ActionId::Denied) .message("Required supervisor middleware failed closed") .build(); events.push(event); @@ -981,6 +995,7 @@ pub(super) fn middleware_events( ("policy", ctx.policy_name.as_str()), ("host", ctx.host.as_str()), ]) + .action(ActionId::Denied) .message("Transformed request denied because policy evaluation failed") .build(); events.push(event); @@ -999,8 +1014,208 @@ fn emit_middleware_events( req: &crate::l7::provider::L7Request, outcome: &openshell_supervisor_middleware::ChainOutcome, ) { - for event in middleware_events(ctx, req, outcome) { - ocsf_emit!(event); + crate::telemetry::emit_decision(middleware_events(ctx, req, outcome), None); +} + +#[cfg(test)] +mod trace_tests { + use super::*; + use crate::telemetry::tests::{assert_deny_resolves, assert_no_deny, deny_spans, exported}; + use openshell_supervisor_middleware::{ChainOutcome, MiddlewareInvocation, NamespacedFinding}; + + fn finding() -> NamespacedFinding { + NamespacedFinding { + middleware: "guard".to_string(), + finding: openshell_core::proto::Finding { + r#type: "test.finding".to_string(), + label: "Test finding".to_string(), + count: 1, + severity: "high".to_string(), + ..Default::default() + }, + } + } + + fn outcome( + allowed: bool, + reason: &str, + applied: Vec, + findings: Vec, + ) -> ChainOutcome { + ChainOutcome { + allowed, + reason: reason.to_string(), + body: vec![], + header_mutations: vec![], + findings, + metadata: std::collections::BTreeMap::new(), + applied, + denial: None, + } + } + + fn request() -> crate::l7::provider::L7Request { + crate::l7::provider::L7Request { + action: "POST".to_string(), + target: "/".to_string(), + query_params: std::collections::HashMap::new(), + raw_header: vec![], + body_length: crate::l7::provider::BodyLength::None, + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn websocket_fail_closed_findings_record_the_denial() { + use openshell_supervisor_middleware::{ + WebSocketInvocation, WebSocketInvocationOutcome as Outcome, + }; + for (outcome, action_id) in [(Outcome::FailClosed, Some(2)), (Outcome::FailOpen, None)] { + let (events, _) = exported("warn", || { + emit_websocket_invocations( + &L7EvalContext::default(), + &[WebSocketInvocation { + config_name: "guard".to_string(), + implementation: "test/guard".to_string(), + outcome, + sequence: None, + original_size: 0, + replacement_size: None, + transformed: false, + failed: true, + stage_disabled: false, + reason_code: None, + }], + ); + }) + .await; + let finding = events + .iter() + .find(|event| { + event["finding_info"]["uid"] == "openshell.middleware.websocket_failure" + }) + .unwrap(); + assert_eq!(finding["action_id"].as_u64(), action_id, "{outcome:?}"); + } + } + + #[test] + fn refusal_findings_record_the_denial() { + for reason in [ + "middleware_failed: guard", + "transformed_body_policy_evaluation_failed: unavailable", + ] { + let events = middleware_events( + &L7EvalContext::default(), + &request(), + &outcome(false, reason, vec![], vec![]), + ); + assert_eq!(events.len(), 1, "{reason}"); + let finding = events[0].to_json().unwrap(); + assert_eq!(finding["class_uid"], 2004); + assert_eq!(finding["action_id"], 2, "{reason}"); + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn inspection_refusals_record_denial_but_fail_open_findings_do_not() { + for denied in [false, true] { + let (events, received) = exported("warn", || { + let ctx = L7EvalContext::default(); + emit_middleware_uninspectable(&ctx, "unknown protocol", denied); + emit_middleware_body_unavailable(&ctx, denied); + }) + .await; + assert_eq!(events.len(), 2); + for (event, uid) in events.iter().zip([ + "openshell.middleware.traffic_uninspectable", + "openshell.middleware.body_unavailable", + ]) { + assert_eq!(event["finding_info"]["uid"], uid); + assert_eq!(event["class_uid"], 2004); + assert_eq!(event["action_id"].as_u64(), denied.then_some(2)); + assert_eq!(event["action"].as_str(), denied.then_some("Denied")); + if !denied { + assert!(event.get("action_id").is_none()); + assert!(event.get("action").is_none()); + } + } + // A finding-only refusal still creates no HTTP deny span. + assert_no_deny(&events, &received); + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn session_capacity_findings_follow_the_preflight_outcome() { + for allowed in [true, false] { + let (events, received) = exported("warn", || { + emit_websocket_preflight_events( + &L7EvalContext::default(), + &openshell_supervisor_middleware::WebSocketPreflightResult { + allowed, + terminal_reason: None, + reason: "middleware_session_capacity_exhausted".into(), + denial: None, + session: None, + findings: vec![], + metadata: std::collections::BTreeMap::new(), + invocations: vec![], + coverage: vec![], + saturated: false, + session_capacity_exhausted: true, + }, + ); + }) + .await; + assert_eq!(events.len(), 1); + assert_eq!( + events[0]["finding_info"]["uid"], + "openshell.middleware.session_capacity_exhausted" + ); + assert_eq!(events[0]["action_id"].as_u64(), (!allowed).then_some(2)); + assert_eq!(events[0]["action"].as_str(), (!allowed).then_some("Denied")); + if allowed { + assert!(events[0].get("action_id").is_none()); + assert!(events[0].get("action").is_none()); + } + assert_no_deny(&events, &received); + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn http_chain_outcomes_group_findings_and_preserve_allowed_controls() { + for allowed in [true, false] { + let (events, received) = exported("warn", || { + let invocation = MiddlewareInvocation { + name: "guard".to_string(), + implementation: "test/guard".to_string(), + decision: if allowed { + openshell_core::proto::Decision::Allow + } else { + openshell_core::proto::Decision::Deny + }, + transformed: false, + failed: false, + }; + let reason = if allowed { + "allowed" + } else { + "middleware_failed: guard" + }; + emit_middleware_events( + &L7EvalContext::default(), + &request(), + &outcome(allowed, reason, vec![invocation], vec![finding()]), + ); + }) + .await; + assert!(events.len() >= 2); + if allowed { + assert_no_deny(&events, &received); + } else { + assert_eq!(deny_spans(&received).len(), 1); + assert_deny_resolves(&events, &received, true); + } + } } } @@ -1026,6 +1241,10 @@ mod tests { let serialized = serde_json::to_string(&middleware_admission_exhausted_event(&ctx)) .expect("serialize admission event"); + let event: serde_json::Value = serde_json::from_str(&serialized).unwrap(); + assert_eq!(event["class_uid"], 2004); + assert_eq!(event["action_id"], 2); + assert_eq!(event["action"], "Denied"); assert!(serialized.contains("openshell.middleware.admission_exhausted")); assert!(serialized.contains("api-policy")); assert!(serialized.contains("api.example.test")); diff --git a/crates/openshell-supervisor-network/src/l7/mod.rs b/crates/openshell-supervisor-network/src/l7/mod.rs index 3f26114998..c24777826e 100644 --- a/crates/openshell-supervisor-network/src/l7/mod.rs +++ b/crates/openshell-supervisor-network/src/l7/mod.rs @@ -66,6 +66,37 @@ pub(crate) fn build_credential_endpoint_mismatch_finding( .build() } +pub(crate) fn build_credential_placement_mismatch_finding( + policy_name: &str, + host: &str, + protocol: Option<&str>, +) -> openshell_ocsf::OcsfEvent { + use openshell_ocsf::{ + ActionId, ActivityId, DetectionFindingBuilder, DispositionId, FindingInfo, SeverityId, + }; + let mut evidence = vec![ + ("policy", policy_name), + ("host", host), + ("disposition", "denied"), + ]; + if let Some(protocol) = protocol { + evidence.push(("protocol", protocol)); + } + DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) + .activity(ActivityId::Open) + .action(ActionId::Denied) + .disposition(DispositionId::Blocked) + .severity(SeverityId::High) + .is_alert(true) + .finding_info(FindingInfo::new( + "openshell.provider_credential.placement_mismatch", + "Provider credential used at an unauthorized request location", + )) + .evidence_pairs(&evidence) + .message("Provider credential placement mismatch; request denied") + .build() +} + /// TLS handling mode for proxy connections. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum TlsMode { @@ -348,6 +379,9 @@ pub fn parse_l7_config(val: ®orus::Value) -> Option { let allow_encoded_slash = get_object_bool(val, "allow_encoded_slash").unwrap_or(false); let websocket_credential_rewrite = get_object_bool(val, "websocket_credential_rewrite").unwrap_or(false); + if protocol == L7Protocol::Rest && websocket_credential_rewrite { + return None; + } let request_body_credential_rewrite = get_object_bool(val, "request_body_credential_rewrite").unwrap_or(false); let allow_uninspected_credentials = @@ -386,7 +420,7 @@ pub fn parse_l7_config(val: ®orus::Value) -> Option { "rejecting endpoint: unrecognized credential_signing value {other:?}" )) .build(); - openshell_ocsf::ocsf_emit!(event); + crate::telemetry::egress_emit!(event); return None; } _ => CredentialSigning::None, @@ -402,7 +436,7 @@ pub fn parse_l7_config(val: ®orus::Value) -> Option { .state(openshell_ocsf::StateId::Disabled, "invalid") .message("rejecting endpoint: credential_signing requires signing_service".to_string()) .build(); - openshell_ocsf::ocsf_emit!(event); + crate::telemetry::egress_emit!(event); return None; } @@ -431,6 +465,7 @@ pub fn parse_l7_config(val: ®orus::Value) -> Option { pub(crate) fn emit_uninspected_credential_finding(host: &str, policy_name: &str, surface: &str) { let event = openshell_ocsf::DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) + .action(openshell_ocsf::ActionId::Denied) .severity(openshell_ocsf::SeverityId::High) .finding_info(openshell_ocsf::FindingInfo::new( "openshell.credentials.traffic_uninspectable", @@ -444,7 +479,7 @@ pub(crate) fn emit_uninspected_credential_finding(host: &str, policy_name: &str, ]) .message("Uninspected credential-bearing traffic denied") .build(); - openshell_ocsf::ocsf_emit!(event); + crate::telemetry::egress_emit!(event); } impl L7EndpointConfig { @@ -1361,6 +1396,10 @@ pub fn validate_l7_policies(data_json: &serde_json::Value) -> (Vec, Vec< .and_then(serde_json::Value::as_bool) .unwrap_or(false); let l7_fields = L7EndpointFields { + websocket_credential_rewrite: ep + .get("websocket_credential_rewrite") + .and_then(serde_json::Value::as_bool) + .unwrap_or(false), protocol, access, has_rules, @@ -1472,7 +1511,7 @@ pub fn validate_l7_policies(data_json: &serde_json::Value) -> (Vec, Vec< && protocol != "websocket" { warnings.push(format!( - "{loc}: websocket_credential_rewrite is ignored unless protocol is rest or websocket" + "{loc}: websocket_credential_rewrite is ignored unless protocol is websocket" )); } @@ -2438,10 +2477,24 @@ mod tests { assert!(!config.websocket_credential_rewrite); } + #[test] + fn rest_websocket_credential_rewrite_fails_validation_and_materialization() { + let endpoint = serde_json::json!({ + "protocol": "rest", "host": "gateway.example.com", "port": 443, + "access": "read-only", "websocket_credential_rewrite": true + }); + let val = regorus::Value::from_json_str(&endpoint.to_string()).unwrap(); + assert!(parse_l7_config(&val).is_none()); + let (errors, _) = validate_l7_policies(&serde_json::json!({ + "network_policies": {"test": {"endpoints": [endpoint], "binaries": []}} + })); + assert!(errors.iter().any(|e| e.contains("use protocol: websocket"))); + } + #[test] fn parse_l7_config_websocket_credential_rewrite_opt_in() { let val = regorus::Value::from_json_str( - r#"{"protocol": "rest", "host": "gateway.example.com", "port": 443, "websocket_credential_rewrite": true}"#, + r#"{"protocol": "websocket", "host": "gateway.example.com", "port": 443, "websocket_credential_rewrite": true}"#, ) .unwrap(); let config = parse_l7_config(&val).unwrap(); @@ -2489,7 +2542,7 @@ mod tests { } #[test] - fn validate_websocket_credential_rewrite_warns_unless_rest_or_websocket() { + fn validate_websocket_credential_rewrite_warns_without_inspected_protocol() { let data = serde_json::json!({ "network_policies": { "test": { diff --git a/crates/openshell-supervisor-network/src/l7/relay.rs b/crates/openshell-supervisor-network/src/l7/relay.rs index e2d9530a56..61c342bc17 100644 --- a/crates/openshell-supervisor-network/src/l7/relay.rs +++ b/crates/openshell-supervisor-network/src/l7/relay.rs @@ -20,6 +20,7 @@ use crate::l7::provider::{L7Provider, RelayOutcome}; use crate::l7::rest::WebSocketExtensionMode; use crate::l7::{EndpointObserver, EnforcementMode, L7EndpointConfig, L7Protocol, L7RequestInfo}; use crate::opa::{PolicyGenerationGuard, TunnelPolicyEngine}; +use crate::telemetry::egress_emit; use miette::{IntoDiagnostic, Result, miette}; use openshell_core::activity::{ActivitySender, try_record_activity}; use openshell_core::endpoint_status::{EndpointObservationSender, EndpointResult}; @@ -27,7 +28,6 @@ use openshell_core::secrets::{self, SecretResolver}; use openshell_ocsf::{ ActionId, ActivityId, DetectionFindingBuilder, DispositionId, Endpoint, FindingInfo, HttpActivityBuilder, HttpRequest, NetworkActivityBuilder, SeverityId, StatusId, Url as OcsfUrl, - ocsf_emit, }; #[cfg(test)] use std::collections::BTreeMap; @@ -287,8 +287,13 @@ where .into_diagnostic()?; client.flush().await.into_diagnostic()?; - ocsf_emit!(build_request_authority_mismatch_event(ctx, method)); - ocsf_emit!(build_request_authority_mismatch_finding(ctx)); + crate::telemetry::emit_decision( + [ + build_request_authority_mismatch_event(ctx, method), + build_request_authority_mismatch_finding(ctx), + ], + None, + ); Ok(()) } @@ -338,7 +343,7 @@ where crate::l7::mcp::selected_revision_context(request, version) ); let summary = l7_protocol_log_summary(None, Some(&info)); - ocsf_emit!(build_l7_request_event( + egress_emit!(build_l7_request_event( ctx, &request.action, redacted_target, @@ -419,7 +424,7 @@ where } let reason = error.rejection_detail(request); let summary = l7_protocol_log_summary(None, Some(info)); - ocsf_emit!(build_l7_request_event( + egress_emit!(build_l7_request_event( ctx, &request.action, redacted_target, @@ -593,12 +598,15 @@ where W: AsyncWrite + Unpin, { let endpoint_mismatch = error.is_endpoint_mismatch(); - let status = if endpoint_mismatch { + let placement_mismatch = error.is_placement_mismatch(); + let status = if endpoint_mismatch || placement_mismatch { "403 Forbidden" } else { "500 Internal Server Error" }; - let body = if endpoint_mismatch { + let body = if placement_mismatch { + r#"{"error":"credential_placement_mismatch","message":"Credential is not authorized for this request location"}"# + } else if endpoint_mismatch { r#"{"error":"credential_endpoint_mismatch","message":"Credential is not authorized for this request endpoint"}"# } else { r#"{"error":"credential_unavailable","message":"Credential placeholder could not be resolved"}"# @@ -613,15 +621,38 @@ where .into_diagnostic()?; client.flush().await.into_diagnostic()?; - ocsf_emit!(build_credential_resolution_event( - ctx, - method, - endpoint_mismatch - )); - + let mut events = if placement_mismatch { + vec![ + HttpActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .activity(ActivityId::for_http_method(method)) + .http_request(method_only_request(method)) + .http_response(openshell_ocsf::HttpResponse { code: 403 }) + .action(ActionId::Denied) + .disposition(DispositionId::Blocked) + .severity(SeverityId::High) + .status(StatusId::Failure) + .dst_endpoint(Endpoint::from_domain(&ctx.host, ctx.port)) + .firewall_rule(&ctx.policy_name, "credential-binding") + .message("Credential use denied: unauthorized request location") + .status_detail("credential_placement_mismatch") + .build(), + crate::l7::build_credential_placement_mismatch_finding( + &ctx.policy_name, + &ctx.host, + None, + ), + ] + } else { + vec![build_credential_resolution_event( + ctx, + method, + endpoint_mismatch, + )] + }; if endpoint_mismatch { - ocsf_emit!(build_credential_endpoint_mismatch_finding(ctx)); + events.push(build_credential_endpoint_mismatch_finding(ctx)); } + crate::telemetry::emit_decision(events, None); Ok(()) } @@ -1015,7 +1046,7 @@ fn emit_parse_rejection(ctx: &L7EvalContext, detail: &str, engine_type: &str) { )) .status_detail(detail) .build(); - ocsf_emit!(event); + egress_emit!(event); emit_activity(ctx, true, "l7_parse_rejection"); } @@ -1131,7 +1162,7 @@ where .dst_endpoint(Endpoint::from_domain(&ctx.host, ctx.port)) .message("SQL L7 provider not yet implemented, falling back to passthrough") .build(); - ocsf_emit!(event); + egress_emit!(event); } tokio::io::copy_bidirectional(client, upstream) .await @@ -1664,7 +1695,7 @@ fn emit_l7_request_log( reason, protocol_summary, ); - ocsf_emit!(event); + egress_emit!(event); emit_activity(ctx, decision_str == "deny", "l7_policy"); } @@ -1830,7 +1861,7 @@ where } else { "raw bidirectional relay (L7 enforcement no longer active)" }; - ocsf_emit!( + egress_emit!( NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) .activity(ActivityId::Other) .activity_name("Upgrade") @@ -1884,9 +1915,13 @@ where assembly_budget, resolver, generation_guard: options.generation_guard, - provider_credentials: options - .ctx - .and_then(|ctx| ctx.provider_credentials.as_ref()), + provider_credentials: if options.websocket.credential_rewrite { + options + .ctx + .and_then(|ctx| ctx.provider_credentials.as_ref()) + } else { + None + }, target: &options.target, inspector, compression, @@ -1955,8 +1990,7 @@ pub(crate) fn upgrade_options<'a>( engine: Option<&'a TunnelPolicyEngine>, ) -> UpgradeRelayOptions<'a> { let websocket_credential_rewrite = - matches!(config.protocol, L7Protocol::Rest | L7Protocol::Websocket) - && config.websocket_credential_rewrite; + config.protocol == L7Protocol::Websocket && config.websocket_credential_rewrite; let deny_uninspected_credentials = config.provider_credentialed && !config.allow_uninspected_credentials; let websocket_message_policy = if config.protocol == L7Protocol::Websocket { @@ -2000,7 +2034,6 @@ pub(crate) fn websocket_extension_mode( ) -> WebSocketExtensionMode { if inspecting_middleware_session || config.protocol == L7Protocol::Websocket - || (config.protocol == L7Protocol::Rest && config.websocket_credential_rewrite) || (config.provider_credentialed && !config.allow_uninspected_credentials) { WebSocketExtensionMode::PermessageDeflate @@ -2169,7 +2202,7 @@ where request_info.action, ctx.host, ctx.port, redacted_target, reason, )) .build(); - ocsf_emit!(event); + egress_emit!(event); } if allowed || config.enforcement == EnforcementMode::Audit { @@ -2328,6 +2361,12 @@ where websocket_permessage_deflate, websocket_subprotocol, } => { + if crate::l7::rest::upgrade_refusal_for_protocol(config.protocol).is_some() { + if let Some(session) = middleware_session.take() { + session.end(openshell_core::proto::MiddlewareSessionEndReason::ProtocolError).await; + } + return Ok(()); + } let mut options = upgrade_options( config, ctx, @@ -2377,7 +2416,7 @@ pub(crate) fn emit_policy_reload( port: u16, policy_name: &str, ) { - ocsf_emit!( + egress_emit!( NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) .activity(ActivityId::Open) .action(ActionId::Denied) @@ -2597,7 +2636,7 @@ where &reason, )) .build(); - ocsf_emit!(event); + egress_emit!(event); } if allowed || (config.enforcement == EnforcementMode::Audit && !force_deny) { @@ -2879,7 +2918,7 @@ where request_info.action, ctx.host, ctx.port, redacted_target, reason, )) .build(); - ocsf_emit!(event); + egress_emit!(event); } if allowed || (config.enforcement == EnforcementMode::Audit && !force_deny) { @@ -3350,7 +3389,7 @@ fn emit_transformed_body_decision( request_info.action, ctx.host, ctx.port, request_info.target, reason )) .build(); - ocsf_emit!(event); + egress_emit!(event); } fn jsonrpc_policy_input(info: &crate::l7::jsonrpc::JsonRpcRequestInfo) -> serde_json::Value { @@ -3583,7 +3622,7 @@ where req.action, ctx.host, ctx.port, redacted_target, )) .build(); - ocsf_emit!(event); + egress_emit!(event); } let request_id = uuid::Uuid::new_v4().to_string(); @@ -3755,6 +3794,67 @@ where Ok(()) } +#[cfg(test)] +mod trace_tests { + use super::*; + use crate::telemetry::tests::{assert_deny_resolves, exported}; + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn authority_mismatch_domain_and_finding_share_one_trace() { + let (events, received) = exported("warn", || { + let ctx = L7EvalContext { + host: "blocked.example".to_string(), + port: 443, + ..Default::default() + }; + let mut response = Vec::new(); + // The in-memory span processor exports with a futures executor. + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(reject_request_authority_mismatch( + &mut response, + &ctx, + "POST", + )) + }) + .unwrap(); + assert!(response.starts_with(b"HTTP/1.1 403 Forbidden")); + }) + .await; + assert_eq!(events.len(), 2); + assert_eq!(received.spans.len(), 1); + assert_deny_resolves(&events, &received, true); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn default_level_l7_relay_deny_uid_resolves_to_one_span_root_trace() { + let (events, received) = crate::telemetry::tests::exported_over_otlp("warn", || { + let ctx = L7EvalContext { + host: "blocked.example".to_string(), + port: 443, + policy_name: "rest-policy".to_string(), + ..Default::default() + }; + egress_emit!(build_l7_request_event( + &ctx, "GET", "/", "deny", "l7-rest", "denied", None + )); + }) + .await; + assert_eq!(events.len(), 1); + assert_deny_resolves(&events, &received, true); + assert_eq!(received.spans.len(), 1); + crate::telemetry::tests::assert_deny_attribute( + &received, + "openshell.policy.name", + "rest-policy", + ); + crate::telemetry::tests::assert_deny_attribute( + &received, + "server.address", + "blocked.example", + ); + } +} + #[cfg(test)] mod tests { use super::*; @@ -4109,6 +4209,9 @@ mod tests { scoped .secret_resolver .expect("endpoint resolver") + .with_request_location(secrets::RequestCredentialLocation::Header( + "Authorization".into() + )) .resolve_placeholder("openshell:resolve:env:v42_API_TOKEN"), Some("secret") ); @@ -4292,6 +4395,570 @@ mod tests { assert_eq!(finding["disposition"], "Blocked"); } + fn placement_test_state() -> ProviderCredentialState { + placement_test_state_with_metadata(true) + } + + fn placement_test_state_with_metadata(with_metadata: bool) -> ProviderCredentialState { + ProviderCredentialState::from_bound_environment( + 1, + TestHashMap::from([("API_TOKEN".into(), "synthetic-private-token".into())]), + TestHashMap::new(), + if with_metadata { + crate::test_support::header_placement_metadata(&["API_TOKEN"]) + } else { + TestHashMap::new() + }, + TestHashMap::from([("API_TOKEN".into(), endpoint_binding("provider:API_TOKEN"))]), + Vec::new(), + ) + .expect("bound placement fixture") + } + + #[tokio::test] + async fn mixed_declared_and_fallback_credentials_preserve_opt_in_body_rewriting() { + let fallback_binding = endpoint_binding("provider:FALLBACK_TOKEN"); + let state = ProviderCredentialState::from_bound_environment( + 1, + TestHashMap::from([ + ("STRICT_TOKEN".into(), "strict-private-token".into()), + ("FALLBACK_TOKEN".into(), "fallback-private-token".into()), + ]), + TestHashMap::new(), + crate::test_support::header_placement_metadata(&["STRICT_TOKEN"]), + TestHashMap::from([ + ( + "STRICT_TOKEN".into(), + endpoint_binding("provider:STRICT_TOKEN"), + ), + ("FALLBACK_TOKEN".into(), fallback_binding), + ]), + Vec::new(), + ) + .expect("mixed placement fixture"); + let snapshot = state.snapshot(); + let strict = &snapshot.child_env["STRICT_TOKEN"]; + let fallback = &snapshot.child_env["FALLBACK_TOKEN"]; + let resolver = state + .resolver_for_endpoint("allowed.example.test", 443, "/allowed/search") + .unwrap(); + let body = format!("fallback={fallback}&strict={strict}"); + let target = "/allowed/search?probe=literal"; + let raw = format!( + "POST {target} HTTP/1.1\r\nHost: allowed.example.test\r\nAuthorization: token {strict}\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: {}\r\n\r\n{body}", + body.len() + ); + let request = crate::l7::rest::request_from_buffered_http( + "POST", + "/allowed/search", + target, + raw.into_bytes(), + ) + .unwrap(); + let (mut _app, mut client) = tokio::io::duplex(8192); + let (mut upstream, mut service) = tokio::io::duplex(8192); + let service_task = tokio::spawn(async move { + let mut bytes = Vec::new(); + loop { + let mut byte = [0]; + service.read_exact(&mut byte).await.unwrap(); + bytes.push(byte[0]); + if bytes.ends_with(b"\r\n\r\n") { + break; + } + } + let headers = String::from_utf8(bytes).unwrap(); + let length = headers + .lines() + .find_map(|line| { + let (name, value) = line.split_once(':')?; + name.eq_ignore_ascii_case("Content-Length") + .then(|| value.trim().parse::().unwrap()) + }) + .unwrap(); + let mut body = vec![0; length]; + service.read_exact(&mut body).await.unwrap(); + service + .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + .await + .unwrap(); + (headers, String::from_utf8(body).unwrap()) + }); + crate::l7::rest::relay_http_request_with_options_guarded( + &request, + &mut client, + &mut upstream, + crate::l7::rest::RelayRequestOptions { + resolver: Some(&resolver), + request_body_credential_rewrite: true, + ..Default::default() + }, + ) + .await + .unwrap(); + let (headers, body) = service_task.await.unwrap(); + assert!(headers.contains("/allowed/search?probe=literal")); + assert!(headers.contains("Authorization: token strict-private-token")); + assert_eq!( + body, + "fallback=fallback-private-token&strict=strict-private-token" + ); + assert!( + resolver + .rewrite_header_value_for_name("X-Legacy", strict) + .unwrap_err() + .is_placement_mismatch() + ); + } + + #[tokio::test] + async fn metadata_credentials_do_not_enable_disabled_body_rewriting() { + let state = placement_test_state(); + let resolver = state + .resolver_for_endpoint("allowed.example.test", 443, "/allowed/search") + .unwrap(); + let token = state.snapshot().child_env["API_TOKEN"].clone(); + let body = format!("token={token}"); + let raw = format!( + "POST /allowed/search HTTP/1.1\r\nHost: allowed.example.test\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: {}\r\n\r\n{body}", + body.len() + ); + let request = crate::l7::rest::request_from_buffered_http( + "POST", + "/allowed/search", + "/allowed/search", + raw.into_bytes(), + ) + .unwrap(); + let (mut _app, mut client) = tokio::io::duplex(8192); + let (mut upstream, mut service) = tokio::io::duplex(8192); + let error = crate::l7::rest::relay_http_request_with_options_guarded( + &request, + &mut client, + &mut upstream, + crate::l7::rest::RelayRequestOptions { + resolver: Some(&resolver), + request_body_credential_rewrite: false, + deny_uninspected_credentials: true, + ..Default::default() + }, + ) + .await + .expect_err("placement metadata must not enable body rewriting"); + assert!( + error + .downcast_ref::() + .is_some(), + "{error}" + ); + drop(upstream); + let mut sent = String::new(); + service.read_to_string(&mut sent).await.unwrap(); + assert!(!sent.contains("synthetic-private-token")); + } + + #[tokio::test] + async fn metadata_credentials_do_not_enable_disabled_websocket_rewriting() { + let state = placement_test_state(); + let token = state.snapshot().child_env["API_TOKEN"].clone(); + let ctx = L7EvalContext { + host: "allowed.example.test".into(), + port: 443, + provider_credentials: Some(state.clone()), + ..Default::default() + }; + let (mut app, mut client) = tokio::io::duplex(8192); + let (mut upstream, mut service) = tokio::io::duplex(8192); + let relay = tokio::spawn(async move { + handle_upgrade( + &mut client, + &mut upstream, + Vec::new(), + "allowed.example.test", + 443, + UpgradeRelayOptions { + websocket_request: true, + websocket: WebSocketUpgradeBehavior { + credential_rewrite: false, + deny_uninspected_credentials: true, + ..Default::default() + }, + secret_resolver: state.resolver(), + ctx: Some(&ctx), + target: "/allowed/socket".into(), + ..Default::default() + }, + ) + .await + }); + app.write_all(&masked_text_frame(token.as_bytes())) + .await + .unwrap(); + let error = tokio::time::timeout(std::time::Duration::from_secs(1), relay) + .await + .unwrap() + .unwrap() + .expect_err("disabled rewriting must deny credential frames"); + assert!(error.to_string().contains("rewrite is disabled"), "{error}"); + let mut sent = Vec::new(); + service.read_to_end(&mut sent).await.unwrap(); + assert!(sent.is_empty()); + } + + #[tokio::test] + async fn header_credential_query_reflection_is_denied_before_upstream_write() { + for with_metadata in [true, false] { + let state = placement_test_state_with_metadata(with_metadata); + let resolver = state + .resolver_for_endpoint("allowed.example.test", 443, "/allowed/search") + .unwrap(); + let token = state.snapshot().child_env["API_TOKEN"].clone(); + // A pagination service copies extra query parameters into its Link header. + // No request bytes may reach it when the parameter contains a header credential. + for (target, extra_headers, body, rewrite_body) in [ + ( + format!("/allowed/{token}/search"), + String::new(), + String::new(), + false, + ), + ( + format!("/allowed/search?q=rust&per_page=1&probe={token}"), + String::new(), + String::new(), + false, + ), + ( + "/allowed/search".into(), + format!("X-Reflect: {token}\r\n"), + String::new(), + false, + ), + ] { + let raw = format!( + "GET {target} HTTP/1.1\r\nHost: allowed.example.test\r\n{extra_headers}Content-Type: application/x-www-form-urlencoded\r\nContent-Length: {}\r\n\r\n{body}", + body.len() + ); + let request = crate::l7::rest::request_from_buffered_http( + "GET", + "/allowed/search", + &target, + raw.into_bytes(), + ) + .unwrap(); + let (mut app, mut client) = tokio::io::duplex(8192); + let (mut upstream, mut service) = tokio::io::duplex(8192); + let ctx = L7EvalContext { + host: "allowed.example.test".into(), + port: 443, + ..Default::default() + }; + let outcome = relay_http_request_with_credential_rejection( + &request, + &mut client, + &mut upstream, + crate::l7::rest::RelayRequestOptions { + resolver: Some(&resolver), + request_body_credential_rewrite: rewrite_body, + ..Default::default() + }, + &ctx, + None, + ) + .await + .unwrap(); + assert!(outcome.is_none()); + drop(client); + drop(upstream); + let mut response = String::new(); + app.read_to_string(&mut response).await.unwrap(); + let mut sent = Vec::new(); + service.read_to_end(&mut sent).await.unwrap(); + assert!( + sent.is_empty(), + "placement denial must precede all upstream writes" + ); + assert!(response.contains("403 Forbidden"), "{response}"); + assert!( + response.contains("credential_placement_mismatch"), + "{response}" + ); + assert!(!response.contains("synthetic-private-token")); + assert!(!response.contains(&token)); + } + let finding = crate::l7::build_credential_placement_mismatch_finding( + "test", + "allowed.example.test", + None, + ) + .to_json() + .unwrap(); + assert_eq!( + finding["finding_info"]["uid"], + "openshell.provider_credential.placement_mismatch" + ); + } + } + + #[tokio::test] + async fn permitted_auth_formats_preserve_pagination_without_disclosing_credentials() { + use base64::Engine as _; + for with_metadata in [true, false] { + let state = placement_test_state_with_metadata(with_metadata); + let resolver = state + .resolver_for_endpoint("allowed.example.test", 443, "/allowed/search") + .unwrap(); + let token = state.snapshot().child_env["API_TOKEN"].clone(); + for (authorization, expected) in [ + ( + format!("Bearer {token}"), + "Bearer synthetic-private-token".to_owned(), + ), + ( + format!("token {token}"), + "token synthetic-private-token".to_owned(), + ), + ( + format!( + "Basic {}", + base64::engine::general_purpose::STANDARD.encode(format!("user:{token}")) + ), + format!( + "Basic {}", + base64::engine::general_purpose::STANDARD + .encode("user:synthetic-private-token") + ), + ), + ] { + let target = "/allowed/search?q=rust&per_page=1"; + let raw = format!( + "GET {target} HTTP/1.1\r\nHost: allowed.example.test\r\nAuthorization: {authorization}\r\n\r\n" + ); + let request = crate::l7::rest::request_from_buffered_http( + "GET", + "/allowed/search", + target, + raw.into_bytes(), + ) + .unwrap(); + let (mut app, mut client) = tokio::io::duplex(8192); + let (mut upstream, mut service) = tokio::io::duplex(8192); + let service_task = tokio::spawn(async move { + let mut bytes = Vec::new(); + loop { + let mut byte = [0]; + service.read_exact(&mut byte).await.unwrap(); + bytes.push(byte[0]); + if bytes.ends_with(b"\r\n\r\n") { + break; + } + } + let received = String::from_utf8(bytes).unwrap(); + let target = received + .lines() + .next() + .unwrap() + .split_whitespace() + .nth(1) + .unwrap(); + let response = format!( + "HTTP/1.1 200 OK\r\nLink: ; rel=\"next\"\r\nContent-Length: 0\r\n\r\n" + ); + service.write_all(response.as_bytes()).await.unwrap(); + received + }); + crate::l7::rest::relay_http_request_with_options_guarded( + &request, + &mut client, + &mut upstream, + crate::l7::rest::RelayRequestOptions { + resolver: Some(&resolver), + ..Default::default() + }, + ) + .await + .unwrap(); + drop(client); + let mut response = String::new(); + app.read_to_string(&mut response).await.unwrap(); + let received = service_task.await.unwrap(); + assert!( + received.contains(&format!("Authorization: {expected}")), + "{received}" + ); + assert!(response.contains("per_page=1&page=2"), "{response}"); + assert!(!response.contains("synthetic-private-token")); + } + } + } + + #[tokio::test] + async fn declared_non_authorization_locations_reach_the_upstream() { + for location in ["header", "query", "path"] { + let mut metadata = crate::test_support::header_placement_metadata(&["API_TOKEN"]); + let declaration = metadata.values_mut().next().unwrap(); + declaration.auth_style = location.into(); + declaration.header_name = if location == "header" { + "x-api-key" + } else { + "" + } + .into(); + declaration.query_param = if location == "query" { "api_key" } else { "" }.into(); + declaration.path_template = if location == "path" { + "/allowed/{credential}/events" + } else { + "" + } + .into(); + let state = ProviderCredentialState::from_bound_environment( + 1, + TestHashMap::from([("API_TOKEN".into(), "synthetic-private-token".into())]), + TestHashMap::new(), + metadata, + TestHashMap::from([("API_TOKEN".into(), endpoint_binding("provider:API_TOKEN"))]), + Vec::new(), + ) + .unwrap(); + let token = state.snapshot().child_env["API_TOKEN"].clone(); + let (target, header, expected) = match location { + "header" => ( + "/allowed/search".into(), + format!("x-api-key: {token}\r\n"), + "x-api-key: synthetic-private-token", + ), + "query" => ( + format!("/allowed/search?api_key={token}"), + String::new(), + "/allowed/search?api_key=synthetic-private-token", + ), + _ => ( + format!("/allowed/{token}/events"), + String::new(), + "/allowed/synthetic-private-token/events", + ), + }; + let path = target.split('?').next().unwrap(); + let resolver = state + .resolver_for_endpoint("allowed.example.test", 443, path) + .unwrap(); + let raw = + format!("GET {target} HTTP/1.1\r\nHost: allowed.example.test\r\n{header}\r\n"); + let request = + crate::l7::rest::request_from_buffered_http("GET", path, &target, raw.into_bytes()) + .unwrap(); + let (mut _app, mut client) = tokio::io::duplex(8192); + let (mut upstream, mut service) = tokio::io::duplex(8192); + let service_task = tokio::spawn(async move { + let mut bytes = Vec::new(); + loop { + let mut byte = [0]; + service.read_exact(&mut byte).await.unwrap(); + bytes.push(byte[0]); + if bytes.ends_with(b"\r\n\r\n") { + break; + } + } + service + .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + .await + .unwrap(); + String::from_utf8(bytes).unwrap() + }); + crate::l7::rest::relay_http_request_with_options_guarded( + &request, + &mut client, + &mut upstream, + crate::l7::rest::RelayRequestOptions { + resolver: Some(&resolver), + ..Default::default() + }, + ) + .await + .unwrap(); + let sent = service_task.await.unwrap(); + assert!(sent.contains(expected), "{location}: {sent}"); + assert!(!sent.contains(&token)); + } + } + + #[tokio::test] + async fn trusted_sigv4_signing_preserves_metadata_free_credentials() { + let values = TestHashMap::from([ + ("AWS_ACCESS_KEY_ID".into(), "synthetic-access-key".into()), + ( + "AWS_SECRET_ACCESS_KEY".into(), + "synthetic-signing-secret".into(), + ), + ("AWS_SESSION_TOKEN".into(), "synthetic-session-token".into()), + ]); + let bindings = values + .keys() + .map(|key: &String| { + let binding = endpoint_binding(&format!("aws:{key}")); + (key.clone(), binding) + }) + .collect(); + let state = ProviderCredentialState::from_bound_environment( + 1, + values, + TestHashMap::new(), + TestHashMap::new(), + bindings, + Vec::new(), + ) + .unwrap(); + let resolver = state + .resolver_for_endpoint("allowed.example.test", 443, "/allowed/api") + .unwrap(); + let request = crate::l7::rest::request_from_buffered_http( + "GET", + "/allowed/api", + "/allowed/api", + b"GET /allowed/api HTTP/1.1\r\nHost: allowed.example.test\r\n\r\n".to_vec(), + ) + .unwrap(); + let (mut _app, mut client) = tokio::io::duplex(8192); + let (mut upstream, mut service) = tokio::io::duplex(8192); + let service_task = tokio::spawn(async move { + let mut bytes = Vec::new(); + loop { + let mut byte = [0]; + service.read_exact(&mut byte).await.unwrap(); + bytes.push(byte[0]); + if bytes.ends_with(b"\r\n\r\n") { + break; + } + } + service + .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + .await + .unwrap(); + String::from_utf8(bytes).unwrap() + }); + crate::l7::rest::relay_http_request_with_options_guarded( + &request, + &mut client, + &mut upstream, + crate::l7::rest::RelayRequestOptions { + resolver: Some(&resolver), + credential_signing: crate::l7::CredentialSigning::SigV4NoBody, + signing_service: "execute-api", + signing_region: "us-west-2", + host: "allowed.example.test", + port: 443, + ..Default::default() + }, + ) + .await + .unwrap(); + let sent = service_task.await.unwrap(); + assert!(sent.contains("AWS4-HMAC-SHA256"), "{sent}"); + assert!(sent.contains("synthetic-access-key")); + assert!(sent.contains("synthetic-session-token")); + assert!(!sent.contains("synthetic-signing-secret")); + } + #[tokio::test] async fn body_only_endpoint_mismatch_returns_typed_403() { let (_state, resolver) = endpoint_mismatch_resolver(TestHashMap::from([( @@ -4800,13 +5467,16 @@ network_policies: endpoints: - host: api.example.test port: 8080 - protocol: rest + protocol: websocket token_grant_owner: test-owner enforcement: enforce rules: - allow: method: GET path: "/v1/**" + - allow: + method: WEBSOCKET_TEXT + path: "/v1/**" binaries: - { path: /usr/bin/curl } "#; @@ -4862,7 +5532,10 @@ network_policies: ) .await .expect("send upgrade request"); - seen_rx.recv().await.expect("preflight reached middleware"); + tokio::time::timeout(std::time::Duration::from_secs(2), seen_rx.recv()) + .await + .expect("preflight timed out") + .expect("preflight reached middleware"); fixture.assert_no_requests(); release.notify_one(); @@ -9405,7 +10078,7 @@ network_policies: } #[tokio::test] - async fn rest_websocket_middleware_inspects_compressed_wss_messages() { + async fn websocket_middleware_inspects_compressed_wss_messages() { let data = r#" network_middlewares: redact: @@ -9419,12 +10092,15 @@ network_policies: endpoints: - host: api.example.test port: 8080 - protocol: rest + protocol: websocket enforcement: enforce rules: - allow: method: GET path: "/ws" + - allow: + method: WEBSOCKET_TEXT + path: "/ws" binaries: - { path: /usr/bin/node } "#; @@ -9513,7 +10189,7 @@ network_policies: endpoints: - host: gateway.example.test port: 443 - protocol: rest + protocol: websocket enforcement: enforce rules: - allow: @@ -9527,7 +10203,7 @@ network_policies: .clone_engine_for_tunnel(engine.current_generation()) .unwrap(); let configs = vec![L7EndpointConfig { - protocol: L7Protocol::Rest, + protocol: L7Protocol::Websocket, endpoint_id: String::new(), policy_hash: String::new(), path: "/ws".into(), @@ -10332,9 +11008,140 @@ network_policies: } #[tokio::test] - async fn route_selected_rest_websocket_upgrade_still_relays_beside_mcp() { - // The refusal follows the selected endpoint's protocol: a REST - // upgrade on the same host and port keeps its documented raw relay. + async fn rest_upgrade_refusal_emits_ocsf_denial() { + let (configs, _, ctx) = jsonrpc_and_rest_route_configs("mcp", "enforce"); + let config = configs + .iter() + .find(|c| c.protocol == L7Protocol::Rest) + .unwrap(); + let request = crate::l7::provider::L7Request { + action: "GET".into(), + target: "/api/ws".into(), + query_params: std::collections::HashMap::new(), + raw_header: b"GET /api/ws HTTP/1.1\r\nHost: mcp.example.test:8000\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n".to_vec(), + body_length: crate::l7::provider::BodyLength::None, + }; + let capture = openshell_ocsf::test_support::OcsfCapture::default(); + let _guard = capture.install(); + let (_app, mut client) = tokio::io::duplex(8192); + assert!( + deny_unsupported_upgrade_if_requested(&request, config, &ctx, None, &mut client) + .await + .unwrap() + ); + let events = capture.json_events(); + assert!( + events.iter().any(|event| event["action_id"] == 2 + && event["status_detail"] == crate::l7::rest::UNSUPPORTED_REST_UPGRADE_DETAIL), + "{events:?}" + ); + } + + #[tokio::test] + async fn rest_upgrades_are_refused_in_both_modes_and_relay_paths() { + for enforcement in [EnforcementMode::Enforce, EnforcementMode::Audit] { + for route_selected in [false, true] { + for upgrade in ["websocket", "custom"] { + let (mut configs, engine, ctx) = + jsonrpc_and_rest_route_configs("mcp", "enforce"); + let config = configs + .iter_mut() + .find(|c| c.protocol == L7Protocol::Rest) + .unwrap(); + config.enforcement = enforcement; + let config = config.clone(); + let request = format!( + "GET /api/ws HTTP/1.1\r\nHost: mcp.example.test:8000\r\nConnection: keep-alive, Upgrade\r\nUpgrade: {upgrade}\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n" + ); + let frame = br#"{"action":"delete"}"#; + let scenario = run_upgrade_scenario( + request.as_bytes(), + frame, + move |mut client, mut upstream| { + tokio::spawn(async move { + if route_selected { + relay_with_route_selection( + &configs, + engine, + &mut client, + &mut upstream, + &ctx, + ) + .await + } else { + relay_with_inspection( + &config, + engine, + &mut client, + &mut upstream, + &ctx, + ) + .await + } + }) + }, + ) + .await; + assert_upgrade_refused_before_forwarding( + &scenario, + frame, + crate::l7::rest::UNSUPPORTED_REST_UPGRADE_DETAIL, + ); + } + } + } + } + + #[tokio::test] + async fn rest_upgrade_after_ordinary_request_is_refused() { + for route_selected in [false, true] { + let (configs, engine, ctx) = jsonrpc_and_rest_route_configs("mcp", "enforce"); + let config = configs + .iter() + .find(|c| c.protocol == L7Protocol::Rest) + .unwrap() + .clone(); + let (mut app, mut client) = tokio::io::duplex(8192); + let (mut upstream, mut server) = tokio::io::duplex(8192); + let relay = tokio::spawn(async move { + if route_selected { + relay_with_route_selection(&configs, engine, &mut client, &mut upstream, &ctx) + .await + } else { + relay_with_inspection(&config, engine, &mut client, &mut upstream, &ctx).await + } + }); + app.write_all(b"GET /api/tickets HTTP/1.1\r\nHost: mcp.example.test:8000\r\n\r\n") + .await + .unwrap(); + let forwarded = read_http_headers(&mut server).await; + assert!(String::from_utf8_lossy(&forwarded).starts_with("GET /api/tickets ")); + server + .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + .await + .unwrap(); + let response = read_http_headers(&mut app).await; + assert!(String::from_utf8_lossy(&response).starts_with("HTTP/1.1 200")); + app.write_all(b"GET /api/ws HTTP/1.1\r\nHost: mcp.example.test:8000\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n").await.unwrap(); + let mut denied = Vec::new(); + tokio::time::timeout( + std::time::Duration::from_secs(2), + app.read_to_end(&mut denied), + ) + .await + .unwrap() + .unwrap(); + assert!(String::from_utf8_lossy(&denied).starts_with("HTTP/1.1 403")); + relay.await.unwrap().unwrap(); + let mut extra = Vec::new(); + server.read_to_end(&mut extra).await.unwrap(); + assert!(extra.is_empty(), "upgrade must not reach the upstream"); + } + } + + #[tokio::test] + async fn route_selected_rest_websocket_upgrade_is_refused_beside_mcp() { + // Both per-request protocols refuse upgrades on their selected path. let (configs, tunnel_engine, ctx) = jsonrpc_and_rest_route_configs("mcp", "enforce"); let frame = br#"{"type":"ping"}"#; let scenario = run_upgrade_scenario( @@ -10354,15 +11161,11 @@ network_policies: }, ) .await; - assert!( - scenario.response.starts_with("HTTP/1.1 101"), - "REST upgrade should still switch protocols, got: {}", - scenario.response + assert_upgrade_refused_before_forwarding( + &scenario, + frame, + crate::l7::rest::UNSUPPORTED_REST_UPGRADE_DETAIL, ); - assert!(contains_bytes( - &scenario.upstream_seen, - &masked_text_frame(frame) - )); } #[tokio::test] diff --git a/crates/openshell-supervisor-network/src/l7/rest.rs b/crates/openshell-supervisor-network/src/l7/rest.rs index 7b85942666..27f10ae7bd 100644 --- a/crates/openshell-supervisor-network/src/l7/rest.rs +++ b/crates/openshell-supervisor-network/src/l7/rest.rs @@ -88,6 +88,7 @@ pub(crate) const UNSUPPORTED_H2C_UPGRADE_DETAIL: &str = pub(crate) const UNSUPPORTED_JSONRPC_UPGRADE_DETAIL: &str = "HTTP upgrade is not supported for JSON-RPC or MCP endpoints"; pub(crate) const UNSUPPORTED_GRAPHQL_UPGRADE_DETAIL: &str = "HTTP upgrade is not supported for GraphQL endpoints; serve GraphQL over WebSocket from a separate protocol: websocket endpoint on another path or port"; +pub(crate) const UNSUPPORTED_REST_UPGRADE_DETAIL: &str = "HTTP upgrade is not supported for REST endpoints; use a separate protocol: websocket endpoint with explicit message rules"; const MIN_HTTP2_PREFACE_DETECTION_BYTES: usize = 8; /// Idle timeout for `relay_until_eof`. If no data arrives within this window @@ -1184,7 +1185,7 @@ where port = options.port, )) .build(); - openshell_ocsf::ocsf_emit!(event); + crate::telemetry::egress_emit!(event); } _ => { return Err(miette::Report::new(CredentialUnavailableError::new( @@ -1534,8 +1535,9 @@ fn emit_uninspected_body_credential_denial( req.action, options.host, options.port )) .build(); - openshell_ocsf::ocsf_emit!(event); + crate::telemetry::egress_emit!(event); let finding = openshell_ocsf::DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) + .action(openshell_ocsf::ActionId::Denied) .severity(openshell_ocsf::SeverityId::High) .finding_info(openshell_ocsf::FindingInfo::new( "openshell.credentials.traffic_uninspectable", @@ -1548,7 +1550,7 @@ fn emit_uninspected_body_credential_denial( ]) .message("Request body credential placeholder denied") .build(); - openshell_ocsf::ocsf_emit!(finding); + crate::telemetry::egress_emit!(finding); } struct PreparedRequestBody { @@ -2441,11 +2443,11 @@ pub(crate) fn unsupported_upgrade_detail( /// individual HTTP requests, or `None` for a protocol that may relay an /// allowed upgrade. /// -/// JSON-RPC, MCP and GraphQL rules inspect each HTTP request body or query. +/// REST, JSON-RPC, MCP and GraphQL rules inspect individual HTTP requests. /// After an upgrade the relay would copy frames that no rule of these /// protocols evaluates, so they never upgrade; GraphQL over WebSocket is /// served by separate `protocol: websocket` endpoints with GraphQL operation -/// rules. REST and WebSocket endpoints relay allowed upgrades, and SQL +/// rules. WebSocket endpoints relay allowed upgrades, and SQL /// endpoints keep their existing upgrade behavior. The match is exhaustive so /// a new protocol must choose. pub(crate) fn upgrade_refusal_for_protocol( @@ -2455,7 +2457,8 @@ pub(crate) fn upgrade_refusal_for_protocol( match protocol { L7Protocol::JsonRpc | L7Protocol::Mcp => Some(UNSUPPORTED_JSONRPC_UPGRADE_DETAIL), L7Protocol::Graphql => Some(UNSUPPORTED_GRAPHQL_UPGRADE_DETAIL), - L7Protocol::Rest | L7Protocol::Websocket | L7Protocol::Sql => None, + L7Protocol::Rest => Some(UNSUPPORTED_REST_UPGRADE_DETAIL), + L7Protocol::Websocket | L7Protocol::Sql => None, } } @@ -6916,6 +6919,256 @@ mod tests { (outcome, delivered) } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn response_trace_failure_records_share_the_terminal_decision() { + use crate::telemetry::tests::{assert_deny_resolves, deny_spans, exported}; + + // This relay fixture has no CONNECT span, so denies are roots at both + // levels. The thread-local export harness observes the relay task; + // emissions in spawned tasks would need explicit subscriber propagation. + for level in ["warn", "debug"] { + for (method, script) in [ + ("GET", ResponseRelayScript::BlockStream), + ("GET", ResponseRelayScript::InvalidBodySequence), + ("GET", ResponseRelayScript::InvalidWholeBodySequence), + ("HEAD", ResponseRelayScript::WholeBody), + ] { + let (events, received) = exported(level, || { + let (outcome, delivered) = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(run_response_middleware_relay( + b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello", + method, + script, + )) + }); + if method == "HEAD" + || matches!(script, ResponseRelayScript::InvalidWholeBodySequence) + { + assert!(matches!(outcome.unwrap(), RelayOutcome::Consumed)); + assert!(delivered.starts_with(b"HTTP/1.1 502 Bad Gateway\r\n")); + } else { + assert!(outcome.is_err()); + assert!(delivered.starts_with(b"HTTP/1.1 200 OK\r\n")); + assert!(!String::from_utf8_lossy(&delivered).contains("502 Bad Gateway")); + } + }) + .await; + let terminal = events + .iter() + .find(|event| { + event["finding_info"]["uid"] == "openshell.middleware.http_response_failure" + }) + .expect("terminal delivery failure is recorded"); + let companions: Vec<_> = events + .iter() + .filter(|event| { + event.get("finding_info").is_some() + || event["unmapped"]["failed"] == true + || event["action_id"] == openshell_ocsf::ActionId::Denied as u8 + }) + .cloned() + .collect(); + assert!(companions.len() >= 2, "failure includes diagnostic records"); + assert_eq!( + deny_spans(&received).len(), + 1, + "one response stop is one exported decision" + ); + assert!(terminal.get("trace").is_none()); + assert_deny_resolves(&companions, &received, true); + } + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn credential_refusal_findings_record_denial_without_http_spans() { + let request = L7Request { + action: "POST".into(), + target: "/api".into(), + query_params: HashMap::new(), + raw_header: vec![], + body_length: BodyLength::None, + }; + let (events, received) = crate::telemetry::tests::exported("warn", || { + crate::l7::emit_uninspected_credential_finding("example.test", "p", "test"); + emit_uninspected_body_credential_denial( + &request, + &RelayRequestOptions { + host: "example.test", + port: 443, + ..Default::default() + }, + BodyCredentialError::KnownUnavailable, + ); + }) + .await; + assert_eq!(events.len(), 3); + assert_eq!( + events + .iter() + .filter(|event| event["class_uid"] == 2004) + .count(), + 2 + ); + for event in &events { + assert_eq!(event["action_id"], 2); + assert_eq!(event["action"], "Denied"); + } + crate::telemetry::tests::assert_no_deny(&events, &received); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn response_trace_upstream_io_failure_is_not_an_enforced_denial() { + struct FailedReader; + impl AsyncRead for FailedReader { + fn poll_read( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + _buf: &mut ReadBuf<'_>, + ) -> Poll> { + Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::ConnectionReset, + "upstream response interrupted", + ))) + } + } + + for script in [ResponseRelayScript::Stream, ResponseRelayScript::WholeBody] { + let (events, received) = crate::telemetry::tests::exported("warn", || { + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + let (runner, chain) = response_middleware_fixture(script); + let mut upstream = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n" + .as_slice() + .chain(FailedReader); + let mut delivered = Vec::new(); + let outcome = relay_response( + "GET", + &mut upstream, + &mut delivered, + RelayResponseOptions::default(), + Some(response_middleware_context(&runner, &chain, "GET")), + ) + .await; + if matches!(script, ResponseRelayScript::Stream) { + assert!(outcome.is_err()); + assert!(delivered.starts_with(b"HTTP/1.1 200 OK\r\n")); + assert!( + !String::from_utf8_lossy(&delivered).contains("502 Bad Gateway") + ); + } else { + assert!(matches!(outcome.unwrap(), RelayOutcome::Consumed)); + assert!(delivered.starts_with(b"HTTP/1.1 502 Bad Gateway\r\n")); + } + }); + }); + }) + .await; + let finding = events + .iter() + .find(|event| { + event["finding_info"]["uid"] == "openshell.middleware.http_response_failure" + }) + .expect("upstream I/O failure retains its diagnostic finding"); + assert!(finding.get("action_id").is_none()); + assert!(finding.get("action").is_none()); + crate::telemetry::tests::assert_no_deny(&events, &received); + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn response_trace_downstream_body_failure_is_not_an_enforced_denial() { + #[derive(Default)] + struct DisconnectAfterHead(Vec); + impl AsyncWrite for DisconnectAfterHead { + fn poll_write( + mut self: Pin<&mut Self>, + _cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + if self.0.ends_with(b"\r\n\r\n") { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "client disconnected", + ))); + } + self.0.extend_from_slice(buf); + Poll::Ready(Ok(buf.len())) + } + fn poll_flush( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn poll_shutdown( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + } + + let (events, received) = crate::telemetry::tests::exported("warn", || { + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + let (runner, chain) = response_middleware_fixture(ResponseRelayScript::Stream); + let mut upstream = + b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello".as_slice(); + let mut delivered = DisconnectAfterHead::default(); + let outcome = relay_response( + "GET", + &mut upstream, + &mut delivered, + RelayResponseOptions::default(), + Some(response_middleware_context(&runner, &chain, "GET")), + ) + .await; + assert!(outcome.is_err()); + assert!(delivered.0.starts_with(b"HTTP/1.1 200 OK\r\n")); + assert!(!String::from_utf8_lossy(&delivered.0).contains("502 Bad Gateway")); + }); + }); + }) + .await; + let finding = events + .iter() + .find(|event| { + event["finding_info"]["uid"] == "openshell.middleware.http_response_failure" + }) + .expect("downstream I/O failure retains its diagnostic finding"); + assert!(finding.get("action_id").is_none()); + assert!(finding.get("action").is_none()); + crate::telemetry::tests::assert_no_deny(&events, &received); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn response_trace_allowed_and_fail_open_controls_do_not_create_deny_spans() { + for script in [ + ResponseRelayScript::Stream, + ResponseRelayScript::InvalidBodySequence, + ResponseRelayScript::InvalidWholeBodySequence, + ] { + let (events, received) = crate::telemetry::tests::exported("warn", || { + let (outcome, delivered) = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on( + run_response_middleware_relay_with_error( + b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello", + "GET", + script, + openshell_supervisor_middleware::OnError::FailOpen, + ), + ) + }); + assert!(outcome.is_ok()); + assert!(delivered.starts_with(b"HTTP/1.1 200 OK\r\n")); + }) + .await; + assert!(!events.is_empty()); + crate::telemetry::tests::assert_no_deny(&events, &received); + } + } + #[tokio::test] async fn response_middleware_headers_only_mutates_head_and_preserves_framing() { let (outcome, delivered) = run_response_middleware_relay( @@ -9253,16 +9506,10 @@ mod tests { let websocket = format!( "GET /ws HTTP/1.1\r\nHost: example.com\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: {VALID_WS_KEY}\r\nSec-WebSocket-Version: 13\r\n\r\n" ); - for protocol in [ - crate::l7::L7Protocol::Rest, - crate::l7::L7Protocol::Websocket, - ] { - assert_eq!( - unsupported_upgrade_detail(websocket.as_bytes(), protocol), - None, - "{protocol:?}" - ); - } + assert_eq!( + unsupported_upgrade_detail(websocket.as_bytes(), crate::l7::L7Protocol::Websocket), + None, + ); // Streamable HTTP requests, a look-alike header name, and a stray // `Connection: upgrade` without an `Upgrade` header stay allowed; @@ -9275,6 +9522,7 @@ mod tests { ]; for raw in ordinary { for protocol in [ + crate::l7::L7Protocol::Rest, crate::l7::L7Protocol::JsonRpc, crate::l7::L7Protocol::Mcp, crate::l7::L7Protocol::Graphql, @@ -9325,7 +9573,11 @@ mod tests { upgrade_refusal_for_protocol(L7Protocol::Graphql), Some(UNSUPPORTED_GRAPHQL_UPGRADE_DETAIL) ); - for protocol in [L7Protocol::Rest, L7Protocol::Websocket, L7Protocol::Sql] { + assert_eq!( + upgrade_refusal_for_protocol(L7Protocol::Rest), + Some(UNSUPPORTED_REST_UPGRADE_DETAIL) + ); + for protocol in [L7Protocol::Websocket, L7Protocol::Sql] { assert_eq!(upgrade_refusal_for_protocol(protocol), None, "{protocol:?}"); } } @@ -9363,6 +9615,7 @@ mod tests { "fixture must be an upgrade to the relay: {raw}" ); for protocol in [ + crate::l7::L7Protocol::Rest, crate::l7::L7Protocol::JsonRpc, crate::l7::L7Protocol::Mcp, crate::l7::L7Protocol::Graphql, @@ -9670,6 +9923,70 @@ mod tests { // receives. // ========================================================================= + fn placed_test_resolver( + environment: HashMap, + host: &str, + declarations: &[(&str, &str, &str)], + ) -> (HashMap, Option) { + use openshell_core::proto::{ + ProviderProfileCredential, StaticCredentialBinding, StaticCredentialEndpointBinding, + }; + use openshell_core::provider_credentials::{ + ProviderCredentialState, STATIC_CREDENTIAL_METADATA_PREFIX, + }; + let metadata = declarations + .iter() + .map(|(key, style, selector)| { + let mut credential = ProviderProfileCredential { + env_vars: vec![(*key).into()], + auth_style: (*style).into(), + ..Default::default() + }; + match *style { + "header" => credential.header_name = (*selector).into(), + "query" => credential.query_param = (*selector).into(), + "path" => credential.path_template = (*selector).into(), + _ => panic!("fixture needs an explicit placement"), + } + ( + format!("{STATIC_CREDENTIAL_METADATA_PREFIX}{key}"), + credential, + ) + }) + .collect(); + let bindings = environment + .keys() + .map(|key| { + ( + key.clone(), + StaticCredentialBinding { + credential_identity: format!("fixture:{key}"), + workload_credential_handle: String::new(), + endpoints: vec![StaticCredentialEndpointBinding { + host: host.into(), + port: 443, + path: "/**".into(), + }], + }, + ) + }) + .collect(); + let state = ProviderCredentialState::from_bound_environment( + 1, + environment, + HashMap::new(), + metadata, + bindings, + Vec::new(), + ) + .unwrap(); + let child_env = state.snapshot().child_env.clone(); + let resolver = state + .resolver_for_endpoint(host, 443, "/") + .map(|resolver| resolver.as_ref().clone()); + (child_env, resolver) + } + /// Helper: run a request through the relay and capture what upstream receives. async fn relay_and_capture( raw_header: Vec, @@ -10347,10 +10664,12 @@ mod tests { #[tokio::test] async fn relay_injects_exact_header_credential() { - let (child_env, resolver) = SecretResolver::from_provider_env( + let (child_env, resolver) = placed_test_resolver( [("CUSTOM_TOKEN".to_string(), "tok-12345".to_string())] .into_iter() .collect(), + "api.example.com", + &[("CUSTOM_TOKEN", "header", "x-api-key")], ); let placeholder = child_env.get("CUSTOM_TOKEN").unwrap(); @@ -10424,10 +10743,12 @@ mod tests { #[tokio::test] async fn relay_injects_query_param_credential() { - let (child_env, resolver) = SecretResolver::from_provider_env( + let (child_env, resolver) = placed_test_resolver( [("YOUTUBE_KEY".to_string(), "AIzaSy-secret".to_string())] .into_iter() .collect(), + "www.googleapis.com", + &[("YOUTUBE_KEY", "query", "key")], ); let placeholder = child_env.get("YOUTUBE_KEY").unwrap(); @@ -10535,13 +10856,15 @@ mod tests { #[tokio::test] async fn relay_injects_url_path_credential_telegram_style() { - let (child_env, resolver) = SecretResolver::from_provider_env( + let (child_env, resolver) = placed_test_resolver( [( "TELEGRAM_TOKEN".to_string(), "123456:ABC-DEF1234ghIkl".to_string(), )] .into_iter() .collect(), + "api.telegram.org", + &[("TELEGRAM_TOKEN", "path", "/bot{credential}/sendMessage")], ); let placeholder = child_env.get("TELEGRAM_TOKEN").unwrap(); @@ -10568,10 +10891,12 @@ mod tests { #[tokio::test] async fn relay_injects_url_path_credential_standalone_segment() { - let (child_env, resolver) = SecretResolver::from_provider_env( + let (child_env, resolver) = placed_test_resolver( [("ORG_TOKEN".to_string(), "org-abc-789".to_string())] .into_iter() .collect(), + "api.example.com", + &[("ORG_TOKEN", "path", "/api/{credential}/resources")], ); let placeholder = child_env.get("ORG_TOKEN").unwrap(); @@ -10598,13 +10923,18 @@ mod tests { #[tokio::test] async fn relay_injects_combined_path_and_header_credentials() { - let (child_env, resolver) = SecretResolver::from_provider_env( + let (child_env, resolver) = placed_test_resolver( [ ("PATH_TOKEN".to_string(), "tok-path-123".to_string()), ("HEADER_KEY".to_string(), "sk-header-456".to_string()), ] .into_iter() .collect(), + "api.example.com", + &[ + ("PATH_TOKEN", "path", "/bot{credential}/send"), + ("HEADER_KEY", "header", "x-api-key"), + ], ); let path_ph = child_env.get("PATH_TOKEN").unwrap(); let header_ph = child_env.get("HEADER_KEY").unwrap(); diff --git a/crates/openshell-supervisor-network/src/l7/rest/http_response.rs b/crates/openshell-supervisor-network/src/l7/rest/http_response.rs index 3efc323bdb..0113dc4323 100644 --- a/crates/openshell-supervisor-network/src/l7/rest/http_response.rs +++ b/crates/openshell-supervisor-network/src/l7/rest/http_response.rs @@ -408,16 +408,27 @@ where allowed = preflight.allowed, "HTTP response middleware preflight completed" ); - for event in crate::l7::middleware::middleware_finding_events(&preflight.findings) { - openshell_ocsf::ocsf_emit!(event); - } + let mut preflight_events = + crate::l7::middleware::middleware_finding_events(&preflight.findings); if !preflight.allowed { - emit_http_response_middleware_invocations( + preflight_events.extend(http_response_decision_events( middleware.policy_name, &middleware.target, status_code, &preflight.invocations, - ); + )); + if preflight.denial.is_none() { + preflight_events.push(http_response_middleware_failure_event( + middleware.policy_name, + &middleware.target, + status_code, + false, + true, + )); + } + } + crate::telemetry::emit_decision(preflight_events, None); + if !preflight.allowed { if let Some(denial) = preflight.denial.as_ref() { send_response_middleware_denial( client, @@ -428,12 +439,6 @@ where ) .await?; } else { - emit_http_response_middleware_failure( - middleware.policy_name, - &middleware.target, - status_code, - false, - ); send_response_delivery_failure( client, request_method, @@ -444,11 +449,14 @@ where } return Ok(Some(RelayOutcome::Consumed)); } - emit_http_response_middleware_invocations( - middleware.policy_name, - &middleware.target, - status_code, - &preflight.invocations, + crate::telemetry::emit_decision( + http_response_decision_events( + middleware.policy_name, + &middleware.target, + status_code, + &preflight.invocations, + ), + None, ); let Some(mut session) = preflight.session else { @@ -482,12 +490,21 @@ where Ok(finish) => finish, Err(error) => { debug!(error = %error, "HTTP response middleware finalization failed"); - emit_http_response_diagnostics( + let mut events = http_response_diagnostic_events( middleware.policy_name, &middleware.target, status_code, - &error.diagnostics, + &error.diagnostics.invocations, + &error.diagnostics.findings, ); + events.push(http_response_middleware_failure_event( + middleware.policy_name, + &middleware.target, + status_code, + false, + true, + )); + crate::telemetry::emit_decision(events, None); send_response_delivery_failure( client, request_method, @@ -502,15 +519,16 @@ where guard.ensure_current()?; } let mut headers = preflight.headers; - emit_http_response_middleware_invocations( - middleware.policy_name, - &middleware.target, - status_code, - &finish.invocations, + crate::telemetry::emit_decision( + http_response_diagnostic_events( + middleware.policy_name, + &middleware.target, + status_code, + &finish.invocations, + &finish.findings, + ), + None, ); - for event in crate::l7::middleware::middleware_finding_events(&finish.findings) { - openshell_ocsf::ocsf_emit!(event); - } if finish.strip_stale_integrity_headers { strip_response_integrity_headers(&mut headers); } @@ -610,13 +628,30 @@ where openshell_core::proto::MiddlewareSessionEndReason::UpstreamDisconnect }; session.end(end_reason).await; - if committed { - emit_http_response_middleware_failure( + // Diagnostics from a middleware stop and its terminal finding are + // one decision. Transport/framing errors still record delivery + // failure, but do not imply an enforced middleware refusal. + let mut events = middleware_stop.map_or_else(Vec::new, |stop| { + http_response_diagnostic_events( middleware.policy_name, &middleware.target, status_code, - true, - ); + &stop.diagnostics.invocations, + &stop.diagnostics.findings, + ) + }); + // A denial sent before commitment is reported by its invocation. + if committed || middleware_stop.is_none_or(|stop| stop.failure.denial.is_none()) { + events.push(http_response_middleware_failure_event( + middleware.policy_name, + &middleware.target, + status_code, + committed, + middleware_stop.is_some(), + )); + } + crate::telemetry::emit_decision(events, None); + if committed { return Err(error); } debug!(error = %error, "HTTP response processing failed before commitment"); @@ -630,12 +665,6 @@ where ) .await?; } else { - emit_http_response_middleware_failure( - middleware.policy_name, - &middleware.target, - status_code, - false, - ); send_response_delivery_failure( client, request_method, @@ -660,19 +689,24 @@ where let finish = match session.finish(trailers).await { Ok(finish) => finish, Err(error) => { - emit_http_response_diagnostics( + let mut events = http_response_diagnostic_events( middleware.policy_name, &middleware.target, status_code, - &error.diagnostics, + &error.diagnostics.invocations, + &error.diagnostics.findings, ); - if committed { - emit_http_response_middleware_failure( + if committed || error.denial.is_none() { + events.push(http_response_middleware_failure_event( middleware.policy_name, &middleware.target, status_code, + committed, true, - ); + )); + } + crate::telemetry::emit_decision(events, None); + if committed { return Err(miette!( "HTTP response middleware failed after commitment: {error}" )); @@ -688,12 +722,6 @@ where ) .await?; } else { - emit_http_response_middleware_failure( - middleware.policy_name, - &middleware.target, - status_code, - false, - ); send_response_delivery_failure( client, request_method, @@ -708,15 +736,16 @@ where if let Some(guard) = middleware.generation_guard { guard.ensure_current()?; } - emit_http_response_middleware_invocations( - middleware.policy_name, - &middleware.target, - status_code, - &finish.invocations, + crate::telemetry::emit_decision( + http_response_diagnostic_events( + middleware.policy_name, + &middleware.target, + status_code, + &finish.invocations, + &finish.findings, + ), + None, ); - for event in crate::l7::middleware::middleware_finding_events(&finish.findings) { - openshell_ocsf::ocsf_emit!(event); - } if whole_body && !committed { let mut headers = preflight.headers; @@ -861,29 +890,39 @@ where }) } -fn emit_http_response_middleware_invocations( +fn http_response_decision_events( policy_name: &str, target: &HttpRequestTarget, status_code: u16, invocations: &[openshell_supervisor_middleware::HttpResponseInvocation], -) { - for event in - http_response_middleware_invocation_events(policy_name, target, status_code, invocations) - { - openshell_ocsf::ocsf_emit!(event); - } +) -> Vec { + let mut events = + http_response_middleware_invocation_events(policy_name, target, status_code, invocations); for invocation in invocations { - if let Some(event) = - http_response_middleware_fail_open_finding_event(policy_name, target, invocation) - { - openshell_ocsf::ocsf_emit!(event); - } - if let Some(event) = - http_response_middleware_block_finding_event(policy_name, target, invocation) - { - openshell_ocsf::ocsf_emit!(event); - } - } + events.extend(http_response_middleware_fail_open_finding_event( + policy_name, + target, + invocation, + )); + events.extend(http_response_middleware_block_finding_event( + policy_name, + target, + invocation, + )); + } + events +} + +fn http_response_diagnostic_events( + policy_name: &str, + target: &HttpRequestTarget, + status_code: u16, + invocations: &[openshell_supervisor_middleware::HttpResponseInvocation], + findings: &[openshell_supervisor_middleware::NamespacedFinding], +) -> Vec { + let mut events = http_response_decision_events(policy_name, target, status_code, invocations); + events.extend(crate::l7::middleware::middleware_finding_events(findings)); + events } fn emit_http_response_diagnostics( @@ -892,15 +931,16 @@ fn emit_http_response_diagnostics( status_code: u16, diagnostics: &openshell_supervisor_middleware::HttpResponseDiagnostics, ) { - emit_http_response_middleware_invocations( - policy_name, - target, - status_code, - &diagnostics.invocations, + crate::telemetry::emit_decision( + http_response_diagnostic_events( + policy_name, + target, + status_code, + &diagnostics.invocations, + &diagnostics.findings, + ), + None, ); - for event in crate::l7::middleware::middleware_finding_events(&diagnostics.findings) { - openshell_ocsf::ocsf_emit!(event); - } } pub(super) fn http_response_middleware_invocation_events( @@ -914,8 +954,11 @@ pub(super) fn http_response_middleware_invocation_events( .map(|invocation| { let outcome = format!("{:?}", invocation.outcome).to_ascii_lowercase(); let failed = invocation.failed; - let blocked = invocation.outcome - == openshell_supervisor_middleware::HttpResponseInvocationOutcome::BlockDelivery; + let blocked = matches!( + invocation.outcome, + openshell_supervisor_middleware::HttpResponseInvocationOutcome::BlockDelivery + | openshell_supervisor_middleware::HttpResponseInvocationOutcome::FailClosed + ); openshell_ocsf::HttpActivityBuilder::new(ocsf_ctx()) .activity(openshell_ocsf::ActivityId::Other) .action(if blocked { @@ -991,6 +1034,7 @@ fn http_response_middleware_block_finding_event( } Some( openshell_ocsf::DetectionFindingBuilder::new(ocsf_ctx()) + .action(openshell_ocsf::ActionId::Denied) .severity(openshell_ocsf::SeverityId::Medium) .finding_info(openshell_ocsf::FindingInfo::new( "openshell.middleware.http_response_blocked", @@ -1068,8 +1112,28 @@ fn emit_http_response_middleware_failure( status_code: u16, committed: bool, ) { + crate::telemetry::egress_emit!(http_response_middleware_failure_event( + policy_name, + target, + status_code, + committed, + true, + )); +} + +fn http_response_middleware_failure_event( + policy_name: &str, + target: &HttpRequestTarget, + status_code: u16, + committed: bool, + denied: bool, +) -> openshell_ocsf::OcsfEvent { let status_code = status_code.to_string(); - let event = openshell_ocsf::DetectionFindingBuilder::new(ocsf_ctx()) + let mut builder = openshell_ocsf::DetectionFindingBuilder::new(ocsf_ctx()); + if denied { + builder = builder.action(openshell_ocsf::ActionId::Denied); + } + builder .severity(openshell_ocsf::SeverityId::High) .finding_info(openshell_ocsf::FindingInfo::new( "openshell.middleware.http_response_failure", @@ -1093,8 +1157,7 @@ fn emit_http_response_middleware_failure( } else { "HTTP response middleware failed before response commitment" }) - .build(); - openshell_ocsf::ocsf_emit!(event); + .build() } #[derive(Debug)] @@ -1627,11 +1690,18 @@ struct ResponseOutputState<'a> { #[derive(Debug)] struct ResponseMiddlewareStop { failure: openshell_supervisor_middleware::HttpResponseMiddlewareFailure, + diagnostics: openshell_supervisor_middleware::HttpResponseDiagnostics, } impl ResponseMiddlewareStop { - fn new(failure: openshell_supervisor_middleware::HttpResponseMiddlewareFailure) -> Self { - Self { failure } + fn new( + failure: openshell_supervisor_middleware::HttpResponseMiddlewareFailure, + diagnostics: openshell_supervisor_middleware::HttpResponseDiagnostics, + ) -> Self { + Self { + failure, + diagnostics, + } } } @@ -1676,13 +1746,15 @@ async fn expire_whole_body_deadline( guard.ensure_current()?; } let diagnostics = session.take_diagnostics(); - emit_http_response_diagnostics( - framing.policy_name, - framing.target, - framing.status_code, - &diagnostics, - ); - let output = output.map_err(ResponseMiddlewareStop::new)?; + if output.is_ok() { + emit_http_response_diagnostics( + framing.policy_name, + framing.target, + framing.status_code, + &diagnostics, + ); + } + let output = output.map_err(|failure| ResponseMiddlewareStop::new(failure, diagnostics))?; deliver_response_units(client, output, framing, session.requires_whole_body()).await } @@ -1818,13 +1890,15 @@ async fn process_response_unit( guard.ensure_current()?; } let diagnostics = session.take_diagnostics(); - emit_http_response_diagnostics( - framing.policy_name, - framing.target, - framing.status_code, - &diagnostics, - ); - let output = output.map_err(ResponseMiddlewareStop::new)?; + if output.is_ok() { + emit_http_response_diagnostics( + framing.policy_name, + framing.target, + framing.status_code, + &diagnostics, + ); + } + let output = output.map_err(|failure| ResponseMiddlewareStop::new(failure, diagnostics))?; deliver_response_units(client, output, framing, session.requires_whole_body()).await } @@ -2081,6 +2155,137 @@ pub(super) fn response_is_event_stream(headers: &str) -> bool { #[cfg(test)] mod tests { + use openshell_supervisor_middleware::{ + HttpResponseInvocation, HttpResponseInvocationOutcome as Outcome, NamespacedFinding, + }; + + fn invocation(outcome: Outcome, failed: bool) -> HttpResponseInvocation { + HttpResponseInvocation { + config_name: "guard".to_string(), + implementation: "test/guard".to_string(), + outcome, + sequence: None, + input_size: 0, + output_size: None, + failed, + stage_disabled: false, + reason_code: None, + failure_category: None, + } + } + + fn finding() -> NamespacedFinding { + NamespacedFinding { + middleware: "guard".to_string(), + finding: openshell_core::proto::Finding { + r#type: "test.finding".to_string(), + label: "Test finding".to_string(), + count: 1, + severity: "high".to_string(), + ..Default::default() + }, + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn response_terminal_failure_without_diagnostics_has_no_deny_trace() { + use crate::telemetry::tests::{assert_no_deny, exported}; + + // Platform refusals can lack invocation diagnostics, but must retain + // their terminal failure record. Without an HTTP record, no deny span. + for committed in [false, true] { + let (events, received) = exported("warn", || { + super::emit_http_response_middleware_failure( + "response-policy", + &super::HttpRequestTarget::default(), + 200, + committed, + ); + }) + .await; + + assert_eq!(events.len(), 1, "only the terminal failure is emitted"); + assert_eq!( + events[0]["finding_info"]["uid"], + "openshell.middleware.http_response_failure" + ); + assert_no_deny(&events, &received); + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn response_diagnostic_findings_share_a_deny_span_only_when_delivery_is_denied() { + use crate::telemetry::tests::{assert_deny_resolves, assert_no_deny, exported}; + + for denied in [false, true] { + let (events, received) = exported("warn", || { + let outcome = if denied { + Outcome::BlockDelivery + } else { + Outcome::PassThrough + }; + let diagnostics = openshell_supervisor_middleware::HttpResponseDiagnostics { + invocations: vec![invocation(outcome, false)], + findings: vec![finding()], + ..Default::default() + }; + super::emit_http_response_diagnostics( + "response-policy", + &super::HttpRequestTarget::default(), + 200, + &diagnostics, + ); + }) + .await; + // A blocking invocation adds its own block finding. + assert_eq!(events.len(), if denied { 3 } else { 2 }); + if denied { + let blocked = events + .iter() + .find(|event| { + event["finding_info"]["uid"] == "openshell.middleware.http_response_blocked" + }) + .expect("blocked delivery emits its refusal finding"); + assert_eq!(blocked["action_id"], 2); + assert_eq!(blocked["action"], "Denied"); + assert_eq!(received.spans.len(), 1); + assert_deny_resolves(&events, &received, true); + } else { + assert_no_deny(&events, &received); + } + } + } + + #[test] + fn fail_closed_invocations_and_enforced_failures_record_the_denial() { + let target = super::HttpRequestTarget::default(); + for (outcome, failed, action_id) in [ + (Outcome::BlockDelivery, false, 2), + (Outcome::FailClosed, true, 2), + (Outcome::FailOpen, true, 99), + (Outcome::PassThrough, false, 1), + ] { + let events = super::http_response_middleware_invocation_events( + "p", + &target, + 200, + &[invocation(outcome, failed)], + ); + assert_eq!( + events[0].to_json().unwrap()["action_id"], + action_id, + "{outcome:?}" + ); + } + for denied in [false, true] { + let failure = + super::http_response_middleware_failure_event("p", &target, 200, false, denied) + .to_json() + .unwrap(); + assert_eq!(failure["action_id"] == 2, denied); + } + } + use super::{ ResponseFraming, parse_connection_keep_alive, parse_response_head_for_middleware, response_is_http_10, serialize_response_head, diff --git a/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs b/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs index 65d7b70d08..f868575097 100644 --- a/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs +++ b/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs @@ -22,12 +22,13 @@ use openshell_core::token_grant_failure::{ }; use openshell_ocsf::{ ActionId, ActivityId, DispositionId, Endpoint, HttpActivityBuilder, HttpRequest, SeverityId, - StatusId, ctx::ctx as ocsf_ctx, ocsf_emit, + StatusId, ctx::ctx as ocsf_ctx, }; use tracing::warn; use crate::l7::provider::L7Request; use crate::l7::relay::L7EvalContext; +use crate::telemetry::egress_emit; /// Borrowed grant configuration for one endpoint-bound provider credential. pub struct TokenGrantRequest<'a> { @@ -340,7 +341,7 @@ fn report_refused_grant( .map_or_else(|| "none".to_string(), |status| status.to_string()), "Token grant failed" ); - ocsf_emit!( + egress_emit!( HttpActivityBuilder::new(ocsf_ctx()) .activity(ActivityId::Fail) .action(ActionId::Denied) @@ -398,7 +399,7 @@ async fn inject_selected( inject_header(&raw_header, &name, &value).map_err(TokenGrantInjectionError::new)?; } for (provider_key, _) in credentials { - ocsf_emit!( + egress_emit!( HttpActivityBuilder::new(ocsf_ctx()) .activity(ActivityId::Other) .action(ActionId::Allowed) diff --git a/crates/openshell-supervisor-network/src/l7/websocket.rs b/crates/openshell-supervisor-network/src/l7/websocket.rs index 772953e302..4d688929c6 100644 --- a/crates/openshell-supervisor-network/src/l7/websocket.rs +++ b/crates/openshell-supervisor-network/src/l7/websocket.rs @@ -9,13 +9,13 @@ use crate::l7::relay::{L7EvalContext, evaluate_l7_request}; use crate::l7::{EnforcementMode, L7RequestInfo}; use crate::opa::{PolicyGenerationGuard, TunnelPolicyEngine}; +use crate::telemetry::egress_emit; use flate2::{Compress, Compression, Decompress, FlushCompress, FlushDecompress, Status}; use miette::{IntoDiagnostic, Result, miette}; use openshell_core::provider_credentials::ProviderCredentialState; use openshell_core::secrets::{SecretResolver, contains_reserved_credential_marker}; use openshell_ocsf::{ ActionId, ActivityId, DispositionId, Endpoint, NetworkActivityBuilder, SeverityId, StatusId, - ocsf_emit, }; use std::collections::HashMap; use std::future::Future; @@ -39,6 +39,7 @@ const OPCODE_BINARY: u8 = 0x2; const OPCODE_CLOSE: u8 = 0x8; const OPCODE_PING: u8 = 0x9; const OPCODE_PONG: u8 = 0xA; +const CREDENTIAL_PLACEMENT_MISMATCH: &str = "websocket credential placement mismatch"; const CREDENTIAL_ENDPOINT_MISMATCH: &str = "websocket credential endpoint mismatch"; #[derive(Clone, Debug)] @@ -582,7 +583,27 @@ where } if let Err(termination) = &result { let error = termination.error.to_string(); - if error.contains(CREDENTIAL_ENDPOINT_MISMATCH) { + if error.contains(CREDENTIAL_PLACEMENT_MISMATCH) { + egress_emit!( + NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .activity(ActivityId::Fail) + .action(ActionId::Denied) + .disposition(DispositionId::Blocked) + .severity(SeverityId::High) + .status(StatusId::Failure) + .dst_endpoint(Endpoint::from_domain(host, port)) + .firewall_rule(options.policy_name, "credential-binding") + .message("WebSocket credential use denied: unauthorized request location") + .status_detail("credential_placement_mismatch") + .build() + ); + egress_emit!(crate::l7::build_credential_placement_mismatch_finding( + options.policy_name, + host, + Some("websocket"), + )); + let _ = write_policy_violation_close(&mut client_write).await; + } else if error.contains(CREDENTIAL_ENDPOINT_MISMATCH) { emit_credential_endpoint_mismatch(host, port, options.policy_name); let _ = write_policy_violation_close(&mut client_write).await; } else if error.contains("credential") { @@ -609,7 +630,7 @@ where } async fn write_policy_violation_close(writer: &mut W) -> Result<()> { - let reason = b"credential endpoint mismatch"; + let reason = b"credential use denied"; let mut frame = Vec::with_capacity(reason.len() + 4); frame.push(0x80 | OPCODE_CLOSE); frame.push(u8::try_from(reason.len() + 2).expect("close reason fits one-byte length")); @@ -620,7 +641,7 @@ async fn write_policy_violation_close(writer: &mut W) -> } fn emit_credential_endpoint_mismatch(host: &str, port: u16, policy_name: &str) { - ocsf_emit!( + egress_emit!( NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) .activity(ActivityId::Fail) .action(ActionId::Denied) @@ -635,7 +656,7 @@ fn emit_credential_endpoint_mismatch(host: &str, port: u16, policy_name: &str) { .status_detail("credential_endpoint_mismatch") .build() ); - ocsf_emit!(crate::l7::build_credential_endpoint_mismatch_finding( + egress_emit!(crate::l7::build_credential_endpoint_mismatch_finding( policy_name, host, Some("websocket"), @@ -1288,7 +1309,12 @@ where resolver .rewrite_websocket_text_placeholders(&mut text) .map_err(|error| { - if error.is_endpoint_mismatch() { + if error.is_placement_mismatch() { + terminate( + WebSocketTerminationCause::PolicyDenial, + miette!(CREDENTIAL_PLACEMENT_MISMATCH), + ) + } else if error.is_endpoint_mismatch() { terminate( WebSocketTerminationCause::PolicyDenial, miette!(CREDENTIAL_ENDPOINT_MISMATCH), @@ -1386,7 +1412,7 @@ fn emit_uninspected_credential_denial(host: &str, port: u16, policy_name: &str, "WebSocket credential traffic denied for {host}:{port}" )) .build(); - ocsf_emit!(event); + egress_emit!(event); crate::l7::emit_uninspected_credential_finding(host, policy_name, surface); } @@ -2061,7 +2087,7 @@ fn emit_rewrite_event(host: &str, port: u16, policy_name: &str, replacements: us .firewall_rule(policy_name, "l7-websocket") .message(rewrite_event_message(host, port, replacements)) .build(); - ocsf_emit!(event); + egress_emit!(event); } fn rewrite_event_message(host: &str, port: u16, replacements: usize) -> String { @@ -2114,7 +2140,7 @@ fn emit_websocket_l7_event( request_info.action, request_info.target, summary )) .build(); - ocsf_emit!(event); + egress_emit!(event); } fn observe_termination( @@ -2127,7 +2153,7 @@ fn observe_termination( emit_protocol_failure(host, port, policy_name, failure_class); } if termination.cause == WebSocketTerminationCause::CapacityExhausted { - ocsf_emit!( + egress_emit!( NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) .activity(ActivityId::Open) .action(ActionId::Denied) @@ -2151,7 +2177,7 @@ fn emit_protocol_failure( policy_name: &str, failure_class: FrameFailureClass, ) { - ocsf_emit!(protocol_failure_event( + egress_emit!(protocol_failure_event( host, port, policy_name, @@ -2713,7 +2739,7 @@ network_policies: 1, HashMap::from([("DISCORD_BOT_TOKEN".to_string(), "real-token".to_string())]), HashMap::new(), - HashMap::new(), + crate::test_support::header_placement_metadata(&["DISCORD_BOT_TOKEN"]), HashMap::from([( "DISCORD_BOT_TOKEN".to_string(), StaticCredentialBinding { @@ -2815,7 +2841,7 @@ network_policies: } #[tokio::test] - async fn guarded_websocket_uses_path_scoped_live_resolver() { + async fn guarded_websocket_rewrites_header_credentials_with_policy_opt_in() { let state = bound_websocket_provider_state(); let placeholder = b"openshell:resolve:env:v1_DISCORD_BOT_TOKEN"; let input = masked_frame(true, OPCODE_TEXT, placeholder); @@ -2847,7 +2873,68 @@ network_policies: .await; assert!( result.is_ok(), - "path-scoped live resolver must satisfy the credential guard: {result:?}" + "explicit WebSocket rewriting must permit header credentials: {result:?}" + ); + + drop(relay_write); + let mut output = Vec::new(); + upstream_read.read_to_end(&mut output).await.unwrap(); + assert_eq!(decode_masked_text_frame(&output), "real-token"); + } + + #[tokio::test] + async fn authorization_fallback_preserves_explicit_websocket_rewriting() { + let state = ProviderCredentialState::from_bound_environment( + 1, + HashMap::from([("DISCORD_BOT_TOKEN".to_string(), "real-token".to_string())]), + HashMap::new(), + HashMap::new(), + HashMap::from([( + "DISCORD_BOT_TOKEN".to_string(), + StaticCredentialBinding { + endpoints: vec![StaticCredentialEndpointBinding { + host: "gateway.example.test".into(), + port: 443, + path: "/socket".into(), + }], + credential_identity: "provider:DISCORD_BOT_TOKEN".into(), + workload_credential_handle: String::new(), + }, + )]), + Vec::new(), + ) + .expect("legacy websocket fixture"); + let placeholder = b"openshell:resolve:env:v1_DISCORD_BOT_TOKEN"; + let input = masked_frame(true, OPCODE_TEXT, placeholder); + let (mut client_write, mut relay_read) = tokio::io::duplex(4096); + let (mut relay_write, mut upstream_read) = tokio::io::duplex(4096); + client_write.write_all(&input).await.unwrap(); + drop(client_write); + + let mut options = RelayOptions { + policy_name: "test-policy", + assembly_budget: WebSocketAssemblyBudget::default(), + resolver: None, + generation_guard: None, + provider_credentials: Some(&state), + target: "/socket", + inspector: None, + compression: WebSocketCompression::None, + middleware_session: None, + middleware_context: None, + deny_uninspected_credentials: true, + }; + let result = relay_client_to_server( + &mut relay_read, + &mut relay_write, + "gateway.example.test", + 443, + &mut options, + ) + .await; + assert!( + result.is_ok(), + "Authorization fallback must preserve explicit WebSocket rewriting: {result:?}" ); drop(relay_write); @@ -2914,7 +3001,7 @@ network_policies: result .as_ref() .is_err_and(|error| error.error.to_string().contains("generation changed")), - "revoked credential generation must fail before the frame write: {result:?}" + "revoked generation must fail before the frame write: {result:?}" ); drop(relay_write); diff --git a/crates/openshell-supervisor-network/src/lib.rs b/crates/openshell-supervisor-network/src/lib.rs index d9fcd004b6..74a6698b92 100644 --- a/crates/openshell-supervisor-network/src/lib.rs +++ b/crates/openshell-supervisor-network/src/lib.rs @@ -24,6 +24,7 @@ pub mod proxy; pub mod run; pub mod sigv4; mod spiffe_endpoint; +mod telemetry; mod token_grant; pub mod upstream_proxy; diff --git a/crates/openshell-supervisor-network/src/opa.rs b/crates/openshell-supervisor-network/src/opa.rs index 2d1a73a949..257bd3ce2e 100644 --- a/crates/openshell-supervisor-network/src/opa.rs +++ b/crates/openshell-supervisor-network/src/opa.rs @@ -145,7 +145,7 @@ fn emit_binary_identity_mode(require_binary_identity: bool, source: &str) { require_binary_identity, source, "Configured OPA runtime binary identity mode" ); - openshell_ocsf::ocsf_emit!( + crate::telemetry::egress_emit!( openshell_ocsf::ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx()) .severity(openshell_ocsf::SeverityId::Informational) .status(openshell_ocsf::StatusId::Success) @@ -1427,7 +1427,7 @@ fn parse_process_policy(val: ®orus::Value) -> ProcessPolicy { fn emit_l7_config_warnings(warnings: &[String], prefix: &str) { for w in warnings { - openshell_ocsf::ocsf_emit!( + crate::telemetry::egress_emit!( openshell_ocsf::ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx()) .severity(openshell_ocsf::SeverityId::Medium) .status(openshell_ocsf::StatusId::Success) @@ -4566,6 +4566,10 @@ process: let mut policy = valid.clone(); policy["version"] = 1.into(); policy["network_policies"]["admin"]["endpoints"][0][field] = control; + if field == "websocket_credential_rewrite" { + policy["network_policies"]["admin"]["endpoints"][0]["protocol"] = + "websocket".into(); + } openshell_policy::parse_sandbox_policy(&policy.to_string()).expect("typed valid twin"); OpaEngine::from_strings(TEST_POLICY, &policy.to_string()).expect("raw valid twin"); policy["network_policies"]["admin"]["endpoints"][0][field] = invalid; @@ -8277,7 +8281,7 @@ network_policies: endpoints: vec![NetworkEndpoint { host: "gateway.example.com".to_string(), port: 443, - protocol: "rest".to_string(), + protocol: "websocket".to_string(), enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32, access: openshell_core::proto::NetworkAccessPreset::Full as i32, websocket_credential_rewrite: true, diff --git a/crates/openshell-supervisor-network/src/policy_dns/mod.rs b/crates/openshell-supervisor-network/src/policy_dns/mod.rs index 5ddbd25cf4..4d5b61895d 100644 --- a/crates/openshell-supervisor-network/src/policy_dns/mod.rs +++ b/crates/openshell-supervisor-network/src/policy_dns/mod.rs @@ -35,10 +35,11 @@ pub(crate) use store::{ use crate::opa::OpaEngine; use crate::proxy::destination::{build_validation_plan, filter_resolved_addresses}; use crate::proxy::is_host_gateway_alias; +use crate::telemetry::egress_emit; use openshell_core::host_pattern::HostSelector; use openshell_ocsf::{ ActionId, ActivityId, ConfigStateChangeBuilder, DispositionId, Endpoint, - NetworkActivityBuilder, SeverityId, StateId, StatusId, ocsf_emit, + NetworkActivityBuilder, SeverityId, StateId, StatusId, }; use sha2::{Digest, Sha256}; use std::sync::Arc; @@ -493,7 +494,7 @@ fn dns_query_endpoint(name: &NormalizedName) -> Endpoint { } fn emit_dns_denial(name: &NormalizedName, detail: &str, message: &str) { - ocsf_emit!(build_dns_denial_event(name, detail, message)); + egress_emit!(build_dns_denial_event(name, detail, message)); } fn build_dns_denial_event( @@ -580,7 +581,7 @@ fn emit_dns_failure( detail: &str, message: &str, ) { - ocsf_emit!(build_dns_failure_event( + egress_emit!(build_dns_failure_event( name, family, eligible_endpoints, @@ -591,11 +592,11 @@ fn emit_dns_failure( } fn emit_mapping_publication(record: &ResolvedEndpointRecord) { - ocsf_emit!(build_mapping_publication_event(record)); + egress_emit!(build_mapping_publication_event(record)); } fn emit_observation_publication(record: &ResolvedEndpointRecord) { - ocsf_emit!(build_observation_publication_event(record)); + egress_emit!(build_observation_publication_event(record)); } fn build_observation_publication_event( diff --git a/crates/openshell-supervisor-network/src/policy_dns/runtime.rs b/crates/openshell-supervisor-network/src/policy_dns/runtime.rs index cf1f61cfca..d7888f991e 100644 --- a/crates/openshell-supervisor-network/src/policy_dns/runtime.rs +++ b/crates/openshell-supervisor-network/src/policy_dns/runtime.rs @@ -14,7 +14,7 @@ use futures::{FutureExt as _, StreamExt as _, stream::FuturesUnordered}; use miette::{IntoDiagnostic, Result, WrapErr}; use openshell_core::net::set_tcp_nodelay_best_effort; use openshell_isolation_interface::contract::{DnsTransport, PendingDnsQuery}; -use openshell_ocsf::{ConfigStateChangeBuilder, SeverityId, StateId, StatusId, ocsf_emit}; +use openshell_ocsf::{ConfigStateChangeBuilder, SeverityId, StateId, StatusId}; use openshell_sandbox_backend::NetworkMediationSource; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::sync::Arc; @@ -169,7 +169,7 @@ impl PolicyDnsRuntime { let _ = expiry_store.expire(std::time::Instant::now()); } }); - ocsf_emit!( + crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx()) .severity(SeverityId::Informational) .status(StatusId::Success) @@ -284,7 +284,7 @@ impl PolicyDnsRuntime { } }); - ocsf_emit!( + crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx()) .severity(SeverityId::Informational) .status(StatusId::Success) diff --git a/crates/openshell-supervisor-network/src/policy_dns/store.rs b/crates/openshell-supervisor-network/src/policy_dns/store.rs index 9c76e89c5e..62fe1c19bf 100644 --- a/crates/openshell-supervisor-network/src/policy_dns/store.rs +++ b/crates/openshell-supervisor-network/src/policy_dns/store.rs @@ -433,7 +433,7 @@ impl ResolvedEndpointStore { if allocated.saturating_mul(5) >= capacity.saturating_mul(4) && !emitted.swap(true, Ordering::Relaxed) { - openshell_ocsf::ocsf_emit!(build_pool_high_water_event( + crate::telemetry::egress_emit!(build_pool_high_water_event( allocated, capacity, family, )); } diff --git a/crates/openshell-supervisor-network/src/policy_local.rs b/crates/openshell-supervisor-network/src/policy_local.rs index 033f492aa7..bdf8898986 100644 --- a/crates/openshell-supervisor-network/src/policy_local.rs +++ b/crates/openshell-supervisor-network/src/policy_local.rs @@ -12,7 +12,7 @@ use openshell_core::proto::{ SandboxPolicy as ProtoSandboxPolicy, }; use openshell_ocsf::{ - ConfigStateChangeBuilder, SeverityId, StateId, StatusId, ocsf_emit, sanitize_reason_for_audit, + ConfigStateChangeBuilder, SeverityId, StateId, StatusId, sanitize_reason_for_audit, }; use serde::Deserialize; use std::collections::HashMap; @@ -721,7 +721,7 @@ async fn submit_proposal(ctx: &PolicyLocalContext, body: &[u8]) -> (u16, serde_j /// and the endpoint the agent is asking to reach — what a developer needs /// to see in the audit trace to correlate against the inbox card. fn emit_policy_propose_event(chunk_id: &str, summary: &str) { - ocsf_emit!( + crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx()) .severity(SeverityId::Informational) .status(StatusId::Success) @@ -741,7 +741,7 @@ fn emit_policy_propose_event(chunk_id: &str, summary: &str) { fn emit_policy_decision_event(chunk: &PolicyChunk) { let summary = summarize_chunk_for_audit(chunk); match chunk.status.as_str() { - "approved" => ocsf_emit!( + "approved" => crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx()) .severity(SeverityId::Informational) .status(StatusId::Success) @@ -763,7 +763,7 @@ fn emit_policy_decision_event(chunk: &PolicyChunk) { } else { format!("\"{sanitized}\"") }; - ocsf_emit!( + crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx()) .severity(SeverityId::Low) .status(StatusId::Success) diff --git a/crates/openshell-supervisor-network/src/proxy.rs b/crates/openshell-supervisor-network/src/proxy.rs index 6f64d91ee2..96c0b8f50a 100644 --- a/crates/openshell-supervisor-network/src/proxy.rs +++ b/crates/openshell-supervisor-network/src/proxy.rs @@ -8,6 +8,8 @@ mod egress; mod relay; #[cfg(test)] pub(crate) use relay::relay_inspected_http_stream_for_test; +#[cfg(test)] +mod trace_tests; #[cfg(test)] use openshell_sandbox_backend::test_support as rpc_test; @@ -19,6 +21,7 @@ use crate::policy_dns::{ MappingLookup, MappingLookupError, PolicyEndpointId, ResolvedEndpointStore, }; use crate::policy_local::{POLICY_LOCAL_HOST, PolicyLocalContext}; +use crate::telemetry::egress_emit; use crate::upstream_proxy::{self, UpstreamProxyConfig}; use futures::{FutureExt as _, StreamExt as _, stream::FuturesUnordered}; use http::StatusCode; @@ -44,7 +47,7 @@ use openshell_isolation_interface::contract::{ use openshell_ocsf::{ ActionId, ActivityId, BaseEventBuilder, DispositionId, Endpoint, HttpActivityBuilder, HttpRequest, HttpResponse, NetworkActivityBuilder, Process, SeverityId, StatusId, - Url as OcsfUrl, ocsf_emit, + Url as OcsfUrl, }; use openshell_sandbox_backend::NetworkMediationSource; use std::future::Future; @@ -244,16 +247,21 @@ fn build_credential_endpoint_mismatch_event( .build() } -fn emit_credential_endpoint_mismatch(method: &str, host: &str, port: u16, policy_name: &str) { +fn emit_credential_endpoint_mismatch( + method: &str, + host: &str, + port: u16, + policy_name: &str, + connect_span: &tracing::Span, +) { let event = build_credential_endpoint_mismatch_event(method, host, port, policy_name); - ocsf_emit!(event); let finding = crate::l7::build_credential_endpoint_mismatch_finding( policy_name, host, None, "Provider credential endpoint binding mismatch; request denied", ); - ocsf_emit!(finding); + crate::telemetry::emit_decision([event, finding], Some(connect_span)); } /// Hostnames injected by compute drivers as `/etc/hosts` aliases for the host @@ -349,7 +357,7 @@ impl ProxyHandle { format!("Proxy listening on {local_addr}") }) .build(); - ocsf_emit!(event); + egress_emit!(event); } // Detect the trusted host gateway IP from /etc/hosts before user code @@ -390,7 +398,7 @@ impl ProxyHandle { refusing to start: {err}" )) .build(); - ocsf_emit!(event); + egress_emit!(event); miette::miette!("invalid upstream corporate proxy configuration: {err}") })?, ); @@ -404,7 +412,7 @@ impl ProxyHandle { cfg.summary() )) .build(); - ocsf_emit!(event); + egress_emit!(event); } let (exited_tx, exited_rx) = tokio::sync::oneshot::channel::<()>(); @@ -574,7 +582,7 @@ impl ProxyHandle { ) .await { - ocsf_emit!(build_proxy_connection_error_event( + egress_emit!(build_proxy_connection_error_event( peer_addr, transparent_destination_addr, format!("Proxy connection error: {err}"), @@ -583,7 +591,7 @@ impl ProxyHandle { }); } Err(ProxyAcceptError::Source(err)) => { - ocsf_emit!(build_mediation_lane_failure_event(format!( + egress_emit!(build_mediation_lane_failure_event(format!( "Network-mediation source failed; proxy accept loop exiting: {err}" ))); break; @@ -594,7 +602,7 @@ impl ProxyHandle { &mut consecutive_resource_errors, &mut consecutive_unknown_errors, ); - ocsf_emit!(build_accept_error_event(local_addr, &err, &action)); + egress_emit!(build_accept_error_event(local_addr, &err, &action)); match action { AcceptAction::Terminal => break, AcceptAction::Retry { backoff, .. } => { @@ -946,7 +954,7 @@ fn emit_staged_transparent_denial( reason: &str, status_detail: &'static str, ) { - ocsf_emit!(build_staged_transparent_denial_event( + egress_emit!(build_staged_transparent_denial_event( destination, mapped_host, identity, @@ -1180,7 +1188,7 @@ impl TransparentTcpHandle { ) .await { - ocsf_emit!(build_connection_error_event( + egress_emit!(build_connection_error_event( peer_addr, format!("Transparent TCP connection error: {error}") )); @@ -1416,7 +1424,7 @@ async fn handle_transparent_tcp_connection( let pid = decision .binary_pid .map_or_else(|| "-".to_string(), |pid| pid.to_string()); - ocsf_emit!(build_transparent_tcp_allow_ocsf_event( + egress_emit!(build_transparent_tcp_allow_ocsf_event( TransparentTcpAllowAudit { workload: workload_addr, synthetic_destination: original, @@ -1560,7 +1568,7 @@ fn emit_transparent_mapping_denial( | MappingLookupError::InvalidMapping | MappingLookupError::LockPoisoned => "transparent_tcp_destination_denied", }; - ocsf_emit!( + egress_emit!( NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) .activity(ActivityId::Open) .action(ActionId::Denied) @@ -1594,7 +1602,7 @@ fn emit_transparent_policy_denial( let pid = decision .binary_pid .map_or_else(|| "-".to_string(), |pid| pid.to_string()); - ocsf_emit!( + egress_emit!( NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) .activity(ActivityId::Open) .action(ActionId::Denied) @@ -2066,6 +2074,61 @@ fn build_forward_allow_ocsf_event( .build() } +fn build_forward_rejection_event( + host: &str, + port: u16, + message: String, +) -> openshell_ocsf::OcsfEvent { + NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .activity(ActivityId::Fail) + .action(ActionId::Denied) + .disposition(DispositionId::Blocked) + .severity(SeverityId::Medium) + .status(StatusId::Failure) + .dst_endpoint(Endpoint::from_domain(host, port)) + .message(message) + .build() +} + +fn build_forward_l7_evaluation_error( + host: &str, + port: u16, + enforcement: crate::l7::EnforcementMode, + error: &str, +) -> openshell_ocsf::OcsfEvent { + let enforced = enforcement == crate::l7::EnforcementMode::Enforce; + NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .activity(ActivityId::Fail) + .action(if enforced { + ActionId::Denied + } else { + ActionId::Other + }) + .disposition(if enforced { + DispositionId::Blocked + } else { + DispositionId::Allowed + }) + .severity(SeverityId::Low) + .status(StatusId::Failure) + .dst_endpoint(Endpoint::from_domain(host, port)) + .message(format!("L7 evaluation failed: {error}")) + .build() +} + +fn emit_forward_l7_decision( + decision: openshell_ocsf::OcsfEvent, + evaluation_error: Option, + connect_span: &tracing::Span, +) { + // The HTTP decision carries policy context that its evaluation-error + // companion lacks, so it must supply the shared deny span's attributes. + crate::telemetry::emit_decision( + std::iter::once(decision).chain(evaluation_error), + Some(connect_span), + ); +} + fn build_forward_parse_error_ocsf_event( peer_addr: Option, method: &str, @@ -2080,6 +2143,8 @@ fn build_forward_parse_error_ocsf_event( .http_response(HttpResponse { code: StatusCode::BAD_REQUEST.as_u16(), }) + .action(ActionId::Denied) + .disposition(DispositionId::Blocked) .severity(SeverityId::Low) .status(StatusId::Failure) .message(format!("FORWARD parse error for {path}")); @@ -2324,7 +2389,7 @@ where C: TokioAsyncWrite + Unpin, { let detail = destination_denial_detail(denial.kind); - ocsf_emit!(build_connect_destination_deny_ocsf_event( + egress_emit!(build_connect_destination_deny_ocsf_event( denial, peer_addr, host, port, binary, pid, ancestors, cmdline, )); @@ -2357,6 +2422,7 @@ where #[allow(clippy::too_many_arguments)] async fn deny_forward_destination( client: &mut C, + connect_span: &tracing::Span, denial: &DestinationDenial, peer_addr: SocketAddr, method: &str, @@ -2376,9 +2442,12 @@ where C: TokioAsyncWrite + Unpin, { let detail = destination_denial_detail(denial.kind); - ocsf_emit!(build_forward_destination_deny_ocsf_event( - denial, peer_addr, method, host, port, path, binary, pid, ancestors, cmdline, policy, - )); + crate::telemetry::emit_decision( + [build_forward_destination_deny_ocsf_event( + denial, peer_addr, method, host, port, path, binary, pid, ancestors, cmdline, policy, + )], + Some(connect_span), + ); emit_denial_simple( denial_tx, @@ -2835,7 +2904,7 @@ async fn handle_mediated_connection( .message(format!("CONNECT denied {host_lc}:{port}")) .status_detail(&deny_reason) .build(); - ocsf_emit!(event); + egress_emit!(event); emit_denial( &denial_tx, &host_lc, @@ -3000,7 +3069,7 @@ async fn handle_mediated_connection( )) .status_detail(TLS_TERMINATION_UNAVAILABLE_DETAIL) .build(); - ocsf_emit!(event); + egress_emit!(event); emit_activity_simple(activity_tx.as_ref(), true, "tls_termination_unavailable"); emit_denial( &denial_tx, @@ -3038,7 +3107,7 @@ async fn handle_mediated_connection( )) .status_detail(DETAIL) .build(); - ocsf_emit!(event); + egress_emit!(event); crate::l7::emit_uninspected_credential_finding( &host_lc, policy_str, @@ -3120,7 +3189,7 @@ async fn handle_mediated_connection( // Log the allowed CONNECT — use CONNECT_L7 when L7 inspection follows, // so log consumers can distinguish L4-only decisions from tunnel lifecycle events. - ocsf_emit!(build_connect_allow_ocsf_event( + egress_emit!(build_connect_allow_ocsf_event( workload_addr, &host_lc, port, @@ -3231,7 +3300,7 @@ async fn handle_mediated_connection( .dst_endpoint(Endpoint::from_domain(&host_lc, port)) .message("Upstream TLS establishment failed") .build(); - ocsf_emit!(event); + egress_emit!(event); return Err(error); } }; @@ -3260,7 +3329,7 @@ async fn handle_mediated_connection( .dst_endpoint(Endpoint::from_domain(&host_lc, port)) .message(format!("TLS relay error: {e}")) .build(); - ocsf_emit!(event); + egress_emit!(event); } } } else { @@ -3292,7 +3361,7 @@ async fn handle_mediated_connection( .message(format!("CONNECT refused for {host_lc}:{port}: {DETAIL}")) .status_detail(DETAIL) .build(); - ocsf_emit!(event); + egress_emit!(event); emit_activity_simple(activity_tx.as_ref(), true, "tls_termination_unavailable"); emit_denial( &denial_tx, @@ -3335,7 +3404,7 @@ async fn handle_mediated_connection( .dst_endpoint(Endpoint::from_domain(&host_lc, port)) .message(message) .build(); - ocsf_emit!(event); + egress_emit!(event); } } } else { @@ -3365,7 +3434,7 @@ async fn handle_mediated_connection( )) .status_detail(protocol_detail) .build(); - ocsf_emit!(event); + egress_emit!(event); emit_activity_simple(activity_tx.as_ref(), true, "l7_parse_rejection"); emit_denial( &denial_tx, @@ -3966,7 +4035,7 @@ fn emit_l7_tunnel_close_after_policy_change(host: &str, port: u16, error: miette "L7 tunnel closed before inspection because policy changed: {error}" )) .build(); - ocsf_emit!(event); + egress_emit!(event); } async fn reject_stale_connect_policy( @@ -4808,7 +4877,7 @@ fn parse_allowed_ips(raw: &[String]) -> std::result::Result, S n.prefix_len() )) .build(); - ocsf_emit!(event); + egress_emit!(event); } nets.push(n); } @@ -5482,7 +5551,7 @@ async fn handle_forward_proxy( // canonicalized below before credential binding, policy-path evaluation, // upstream bytes, or telemetry consume it. let Ok((scheme, host, port, mut path)) = parse_proxy_uri(target_uri) else { - ocsf_emit!(build_forward_parse_error_ocsf_event( + egress_emit!(build_forward_parse_error_ocsf_event( workload_peer_addr, method, &telemetry_path @@ -5551,7 +5620,7 @@ async fn handle_forward_proxy( if scheme != "http" { let event = build_forward_unsupported_scheme_ocsf_event(method, &scheme, &host_lc, port); - ocsf_emit!(event); + egress_emit!(event); if scheme == "https" { respond( client, @@ -5643,18 +5712,21 @@ async fn handle_forward_proxy( let matched_policy = match &decision.action { NetworkAction::Allow { matched_policy } => matched_policy.clone(), NetworkAction::Deny { reason } => { - ocsf_emit!(build_forward_policy_deny_ocsf_event( - workload_addr, - method, - &host_lc, - port, - &telemetry_path, - &binary_str, - &pid_str, - &ancestors_str, - &cmdline_str, - reason, - )); + crate::telemetry::emit_decision( + [build_forward_policy_deny_ocsf_event( + workload_addr, + method, + &host_lc, + port, + &telemetry_path, + &binary_str, + &pid_str, + &ancestors_str, + &cmdline_str, + reason, + )], + Some(&connect_span), + ); emit_denial_simple( denial_tx, &host_lc, @@ -5754,16 +5826,12 @@ async fn handle_forward_proxy( let prepared_target = match prepare_forward_target(&path, canonicalize_options) { Ok(prepared) => prepared, Err(error) => { - let event = NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) - .activity(ActivityId::Fail) - .severity(SeverityId::Medium) - .status(StatusId::Failure) - .dst_endpoint(Endpoint::from_domain(&host_lc, port)) - .message(format!( - "FORWARD rejecting non-canonical request-target: {error}" - )) - .build(); - ocsf_emit!(event); + let event = build_forward_rejection_event( + &host_lc, + port, + format!("FORWARD rejecting non-canonical request-target: {error}"), + ); + egress_emit!(event); emit_activity_simple(activity_tx, true, "forward_parse_rejection"); respond( client, @@ -5916,19 +5984,22 @@ async fn handle_forward_proxy( if !l7_config.config.allow_encoded_slash && crate::l7::path::canonical_path_has_encoded_slash(&path) { - ocsf_emit!(build_forward_l7_parse_rejection_ocsf_event( - workload_addr, - method, - &host_lc, - port, - &telemetry_path, - &binary_str, - &pid_str, - &ancestors_str, - &cmdline_str, - policy_str, - FORWARD_ENCODED_SLASH_REJECTION_DETAIL, - )); + crate::telemetry::emit_decision( + [build_forward_l7_parse_rejection_ocsf_event( + workload_addr, + method, + &host_lc, + port, + &telemetry_path, + &binary_str, + &pid_str, + &ancestors_str, + &cmdline_str, + policy_str, + FORWARD_ENCODED_SLASH_REJECTION_DETAIL, + )], + Some(&connect_span), + ); emit_activity_simple(activity_tx, true, "forward_parse_rejection"); respond( client, @@ -5973,7 +6044,7 @@ async fn handle_forward_proxy( )) .status_detail(upgrade_detail) .build(); - ocsf_emit!(event); + crate::telemetry::emit_decision([event], Some(&connect_span)); emit_activity_simple(activity_tx, true, "l7_parse_rejection"); emit_denial_simple( denial_tx, @@ -6031,14 +6102,12 @@ async fn handle_forward_proxy( { Ok(info) => info, Err(e) => { - let event = NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) - .activity(ActivityId::Fail) - .severity(SeverityId::Medium) - .status(StatusId::Failure) - .dst_endpoint(Endpoint::from_domain(&host_lc, port)) - .message(format!("FORWARD_GRAPHQL_L7 request rejected: {e}")) - .build(); - ocsf_emit!(event); + let event = build_forward_rejection_event( + &host_lc, + port, + format!("FORWARD_GRAPHQL_L7 request rejected: {e}"), + ); + egress_emit!(event); emit_activity_simple(activity_tx, true, "l7_parse_rejection"); respond( client, @@ -6085,14 +6154,12 @@ async fn handle_forward_proxy( { Ok(body) => body, Err(e) => { - let event = NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) - .activity(ActivityId::Fail) - .severity(SeverityId::Medium) - .status(StatusId::Failure) - .dst_endpoint(Endpoint::from_domain(&host_lc, port)) - .message(format!("FORWARD_JSONRPC_L7 request rejected: {e}")) - .build(); - ocsf_emit!(event); + let event = build_forward_rejection_event( + &host_lc, + port, + format!("FORWARD_JSONRPC_L7 request rejected: {e}"), + ); + egress_emit!(event); emit_activity_simple(activity_tx, true, "l7_parse_rejection"); respond( client, @@ -6143,18 +6210,17 @@ async fn handle_forward_proxy( let hard_deny_reason = crate::l7::relay::l7_request_hard_deny_reason(l7_config.config.protocol, &request_info); let force_deny = hard_deny_reason.is_some(); + let mut evaluation_error = None; let (allowed, reason) = hard_deny_reason.map_or_else( || { crate::l7::relay::evaluate_l7_request(&tunnel_engine, &l7_ctx, &request_info) .unwrap_or_else(|e| { - let event = NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) - .activity(ActivityId::Fail) - .severity(SeverityId::Low) - .status(StatusId::Failure) - .dst_endpoint(Endpoint::from_domain(&host_lc, port)) - .message(format!("L7 eval failed, denying request: {e}")) - .build(); - ocsf_emit!(event); + evaluation_error = Some(build_forward_l7_evaluation_error( + &host_lc, + port, + l7_config.config.enforcement, + &e.to_string(), + )); (false, format!("L7 evaluation error: {e}")) }) }, @@ -6230,7 +6296,7 @@ async fn handle_forward_proxy( .firewall_rule(policy_str, engine_type) .message(log_message) .build(); - ocsf_emit!(event); + emit_forward_l7_decision(event, evaluation_error, &connect_span); } let effectively_denied = force_deny @@ -6281,6 +6347,7 @@ async fn handle_forward_proxy( Err(denial) => { deny_forward_destination( client, + &connect_span, &denial, workload_addr, method, @@ -6319,6 +6386,7 @@ async fn handle_forward_proxy( Err(denial) => { deny_forward_destination( client, + &connect_span, &denial, workload_addr, method, @@ -6428,7 +6496,12 @@ async fn handle_forward_proxy( exchange: &middleware_exchange, l7_reevaluation, }; - forward_request_bytes = match pipeline.apply(request, client).await? { + // Middleware denials join the connect trace, like other FORWARD denials. + forward_request_bytes = match pipeline + .apply(request, client) + .instrument(connect_span.clone()) + .await? + { crate::l7::middleware::MiddlewareApplyResult::Allowed(request) => request.raw_header, crate::l7::middleware::MiddlewareApplyResult::Denied { denial, .. } => { emit_activity_simple(activity_tx, true, "middleware"); @@ -6608,11 +6681,22 @@ async fn handle_forward_proxy( .end(openshell_core::proto::MiddlewareSessionEndReason::Cancellation) .await; } - if e.is_endpoint_mismatch() { + if e.is_placement_mismatch() { + crate::l7::relay::reject_credential_resolution(client, &l7_ctx, method, &e) + .instrument(connect_span.clone()) + .await?; + client.shutdown().await.into_diagnostic()?; + } else if e.is_endpoint_mismatch() { if let Some(observer) = endpoint_observer.as_ref() { observer.observe_credential_failure(true); } - emit_credential_endpoint_mismatch(method, &host_lc, port, policy_str); + emit_credential_endpoint_mismatch( + method, + &host_lc, + port, + policy_str, + &connect_span, + ); respond( client, &build_json_error_response( @@ -6739,7 +6823,7 @@ async fn handle_forward_proxy( "FORWARD upstream connect failed for {host_lc}:{port}: {e}" )) .build(); - ocsf_emit!(event); + egress_emit!(event); if let Some(session) = middleware_session.take() { session .end(openshell_core::proto::MiddlewareSessionEndReason::UpstreamFailure) @@ -6869,7 +6953,7 @@ async fn handle_forward_proxy( // The request has now survived middleware, token grant, credential // rewriting, generation checks, and the HTTP relay. Only now record the // final allowed outcome. - ocsf_emit!(build_forward_allow_ocsf_event( + egress_emit!(build_forward_allow_ocsf_event( workload_addr, method, &host_lc, @@ -13633,6 +13717,9 @@ network_policies: credentials .resolver .expect("endpoint resolver") + .with_request_location(secrets::RequestCredentialLocation::Header( + "Authorization".into() + )) .resolve_placeholder("openshell:resolve:env:v42_API_TOKEN"), Some("secret") ); diff --git a/crates/openshell-supervisor-network/src/proxy/tests/compatibility.rs b/crates/openshell-supervisor-network/src/proxy/tests/compatibility.rs index 010cb466c5..3623b1dbb7 100644 --- a/crates/openshell-supervisor-network/src/proxy/tests/compatibility.rs +++ b/crates/openshell-supervisor-network/src/proxy/tests/compatibility.rs @@ -116,6 +116,7 @@ async fn destination_denials_preserve_adapter_specific_wire_contracts() { let (mut app, mut proxy) = tcp_pair().await; deny_forward_destination( &mut proxy, + &tracing::Span::none(), &denial, peer, "POST", diff --git a/crates/openshell-supervisor-network/src/proxy/trace_tests.rs b/crates/openshell-supervisor-network/src/proxy/trace_tests.rs new file mode 100644 index 0000000000..1602913885 --- /dev/null +++ b/crates/openshell-supervisor-network/src/proxy/trace_tests.rs @@ -0,0 +1,246 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use super::*; +use crate::telemetry::tests::{ + assert_deny_resolves, assert_no_deny, deny_spans, exported, span_attribute, +}; +use opentelemetry_proto::tonic::common::v1::any_value::Value; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn paired_credential_denial_shares_one_deny_span() { + let (events, received) = exported("warn", || { + emit_credential_endpoint_mismatch( + "POST", + "blocked.example", + 443, + "credential-policy", + &tracing::Span::none(), + ); + }) + .await; + assert_eq!(events.len(), 2); + assert_eq!(received.spans.len(), 1); + assert_deny_resolves(&events, &received, true); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn repeated_credential_denials_keep_decision_spans_separate() { + for level in ["warn", "debug"] { + let (events, received) = exported(level, || { + let connect = + egress::connect_span(&EgressIntent::connect("blocked.example".to_string(), 443)); + // The FORWARD path passes its saved connect span without entering it. + for _ in 0..2 { + emit_credential_endpoint_mismatch( + "POST", + "blocked.example", + 443, + "credential-policy", + &connect, + ); + } + let allowed = NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .action(ActionId::Allowed) + .dst_endpoint(Endpoint::from_domain("allowed.example", 443)) + .build(); + egress_emit!(allowed); + }) + .await; + + assert_eq!(events.len(), 5); + assert_deny_resolves(&events[..4], &received, level == "warn"); + assert!(events[4].get("trace").is_none(), "deny context must exit"); + let denies = deny_spans(&received); + assert_eq!(denies.len(), 2); + assert_ne!(denies[0].span_id, denies[1].span_id); + assert_eq!(received.spans.len(), if level == "debug" { 3 } else { 2 }); + if level == "debug" { + let connect = received + .spans + .iter() + .find(|span| span.name == "supervisor.egress.connect") + .unwrap(); + for deny in denies { + assert_eq!(deny.trace_id, connect.trace_id); + assert_eq!(deny.parent_span_id, connect.span_id); + } + assert_eq!(events[0]["trace"]["uid"], events[2]["trace"]["uid"]); + } else { + assert_ne!(events[0]["trace"]["uid"], events[2]["trace"]["uid"]); + } + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn network_class_forward_rejections_have_no_deny_trace() { + let (events, received) = exported("warn", || { + let Err(error) = + prepare_forward_target("/%zz", crate::l7::path::CanonicalizeOptions::default()) + else { + panic!("invalid escape must be rejected"); + }; + egress_emit!(build_forward_rejection_event( + "blocked.example", + 443, + format!("FORWARD rejecting non-canonical request-target: {error}") + )); + }) + .await; + assert_eq!(events.len(), 1); + assert_eq!(events[0]["action_id"], 2); + assert_no_deny(&events, &received); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn forward_denials_use_the_saved_connect_parent() { + for level in ["warn", "debug"] { + let (events, received) = crate::telemetry::tests::exported_over_otlp(level, || { + let intent = EgressIntent::connect("blocked.example".to_string(), 443); + let connect = egress::connect_span(&intent); + let decision = EgressDecision { + intent, + action: NetworkAction::Allow { + matched_policy: None, + }, + policy_generation: 1, + identity: ProcessIdentityEvidence::Available, + endpoint: EndpointDecision::default(), + binary: None, + binary_pid: None, + ancestors: vec![], + cmdline_paths: vec![], + }; + let denial = DestinationDenial { + kind: DestinationDenialKind::InternalAddress, + reason: "private destination".to_string(), + }; + let mut output = Vec::new(); + futures::executor::block_on(deny_forward_destination( + &mut output, + &connect, + &denial, + "127.0.0.1:41000".parse().unwrap(), + "POST", + "blocked.example", + 443, + "/", + "curl", + "42", + "-", + "curl", + "forward-policy", + &decision, + None, + None, + )) + .unwrap(); + assert!(output.starts_with(b"HTTP/1.1 403 Forbidden")); + }) + .await; + assert_eq!(events.len(), 1); + assert_deny_resolves(&events, &received, level == "warn"); + let denies = deny_spans(&received); + assert_eq!(denies.len(), 1); + if level == "debug" { + let connect = received + .spans + .iter() + .find(|span| span.name == "supervisor.egress.connect") + .unwrap(); + let deny = denies[0]; + assert_eq!(deny.parent_span_id, connect.span_id); + assert_eq!(events[0]["trace"]["uid"], hex::encode(&connect.trace_id)); + } + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn forward_evaluation_error_keeps_the_http_decision_policy_context() { + for level in ["warn", "debug"] { + for mode in [ + crate::l7::EnforcementMode::Audit, + crate::l7::EnforcementMode::Enforce, + ] { + let enforced = mode == crate::l7::EnforcementMode::Enforce; + let (events, received) = exported(level, || { + let connect = egress::connect_span(&EgressIntent::forward_http( + "blocked.example".to_string(), + 443, + )); + let decision = HttpActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .activity(ActivityId::Other) + .action(if enforced { + ActionId::Denied + } else { + ActionId::Allowed + }) + .disposition(if enforced { + DispositionId::Blocked + } else { + DispositionId::Allowed + }) + .http_request(HttpRequest::new( + "GET", + OcsfUrl::new("http", "blocked.example", "/", 443), + )) + .dst_endpoint(Endpoint::from_domain("blocked.example", 443)) + .firewall_rule("forward-policy", "l7") + .build(); + let error = build_forward_l7_evaluation_error( + "blocked.example", + 443, + mode, + "evaluation unavailable", + ); + // Use the same grouping as the forward-proxy production path. + emit_forward_l7_decision(decision, Some(error), &connect); + }) + .await; + + assert_eq!(events.len(), 2); + let http_decision = events + .iter() + .find(|event| event["class_uid"] == 4002) + .unwrap(); + let evaluation_error = events + .iter() + .find(|event| event["class_uid"] == 4001) + .unwrap(); + let denies = deny_spans(&received); + if enforced { + assert_eq!(denies.len(), 1); + assert_eq!(received.spans.len(), if level == "debug" { 2 } else { 1 }); + assert_eq!(http_decision["action_id"], 2); + assert_eq!(evaluation_error["action_id"], 2); + assert_deny_resolves(&events, &received, level == "warn"); + let attribute = |key: &str| span_attribute(denies[0], key); + assert_eq!( + attribute("openshell.policy.name"), + Some(&Value::StringValue("forward-policy".to_string())), + "the shared deny span must retain the HTTP decision's policy name" + ); + assert_eq!( + attribute("server.address"), + Some(&Value::StringValue("blocked.example".to_string())) + ); + assert_eq!(attribute("ocsf.class_uid"), Some(&Value::IntValue(4002))); + // This production decision currently has no status_detail. + assert!(attribute("openshell.policy.reason").is_none()); + if level == "debug" { + let connect = received + .spans + .iter() + .find(|span| span.name == "supervisor.egress.connect") + .unwrap(); + assert_eq!(denies[0].parent_span_id, connect.span_id); + assert_eq!(denies[0].trace_id, connect.trace_id); + } + } else { + assert_eq!(http_decision["action_id"], 1); + assert_eq!(evaluation_error["action_id"], 99); + assert_no_deny(&events, &received); + } + } + } +} diff --git a/crates/openshell-supervisor-network/src/run.rs b/crates/openshell-supervisor-network/src/run.rs index 9f8ce690f0..0c342c6bab 100644 --- a/crates/openshell-supervisor-network/src/run.rs +++ b/crates/openshell-supervisor-network/src/run.rs @@ -22,7 +22,7 @@ use openshell_core::policy::{NetworkMode, SandboxPolicy}; use openshell_core::proto::SandboxPolicy as ProtoSandboxPolicy; use openshell_core::provider_credentials::ProviderCredentialState; use openshell_ocsf::{ - ConfigStateChangeBuilder, SeverityId, StateId, StatusId, ctx::ctx as ocsf_ctx, ocsf_emit, + ConfigStateChangeBuilder, SeverityId, StateId, StatusId, ctx::ctx as ocsf_ctx, }; use openshell_core::activity::ActivitySender; @@ -491,7 +491,7 @@ pub async fn run_networking( let upstream_config = build_upstream_client_config(&system_ca_bundle)?; let cert_cache = CertCache::new(ca); let state = Arc::new(ProxyTlsState::new(cert_cache, upstream_config)); - ocsf_emit!( + crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(ocsf_ctx()) .severity(SeverityId::Informational) .status(StatusId::Success) @@ -510,7 +510,7 @@ pub async fn run_networking( // cannot rewrite credentials, so it fails closed on // TLS-bearing connections (see proxy.rs) rather than // leaking placeholders through a raw tunnel. - ocsf_emit!( + crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(ocsf_ctx()) .severity(SeverityId::High) .status(StatusId::Failure) @@ -529,7 +529,7 @@ pub async fn run_networking( // rewrite credentials, so it fails closed on TLS-bearing // connections (see proxy.rs) rather than leaking placeholders // through a raw tunnel. - ocsf_emit!( + crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(ocsf_ctx()) .severity(SeverityId::High) .status(StatusId::Failure) diff --git a/crates/openshell-supervisor-network/src/telemetry.rs b/crates/openshell-supervisor-network/src/telemetry.rs new file mode 100644 index 0000000000..73e573b985 --- /dev/null +++ b/crates/openshell-supervisor-network/src/telemetry.rs @@ -0,0 +1,222 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Keep a deny span active during OCSF emission. + +use std::collections::HashMap; +use std::sync::{LazyLock, Mutex}; +use std::time::{Duration, Instant}; + +use openshell_ocsf::{ActionId, HttpActivityEvent, OcsfEvent}; + +/// Emit one egress event, entering an INFO deny span if it is denied HTTP Activity. +macro_rules! egress_emit { + ($event:expr) => { + $crate::telemetry::emit_decision([$event], None) + }; +} +pub(crate) use egress_emit; + +fn is_denied(event: &OcsfEvent) -> bool { + match event { + OcsfEvent::NetworkActivity(event) => event.action == Some(ActionId::Denied), + OcsfEvent::HttpActivity(event) => event.action == Some(ActionId::Denied), + OcsfEvent::DetectionFinding(event) => event.action == Some(ActionId::Denied), + _ => false, + } +} + +fn as_http(event: &OcsfEvent) -> Option<&HttpActivityEvent> { + match event { + OcsfEvent::HttpActivity(event) => Some(event), + _ => None, + } +} + +/// Emit all records of one synchronous decision under one deny span. Only HTTP +/// Activity carries `trace`, so only a denied decision with an HTTP record gets +/// a span. The span describes the denied HTTP record, or the first HTTP record +/// when only a companion finding is denied. A saved `parent` keeps the span in +/// the connect trace after authorization exits. +#[allow(clippy::disallowed_macros)] +pub fn emit_decision(events: impl IntoIterator, parent: Option<&tracing::Span>) { + let events: Vec<_> = events.into_iter().collect(); + let span = if events.iter().any(is_denied) { + events + .iter() + .filter(|event| is_denied(event)) + .find_map(as_http) + .or_else(|| events.iter().find_map(as_http)) + .and_then(|event| deny_span(event, parent)) + } else { + None + }; + let emit = || { + for event in events { + openshell_ocsf::ocsf_emit!(event); + } + }; + if let Some(span) = span { + span.in_scope(emit); + } else { + emit(); + } +} + +/// Returns `None` when deny spans are disabled or the destination has used up +/// its deny spans. Disabled spans spend no tokens. +fn deny_span(event: &HttpActivityEvent, parent: Option<&tracing::Span>) -> Option { + if !tracing::enabled!(kind: tracing::metadata::Kind::SPAN, tracing::Level::INFO) { + return None; + } + let endpoint = event.dst_endpoint.as_ref(); + let address = + endpoint.and_then(|endpoint| endpoint.domain.as_deref().or(endpoint.ip.as_deref())); + let port = endpoint.and_then(|endpoint| endpoint.port); + let destination = format!( + "{}:{}", + address.unwrap_or_default(), + port.unwrap_or_default() + ); + if !LIMITER + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .allow(&destination, Instant::now()) + { + return None; + } + let parent = parent.cloned().unwrap_or_else(tracing::Span::current); + Some(tracing::info_span!( + parent: &parent, + "supervisor.egress.deny", + openshell.policy.decision = "deny", + openshell.policy.name = event.firewall_rule.as_ref().map(|rule| rule.name.as_str()), + openshell.policy.reason = event.base.status_detail.as_deref(), + server.address = address, + server.port = port.map(i64::from), + ocsf.class_uid = i64::from(event.base.class_uid), + )) +} + +/// Deny spans one destination can export at once; one more is allowed per +/// second after that. +const DENY_SPAN_BURST: u32 = 10; +const DENY_SPAN_REFILL: Duration = Duration::from_secs(1); +const DENY_SPAN_DESTINATIONS: usize = 1024; +/// Bound aggregate span creation even when a workload rotates destinations. +const DENY_SPAN_GLOBAL_BURST: u32 = 100; +const DENY_SPAN_GLOBAL_REFILL: Duration = Duration::from_millis(10); + +static LIMITER: LazyLock> = LazyLock::new(|| Mutex::new(default_limiter())); + +fn default_limiter() -> DenySpanLimiter { + DenySpanLimiter::new(DENY_SPAN_BURST, DENY_SPAN_REFILL, DENY_SPAN_DESTINATIONS) +} + +#[cfg(test)] +fn reset_deny_span_limiter() { + *LIMITER + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) = default_limiter(); +} + +/// Per-destination and process-wide token buckets bounding deny-span work. +/// OCSF records are unaffected; a limited decision creates no additional span. +struct DenySpanLimiter { + burst: u32, + refill: Duration, + capacity: usize, + buckets: HashMap, + global_burst: u32, + global_refill: Duration, + global: Option, +} + +struct Bucket { + tokens: u32, + refilled: Instant, +} + +impl DenySpanLimiter { + fn new(burst: u32, refill: Duration, capacity: usize) -> Self { + Self::with_global_limit( + burst, + refill, + capacity, + DENY_SPAN_GLOBAL_BURST, + DENY_SPAN_GLOBAL_REFILL, + ) + } + + fn with_global_limit( + burst: u32, + refill: Duration, + capacity: usize, + global_burst: u32, + global_refill: Duration, + ) -> Self { + Self { + burst, + refill, + capacity, + buckets: HashMap::new(), + global_burst, + global_refill, + global: None, + } + } + + fn allow(&mut self, destination: &str, now: Instant) -> bool { + let global = self.global.get_or_insert(Bucket { + tokens: self.global_burst, + refilled: now, + }); + global.refill(self.global_burst, self.global_refill, now); + if global.tokens == 0 { + return false; + } + if !self.buckets.contains_key(destination) && self.buckets.len() >= self.capacity { + for bucket in self.buckets.values_mut() { + bucket.refill(self.burst, self.refill, now); + } + let burst = self.burst; + self.buckets.retain(|_, bucket| bucket.tokens < burst); + if self.buckets.len() >= self.capacity { + return false; + } + } + let bucket = self + .buckets + .entry(destination.to_string()) + .or_insert(Bucket { + tokens: self.burst, + refilled: now, + }); + bucket.refill(self.burst, self.refill, now); + let allowed = bucket.tokens > 0; + if allowed { + bucket.tokens -= 1; + global.tokens -= 1; + } + allowed + } +} + +impl Bucket { + fn refill(&mut self, burst: u32, interval: Duration, now: Instant) { + let earned = now.saturating_duration_since(self.refilled).as_nanos() / interval.as_nanos(); + if earned == 0 { + return; + } + let earned = u32::try_from(earned).unwrap_or(u32::MAX); + self.tokens = self.tokens.saturating_add(earned).min(burst); + self.refilled = if self.tokens == burst { + now + } else { + self.refilled + interval * earned + }; + } +} + +#[cfg(test)] +pub mod tests; diff --git a/crates/openshell-supervisor-network/src/telemetry/tests.rs b/crates/openshell-supervisor-network/src/telemetry/tests.rs new file mode 100644 index 0000000000..7a3388ad0d --- /dev/null +++ b/crates/openshell-supervisor-network/src/telemetry/tests.rs @@ -0,0 +1,602 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::{Arc, LazyLock, Mutex}; + +use openshell_otel_test_support::{OtlpTestServer, ReceivedTraces, tracing_test_lock}; +use opentelemetry_proto::tonic::common::v1::any_value::Value; +use opentelemetry_proto::tonic::trace::v1::Span; +use tracing_subscriber::prelude::*; + +#[derive(Clone)] +struct Writer(Arc>>); +impl std::io::Write for Writer { + fn write(&mut self, bytes: &[u8]) -> std::io::Result { + self.0.lock().unwrap().extend_from_slice(bytes); + Ok(bytes.len()) + } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +/// Capture the actual OCSF dispatch and the spans an in-memory exporter +/// receives through the supervisor's span filter. +pub async fn exported( + log_level: &str, + emit: impl FnOnce(), +) -> (Vec, ReceivedTraces) { + let _lock = tracing_test_lock().await; + LazyLock::force(®ISTER); + super::reset_deny_span_limiter(); + let exporter = opentelemetry_sdk::trace::InMemorySpanExporter::default(); + let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder() + .with_simple_exporter(exporter.clone()) + .build(); + let events = capture(log_level, &provider, emit); + provider.force_flush().unwrap(); + let spans = exporter + .get_finished_spans() + .unwrap() + .into_iter() + .map(Into::into) + .collect(); + provider.shutdown().unwrap(); + ( + events, + ReceivedTraces { + spans, + ..ReceivedTraces::default() + }, + ) +} + +/// Capture the actual OCSF dispatch and receive spans over OTLP/gRPC. +pub async fn exported_over_otlp( + log_level: &str, + emit: impl FnOnce(), +) -> (Vec, ReceivedTraces) { + let _lock = tracing_test_lock().await; + LazyLock::force(®ISTER); + super::reset_deny_span_limiter(); + let collector = OtlpTestServer::start().await; + let (provider, error) = openshell_otel::provider_for(Some(openshell_otel::OtlpExportConfig { + endpoint: collector.endpoint(), + service_name: openshell_otel::ServiceName::Fixed("openshell-supervisor"), + service_version: None, + resource_attributes: vec![], + })); + assert!(error.is_none()); + let provider = provider.unwrap(); + let events = capture(log_level, &provider, emit); + provider.force_flush().unwrap(); + collector.wait_for_export().await; + provider.shutdown().unwrap(); + (events, collector.shutdown().await) +} + +static REGISTER: LazyLock<()> = LazyLock::new(|| { + assert!(openshell_ocsf::register_trace_correlation_extractor( + openshell_otel::current_ocsf_trace_correlation + )); +}); + +fn capture( + log_level: &str, + provider: &opentelemetry_sdk::trace::SdkTracerProvider, + emit: impl FnOnce(), +) -> Vec { + let bytes = Arc::new(Mutex::new(vec![])); + let subscriber = tracing_subscriber::registry() + .with( + openshell_ocsf::OcsfJsonlLayer::new(Writer(bytes.clone())) + .with_filter(tracing_subscriber::filter::LevelFilter::INFO), + ) + .with( + openshell_otel::layer(provider, "openshell-supervisor") + .with_filter(openshell_otel::supervisor_span_filter(log_level)), + ) + // The shared test registry keeps callsites enabled process-wide. + // Gate this subscriber too, so the default case also checks a disabled + // DEBUG connect handle, as well as absence from the OTLP export. + .with(openshell_otel::supervisor_span_filter(log_level)); + tracing::subscriber::with_default(subscriber, emit); + String::from_utf8(bytes.lock().unwrap().clone()) + .unwrap() + .lines() + .map(|line| serde_json::from_str(line).unwrap()) + .collect() +} + +pub fn assert_deny_resolves(events: &[serde_json::Value], received: &ReceivedTraces, root: bool) { + assert!(events.iter().any(|event| event["class_uid"] == 4002)); + for event in events { + if event["class_uid"] != 4002 { + assert!( + event.get("trace").is_none(), + "only HTTP Activity carries trace" + ); + continue; + } + let uid = event["trace"]["uid"] + .as_str() + .expect("emission was inside sampled deny span"); + assert_eq!(uid.len(), 32); + assert!( + event["metadata"]["profiles"] + .as_array() + .unwrap() + .iter() + .any(|p| p == "trace") + ); + let span = deny_spans(received) + .into_iter() + .find(|span| hex::encode(&span.trace_id) == uid) + .expect("OCSF trace.uid resolves to exported deny span"); + if root { + assert!(span.parent_span_id.is_empty()); + assert_eq!( + received + .spans + .iter() + .filter(|other| other.trace_id == span.trace_id) + .count(), + 1 + ); + } + let attributes = span + .attributes + .iter() + .map(|kv| kv.key.as_str()) + .collect::>(); + for key in ["openshell.policy.decision", "ocsf.class_uid"] { + assert!(attributes.contains(&key), "deny span carries {key}"); + } + let class_uid = span_attribute(span, "ocsf.class_uid"); + assert!( + matches!(class_uid, Some(Value::IntValue(_))), + "deny span exports ocsf.class_uid as an integer: {class_uid:?}" + ); + } +} + +pub fn http_deny(host: &str, decision: &str) -> openshell_ocsf::OcsfEvent { + openshell_ocsf::HttpActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .action(openshell_ocsf::ActionId::Denied) + .http_request(openshell_ocsf::HttpRequest::new( + "GET", + openshell_ocsf::Url::new("https", host, "/", 443), + )) + .dst_endpoint(openshell_ocsf::Endpoint::from_domain(host, 443)) + .status_detail(decision) + .message(decision) + .build() +} + +pub fn deny_spans(received: &ReceivedTraces) -> Vec<&Span> { + received + .spans + .iter() + .filter(|span| span.name == "supervisor.egress.deny") + .collect() +} + +/// Assert that no record carries `trace` and no deny span was exported. +pub fn assert_no_deny(events: &[serde_json::Value], received: &ReceivedTraces) { + for event in events { + assert!(event.get("trace").is_none(), "{event}"); + } + assert!(deny_spans(received).is_empty()); +} + +pub fn span_attribute<'a>(span: &'a Span, key: &str) -> Option<&'a Value> { + span.attributes + .iter() + .find(|attribute| attribute.key == key) + .and_then(|attribute| attribute.value.as_ref()?.value.as_ref()) +} + +pub fn assert_deny_attribute(received: &ReceivedTraces, key: &str, expected: &str) { + assert_eq!( + span_attribute(deny_spans(received)[0], key), + Some(&Value::StringValue(expected.to_string())) + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn companion_records_share_one_deny_span() { + let (events, received) = exported("warn", || { + let event = http_deny("blocked.example", "denied"); + let finding = + openshell_ocsf::DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()).build(); + super::emit_decision([event, finding], None); + }) + .await; + assert_eq!(events.len(), 2); + assert_eq!(received.spans.len(), 1); + assert_deny_resolves(&events, &received, true); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn concurrent_decisions_keep_companions_and_request_contexts_separate() { + for level in ["warn", "debug"] { + let (events, received) = exported(level, || { + let dispatch = tracing::dispatcher::get_default(Clone::clone); + let barrier = std::sync::Barrier::new(2); + std::thread::scope(|threads| { + for host in ["alpha.example", "beta.example"] { + let dispatch = &dispatch; + let barrier = &barrier; + threads.spawn(move || { + tracing::dispatcher::with_default(dispatch, || { + let outside = openshell_ocsf::ctx::ctx().sandbox_id.clone(); + let mut context = (*openshell_ocsf::ctx::ctx()).clone(); + context.sandbox_id = host.to_string(); + context.sandbox_name = format!("sandbox-{host}"); + openshell_ocsf::ctx::bind_thread_ctx(Some(Arc::new(context))); + let connect = tracing::debug_span!("supervisor.egress.connect"); + connect.in_scope(|| { + for attempt in 0..2 { + let decision = format!("{host}:{attempt}"); + let event = http_deny(host, &decision); + let finding = openshell_ocsf::DetectionFindingBuilder::new( + openshell_ocsf::ctx::ctx(), + ) + .message(&decision) + .build(); + super::emit_decision([event, finding], None); + // Both threads finish each decision before either + // starts the next. + barrier.wait(); + } + }); + openshell_ocsf::ctx::bind_thread_ctx(None); + assert_eq!(openshell_ocsf::ctx::ctx().sandbox_id, outside); + }); + }); + } + }); + }) + .await; + + assert_eq!(events.len(), 8); + assert_deny_resolves(&events, &received, level == "warn"); + let denies = deny_spans(&received); + assert_eq!(denies.len(), 4); + assert_eq!(received.spans.len(), if level == "debug" { 6 } else { 4 }); + let attribute = |span: &Span, key: &str| match span_attribute(span, key) { + Some(Value::StringValue(value)) => value.clone(), + other => panic!("expected string attribute {key}, got {other:?}"), + }; + let mut traces_by_host = Vec::new(); + for host in ["alpha.example", "beta.example"] { + let mut traces = Vec::new(); + for attempt in 0..2 { + let decision = format!("{host}:{attempt}"); + let pair = events + .iter() + .filter(|event| event["message"] == decision) + .collect::>(); + assert_eq!(pair.len(), 2); + for event in &pair { + assert_eq!(event["device"]["container"]["uid"], host); + assert_eq!( + event["device"]["container"]["name"], + format!("sandbox-{host}") + ); + } + let http = pair + .iter() + .find(|event| event["class_uid"] == 4002) + .unwrap(); + let spans = denies + .iter() + .filter(|span| attribute(span, "openshell.policy.reason") == decision) + .collect::>(); + assert_eq!(spans.len(), 1); + let deny = spans[0]; + assert_eq!(attribute(deny, "server.address"), host); + assert_eq!(http["trace"]["uid"], hex::encode(&deny.trace_id)); + if level == "debug" { + let connect = received + .spans + .iter() + .find(|span| span.span_id == deny.parent_span_id) + .unwrap(); + assert_eq!(connect.name, "supervisor.egress.connect"); + assert_eq!(connect.trace_id, deny.trace_id); + } + traces.push(deny.trace_id.clone()); + } + if level == "debug" { + assert_eq!(traces[0], traces[1]); + } else { + assert_ne!(traces[0], traces[1]); + } + traces_by_host.push(traces); + } + for alpha in &traces_by_host[0] { + for beta in &traces_by_host[1] { + assert_ne!( + alpha, beta, + "independent request contexts must stay separate" + ); + } + } + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn deny_span_describes_the_denied_http_record() { + let (events, received) = exported("warn", || { + let allowed = openshell_ocsf::HttpActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .action(openshell_ocsf::ActionId::Allowed) + .http_request(openshell_ocsf::HttpRequest::new( + "GET", + openshell_ocsf::Url::new("https", "allowed.example", "/", 443), + )) + .dst_endpoint(openshell_ocsf::Endpoint::from_domain( + "allowed.example", + 443, + )) + .build(); + super::emit_decision([allowed, http_deny("blocked.example", "denied")], None); + }) + .await; + assert_deny_resolves(&events, &received, true); + assert_deny_attribute(&received, "server.address", "blocked.example"); + assert_deny_attribute(&received, "openshell.policy.reason", "denied"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn missing_deny_context_leaves_optional_attributes_unset() { + let (events, received) = exported("warn", || { + let http = openshell_ocsf::HttpActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .action(openshell_ocsf::ActionId::Denied) + .http_request(openshell_ocsf::HttpRequest { + http_method: "GET".parse().unwrap(), + url: None, + }) + .message("diagnostic text is not a policy reason") + .build(); + super::egress_emit!(http); + }) + .await; + assert_deny_resolves(&events, &received, true); + assert_eq!(events.len(), 1); + assert_eq!(received.spans.len(), 1); + for span in &received.spans { + for key in [ + "openshell.policy.name", + "openshell.policy.reason", + "server.address", + "server.port", + ] { + assert!( + span.attributes.iter().all(|kv| kv.key != key), + "{key} stays unset" + ); + } + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn denials_without_an_http_record_do_not_open_deny_spans() { + let (events, received) = exported("warn", || { + let network = openshell_ocsf::NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .action(openshell_ocsf::ActionId::Denied) + .dst_endpoint(openshell_ocsf::Endpoint::from_domain( + "blocked.example", + 443, + )) + .build(); + let finding = openshell_ocsf::DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) + .action(openshell_ocsf::ActionId::Denied) + .build(); + super::emit_decision([network, finding], None); + }) + .await; + assert_eq!(events.len(), 2); + assert_no_deny(&events, &received); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn allowed_emissions_do_not_create_info_deny_spans() { + let (events, received) = exported("warn", || { + let allowed = openshell_ocsf::NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .action(openshell_ocsf::ActionId::Allowed) + .dst_endpoint(openshell_ocsf::Endpoint::from_domain( + "allowed.example", + 443, + )) + .build(); + super::egress_emit!(allowed); + crate::l7::middleware::emit_middleware_uninspectable( + &crate::l7::relay::L7EvalContext::default(), + "unknown protocol", + false, + ); + }) + .await; + assert_eq!(events.len(), 2); + assert_no_deny(&events, &received); +} + +#[test] +fn deny_span_limiter_bounds_each_destination_and_refills() { + use std::time::{Duration, Instant}; + + let start = Instant::now(); + let mut limiter = super::DenySpanLimiter::new(2, Duration::from_secs(1), 8); + assert!(limiter.allow("a:443", start)); + assert!(limiter.allow("a:443", start)); + assert!(!limiter.allow("a:443", start)); + assert!( + limiter.allow("b:443", start), + "destinations are independent" + ); + assert!(!limiter.allow("a:443", start + Duration::from_millis(999))); + assert!(limiter.allow("a:443", start + Duration::from_secs(1))); + assert!(!limiter.allow("a:443", start + Duration::from_secs(1))); + let later = start + Duration::from_mins(1); + assert!(limiter.allow("a:443", later)); + assert!(limiter.allow("a:443", later)); + assert!(!limiter.allow("a:443", later), "refill stops at the burst"); +} + +#[test] +fn deny_span_limiter_bounds_tracked_destinations() { + use std::time::{Duration, Instant}; + + let start = Instant::now(); + let mut limiter = super::DenySpanLimiter::new(1, Duration::from_secs(1), 2); + assert!(limiter.allow("a", start)); + assert!(limiter.allow("b", start)); + assert!( + !limiter.allow("c", start), + "untracked destinations wait for room" + ); + // Full buckets are idle and can be evicted. + assert!(limiter.allow("c", start + Duration::from_secs(1))); +} + +#[test] +fn deny_span_limiter_bounds_aggregate_bursts_and_refills() { + use std::time::{Duration, Instant}; + + let start = Instant::now(); + let mut limiter = super::DenySpanLimiter::with_global_limit( + 1, + Duration::from_secs(1), + 8, + 2, + Duration::from_millis(100), + ); + assert!(limiter.allow("a", start)); + assert!( + !limiter.allow("a", start), + "a destination rejection spends no global token" + ); + assert!(limiter.allow("b", start)); + assert!( + !limiter.allow("c", start), + "rotating destinations cannot exceed the global burst" + ); + assert!(!limiter.allow("c", start + Duration::from_millis(99))); + assert!(limiter.allow("c", start + Duration::from_millis(100))); + assert!(!limiter.allow("d", start + Duration::from_millis(100))); + let later = start + Duration::from_secs(10); + assert!(limiter.allow("a", later)); + assert!(limiter.allow("b", later)); + assert!( + !limiter.allow("c", later), + "global refill stops at its burst" + ); +} + +#[test] +fn destination_capacity_rejections_spend_no_global_tokens() { + use std::time::{Duration, Instant}; + + let start = Instant::now(); + let mut limiter = super::DenySpanLimiter::with_global_limit( + 10, + Duration::from_secs(1), + 1, + 2, + Duration::from_secs(1), + ); + assert!(limiter.allow("a", start)); + assert!(!limiter.allow("b", start)); + assert!(limiter.allow("a", start)); + assert!(!limiter.allow("a", start)); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn aggregate_deny_limit_preserves_every_audit_record() { + let (events, received) = exported("warn", || { + *super::LIMITER.lock().unwrap() = super::DenySpanLimiter::with_global_limit( + 10, + std::time::Duration::from_secs(1), + 1024, + 3, + std::time::Duration::MAX, + ); + for index in 0..12 { + super::egress_emit!(http_deny(&format!("host-{index}.example"), "denied")); + } + }) + .await; + assert_eq!( + events.len(), + 12, + "the span budget never suppresses audit records" + ); + assert!(events.iter().all(|event| event["action_id"] == 2)); + assert_eq!(deny_spans(&received).len(), 3); + assert_eq!( + events + .iter() + .filter(|event| event.get("trace").is_some()) + .count(), + 3 + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn limited_denial_keeps_an_active_sampled_connect_trace() { + let (events, received) = exported("debug", || { + *super::LIMITER.lock().unwrap() = super::DenySpanLimiter::with_global_limit( + 1, + std::time::Duration::MAX, + 1024, + 100, + std::time::Duration::MAX, + ); + tracing::debug_span!("supervisor.egress.connect").in_scope(|| { + super::egress_emit!(http_deny("blocked.example", "first")); + super::egress_emit!(http_deny("blocked.example", "limited")); + }); + }) + .await; + assert_eq!(events.len(), 2); + assert_eq!(deny_spans(&received).len(), 1); + let connect = received + .spans + .iter() + .find(|span| span.name == "supervisor.egress.connect") + .unwrap(); + assert_eq!(events[1]["trace"]["uid"], hex::encode(&connect.trace_id)); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn repeated_denials_to_one_destination_export_bounded_spans() { + let (events, received) = exported("warn", || { + for _ in 0..(super::DENY_SPAN_BURST + 5) { + super::egress_emit!(http_deny("loop.example", "denied")); + } + super::egress_emit!(http_deny("other.example", "denied")); + }) + .await; + let denies = deny_spans(&received).len(); + assert_eq!(denies, super::DENY_SPAN_BURST as usize + 1); + let traced = events + .iter() + .filter(|event| event.get("trace").is_some()) + .count(); + assert_eq!(traced, denies, "limited records carry no trace.uid"); + assert_eq!(events.len(), super::DENY_SPAN_BURST as usize + 6); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn disabled_deny_spans_spend_no_tokens() { + let _lock = tracing_test_lock().await; + super::reset_deny_span_limiter(); + let subscriber = + tracing_subscriber::registry().with(tracing_subscriber::filter::LevelFilter::WARN); + tracing::subscriber::with_default(subscriber, || { + super::egress_emit!(http_deny("quiet.example", "denied")); + }); + assert!(super::LIMITER.lock().unwrap().buckets.is_empty()); + assert!(super::LIMITER.lock().unwrap().global.is_none()); +} diff --git a/crates/openshell-supervisor-network/src/test_support.rs b/crates/openshell-supervisor-network/src/test_support.rs index d1b23b6f31..5c302b91de 100644 --- a/crates/openshell-supervisor-network/src/test_support.rs +++ b/crates/openshell-supervisor-network/src/test_support.rs @@ -1,7 +1,28 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! Executable identity and socket-owner subprocess fixtures. +//! Credential, executable identity, and socket-owner subprocess fixtures. + +pub fn header_placement_metadata( + keys: &[&str], +) -> std::collections::HashMap { + keys.iter() + .map(|key| { + ( + format!( + "{}{key}", + openshell_core::provider_credentials::STATIC_CREDENTIAL_METADATA_PREFIX + ), + openshell_core::proto::ProviderProfileCredential { + env_vars: vec![(*key).to_string()], + auth_style: "bearer".to_string(), + header_name: "Authorization".to_string(), + ..Default::default() + }, + ) + }) + .collect() +} #[cfg(target_os = "linux")] use std::net::TcpStream; diff --git a/crates/openshell-supervisor-process/src/ssh.rs b/crates/openshell-supervisor-process/src/ssh.rs index 63ae30fbcc..795838da17 100644 --- a/crates/openshell-supervisor-process/src/ssh.rs +++ b/crates/openshell-supervisor-process/src/ssh.rs @@ -46,6 +46,7 @@ const MAIN_DETACH_KEY: u8 = 0x11; const MAIN_DETACH_EOF: u8 = 0x04; const SSH_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(15); const SSH_PEER_TIMEOUT: Duration = Duration::from_mins(1); +const STARTUP_DIAGNOSTIC_TIMEOUT: Duration = Duration::from_millis(50); mod peer_stream; @@ -392,6 +393,7 @@ async fn handle_connection( #[allow(clippy::struct_excessive_bools)] #[derive(Default)] struct ChannelState { + output: Option>, upload: Option, command_started: bool, input_sender: Option, @@ -501,7 +503,15 @@ impl russh::server::Handler for SshHandler { reply: ChannelOpenHandle, _session: &mut Session, ) -> Result<(), Self::Error> { - self.channels.insert(channel.id(), ChannelState::default()); + let id = channel.id(); + let (_, output) = channel.split(); + self.channels.insert( + id, + ChannelState { + output: Some(output), + ..Default::default() + }, + ); reply.accept().await; Ok(()) } @@ -687,11 +697,9 @@ impl russh::server::Handler for SshHandler { channel: ChannelId, session: &mut Session, ) -> Result<(), Self::Error> { - if self - .channels - .get(&channel) - .is_some_and(|state| state.upload.is_some() || state.command_started) - { + if self.channels.get(&channel).is_some_and(|state| { + state.upload.is_some() || state.command_started || state.main_attached + }) { session.channel_failure(channel)?; return Ok(()); } @@ -701,11 +709,15 @@ impl russh::server::Handler for SshHandler { // endings. Forcing a PTY here caused CRLF translation which made // VS Code misdetect the platform as Windows (and then try to run // `powershell`). + let terminal = self + .channels + .get(&channel) + .is_some_and(|state| state.pty_request.is_some()); match self.start_shell(channel, session.handle(), None).await { Ok(()) => session.channel_success(channel)?, Err(error) => { warn!(%error, ?channel, "failed to start boundary shell"); - session.channel_failure(channel)?; + self.finish_failed_start(channel, session, &error, terminal)?; } } Ok(()) @@ -717,11 +729,9 @@ impl russh::server::Handler for SshHandler { data: &[u8], session: &mut Session, ) -> Result<(), Self::Error> { - if self - .channels - .get(&channel) - .is_some_and(|state| state.upload.is_some() || state.command_started) - { + if self.channels.get(&channel).is_some_and(|state| { + state.upload.is_some() || state.command_started || state.main_attached + }) { session.channel_failure(channel)?; return Ok(()); } @@ -730,6 +740,10 @@ impl russh::server::Handler for SshHandler { session.channel_success(channel)?; return Ok(()); } + let terminal = self + .channels + .get(&channel) + .is_some_and(|state| state.pty_request.is_some()); match self .start_shell(channel, session.handle(), Some(command)) .await @@ -737,7 +751,7 @@ impl russh::server::Handler for SshHandler { Ok(()) => session.channel_success(channel)?, Err(error) => { warn!(%error, ?channel, "failed to start boundary exec"); - session.channel_failure(channel)?; + self.finish_failed_start(channel, session, &error, terminal)?; } } Ok(()) @@ -1237,6 +1251,56 @@ impl SshHandler { Ok(()) } + /// Complete an accepted command request whose boundary process cannot start. + /// Request failure alone leaves clients waiting for command output or exit. + fn finish_failed_start( + &mut self, + channel: ChannelId, + session: &mut Session, + error: &anyhow::Error, + terminal: bool, + ) -> anyhow::Result<()> { + let mut output = None; + if let Some(mut state) = self.channels.remove(&channel) { + output = state.output.take(); + state.input_task.take(); + // A PTY resize can fail after spawning. Release that process too. + if let Some(process) = state.process.take() { + tokio::spawn(async move { + if let Err(error) = process.terminate().await { + warn!(%error, "failed to terminate exec after startup failure"); + } + }); + } + } + let line_ending = if terminal { "\r\n" } else { "\n" }; + session.channel_success(channel)?; + if let Some(output) = output { + let diagnostic = + format!("openshell: cannot start command: {error}{line_ending}").into_bytes(); + tokio::spawn(async move { + // Reserve client credit before queueing bytes. Session output + // would buffer stderr without credit and block closure and + // Handle messages for every channel on this connection. + let _ = tokio::time::timeout( + STARTUP_DIAGNOSTIC_TIMEOUT, + output.extended_data_bytes(1, diagnostic), + ) + .await; + // Diagnostics may be partial or absent; terminal messages + // must still be sent when the peer withholds output credit. + let _ = output.exit_status(1).await; + let _ = output.eof().await; + let _ = output.close().await; + }); + } else { + session.exit_status_request(channel, 1)?; + session.eof(channel)?; + session.close(channel)?; + } + Ok(()) + } + async fn start_shell( &mut self, channel: ChannelId, @@ -1969,6 +2033,125 @@ mod tests { )); } + async fn assert_boundary_start_failure(shell: bool, terminal: bool) { + let client = authenticated_test_client().await; + let mut channel = client.channel_open_session().await.unwrap(); + if terminal { + channel + .request_pty(true, "xterm", 80, 24, 0, 0, &[]) + .await + .unwrap(); + assert!(matches!( + next_main_event(&mut channel).await, + russh::ChannelMsg::Success + )); + } + if shell { + channel.request_shell(true).await.unwrap(); + } else { + channel.exec(true, "true").await.unwrap(); + } + assert!(matches!( + next_main_event(&mut channel).await, + russh::ChannelMsg::Success + )); + match next_main_event(&mut channel).await { + russh::ChannelMsg::ExtendedData { data, ext: 1 } => { + let message = String::from_utf8_lossy(&data); + assert!( + message.starts_with("openshell: cannot start command: "), + "{message}" + ); + assert!( + message.contains("exec is not used by direct-tcpip tests"), + "{message}" + ); + assert!(message.ends_with(if terminal { "\r\n" } else { "\n" })); + } + event => panic!("expected startup diagnostic on stderr, got {event:?}"), + } + assert!(matches!( + next_main_event(&mut channel).await, + russh::ChannelMsg::ExitStatus { exit_status: 1 } + )); + assert!(matches!( + next_main_event(&mut channel).await, + russh::ChannelMsg::Eof + )); + assert!(matches!( + next_main_event(&mut channel).await, + russh::ChannelMsg::Close + )); + assert!( + tokio::time::timeout(Duration::from_secs(5), channel.wait()) + .await + .expect("failed command channel must close") + .is_none() + ); + } + + #[tokio::test] + async fn boundary_exec_start_failure_reports_error_and_closes_channel() { + for terminal in [false, true] { + assert_boundary_start_failure(false, terminal).await; + } + } + + #[tokio::test] + async fn boundary_shell_start_failure_reports_error_and_closes_channel() { + for terminal in [false, true] { + assert_boundary_start_failure(true, terminal).await; + } + } + + #[tokio::test] + async fn main_attachment_rejects_extra_commands_without_losing_input_lease() { + for shell in [false, true] { + tokio::time::timeout(Duration::from_secs(5), async { + let (main, mut input) = MainSession::inert_with_input(); + let client = main_test_client(Some(main.clone())).await; + let mut attached = client.channel_open_session().await.unwrap(); + attached + .request_subsystem(true, "openshell-main") + .await + .unwrap(); + assert!(matches!( + next_main_event(&mut attached).await, + russh::ChannelMsg::Success + )); + if shell { + attached.request_shell(true).await.unwrap(); + } else { + attached.exec(true, "extra").await.unwrap(); + } + assert!(matches!( + next_main_event(&mut attached).await, + russh::ChannelMsg::Failure + )); + attached.data(&b"still attached"[..]).await.unwrap(); + assert_eq!(input.recv().await.unwrap(), b"still attached"); + attached.close().await.unwrap(); + // Processing a new channel follows the previous close on the + // wire, so the old owner's lease must already be released. + let mut fresh = client.channel_open_session().await.unwrap(); + fresh + .request_subsystem(true, "openshell-main") + .await + .unwrap(); + assert!(matches!( + next_main_event(&mut fresh).await, + russh::ChannelMsg::Success + )); + fresh.data(&b"new owner"[..]).await.unwrap(); + assert_eq!(input.recv().await.unwrap(), b"new owner"); + assert!(!main.finished()); + fresh.close().await.unwrap(); + }) + .await + .expect("extra command stranded main attachment or input lease"); + } + } + #[tokio::test] async fn main_attachment_missing_session_reports_terminal_error() { assert_main_rejection(None, false, "openshell: cannot connect to the canonical main process: no main session is available\n").await; diff --git a/crates/openshell-supervisor-process/src/ssh/exec_input_tests.rs b/crates/openshell-supervisor-process/src/ssh/exec_input_tests.rs index 35c16cf75d..bc3621b93f 100644 --- a/crates/openshell-supervisor-process/src/ssh/exec_input_tests.rs +++ b/crates/openshell-supervisor-process/src/ssh/exec_input_tests.rs @@ -197,7 +197,19 @@ async fn exec_start_failure_keeps_ssh_session_usable() { let mut rejected = client.channel_open_session().await.unwrap(); rejected.exec(true, "first").await.unwrap(); - assert!(matches!(rejected.wait().await, Some(ChannelMsg::Failure))); + assert!(matches!(rejected.wait().await, Some(ChannelMsg::Success))); + match rejected.wait().await { + Some(ChannelMsg::ExtendedData { data, ext: 1 }) => { + assert!(String::from_utf8_lossy(&data).contains("test boundary admission failure")); + } + event => panic!("expected boundary failure diagnostic, got {event:?}"), + } + assert!(matches!( + rejected.wait().await, + Some(ChannelMsg::ExitStatus { exit_status: 1 }) + )); + assert!(matches!(rejected.wait().await, Some(ChannelMsg::Eof))); + assert!(matches!(rejected.wait().await, Some(ChannelMsg::Close))); let mut channel = start_exec(&client).await; control.stdout.shutdown().await.unwrap(); @@ -218,6 +230,87 @@ async fn exec_start_failure_keeps_ssh_session_usable() { .expect("a rejected exec closed the SSH session"); } +#[tokio::test] +async fn startup_failure_backpressure_does_not_block_sibling_completion() { + for window_size in [0, 8, 65_536] { + for shell in [false, true] { + tokio::time::timeout(DEADLINE, async { + let (session, mut control) = exec_session(); + let client = test_client( + None, + rpc_test::exec(Arc::new(FailThenExec { + failed: AtomicBool::new(false), + session: Mutex::new(Some(session)), + })), + russh::client::Config { + window_size, + ..Default::default() + }, + ) + .await; + let mut rejected = client.channel_open_session().await.unwrap(); + if shell { + rejected.request_shell(true).await.unwrap(); + } else { + rejected.exec(true, "rejected").await.unwrap(); + } + assert!(matches!(rejected.wait().await, Some(ChannelMsg::Success))); + let mut sibling = start_exec(&client).await; + control.stdout.shutdown().await.unwrap(); + control.stderr.shutdown().await.unwrap(); + control + .process + .status + .send_replace(Some(BoundaryExitStatus::Exited(0))); + // Withhold reads on the failed channel until its sibling has + // finished: a small receive window must not stall the session. + let mut sibling_exited = false; + while let Some(event) = sibling.wait().await { + match event { + ChannelMsg::ExitStatus { exit_status: 0 } => sibling_exited = true, + ChannelMsg::Close => break, + _ => {} + } + } + assert!(sibling_exited, "sibling must report its exit status"); + let mut exited = false; + let mut eof = false; + let mut closed = false; + let mut diagnostic = Vec::new(); + while let Some(event) = rejected.wait().await { + match event { + ChannelMsg::ExtendedData { data, ext: 1 } => { + diagnostic.extend_from_slice(&data); + } + ChannelMsg::ExitStatus { exit_status: 1 } => exited = true, + ChannelMsg::Eof => eof = true, + ChannelMsg::Close => { + closed = true; + break; + } + event => panic!("unexpected failure event: {event:?}"), + } + } + assert!( + exited && eof && closed, + "failure must finish independently of stderr" + ); + if window_size == 0 { + assert!(diagnostic.is_empty()); + } + if window_size == 65_536 { + assert!( + String::from_utf8_lossy(&diagnostic) + .contains("test boundary admission failure") + ); + } + }) + .await + .expect("startup diagnostic blocked channel closure or sibling completion"); + } + } +} + #[tokio::test] async fn full_input_allows_output_signals_and_channel_close() { tokio::time::timeout(DEADLINE, async { diff --git a/crates/openshell-supervisor/src/config_runtime.rs b/crates/openshell-supervisor/src/config_runtime.rs index ecda1a7194..02736ad184 100644 --- a/crates/openshell-supervisor/src/config_runtime.rs +++ b/crates/openshell-supervisor/src/config_runtime.rs @@ -2005,6 +2005,9 @@ mod tests { state .resolver_for_endpoint("tools.example.com", 443, "/v1/chat") .unwrap() + .with_request_location(openshell_core::secrets::RequestCredentialLocation::Header( + "Authorization".into(), + )) .resolve_placeholder(reference), Some("initial"), ); @@ -2076,6 +2079,9 @@ mod tests { state .resolver_for_endpoint("tools.example.com", 443, "/v1/chat") .unwrap() + .with_request_location(openshell_core::secrets::RequestCredentialLocation::Header( + "Authorization".into(), + )) .resolve_placeholder(reference), Some("initial"), ); @@ -2628,7 +2634,14 @@ mod tests { let resolved = state .resolver_for_endpoint("tools.example.com", 443, "/v1/chat") .and_then(|resolver| { - resolver.resolve_placeholder(&reference).map(str::to_owned) + resolver + .with_request_location( + openshell_core::secrets::RequestCredentialLocation::Header( + "Authorization".into(), + ), + ) + .resolve_placeholder(&reference) + .map(str::to_owned) }); let observed = tracker.observation(&state); if state.revision() == revision @@ -2650,6 +2663,11 @@ mod tests { state .resolver_for_endpoint("tools.example.com", 443, "/v1/chat") .unwrap() + .with_request_location( + openshell_core::secrets::RequestCredentialLocation::Header( + "Authorization".into(), + ) + ) .resolve_placeholder(old_reference), Some("initial") ); @@ -2722,7 +2740,14 @@ mod tests { let resolved = state .resolver_for_endpoint("tools.example.com", 443, "/v1/chat") .and_then(|resolver| { - resolver.resolve_placeholder(&reference).map(str::to_owned) + resolver + .with_request_location( + openshell_core::secrets::RequestCredentialLocation::Header( + "Authorization".into(), + ), + ) + .resolve_placeholder(&reference) + .map(str::to_owned) }); let observed = tracker.observation(&state); if state.revision() == revision diff --git a/crates/openshell-supervisor/src/main.rs b/crates/openshell-supervisor/src/main.rs index feb3aea9c8..4c1bcac7a0 100644 --- a/crates/openshell-supervisor/src/main.rs +++ b/crates/openshell-supervisor/src/main.rs @@ -18,7 +18,6 @@ use openshell_core::sandbox_env::OtlpProtocol; use openshell_isolation_interface::contract::BackendDescriptor; use tracing::{error, info, warn}; use tracing_subscriber::EnvFilter; -use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::{Layer as _, layer::SubscriberExt as _, util::SubscriberInitExt as _}; mod logging; @@ -455,6 +454,10 @@ fn main() -> Result<()> { return openshell_supervisor::check_control_readiness(&args.socket); } + openshell_ocsf::register_trace_correlation_extractor( + openshell_otel::current_ocsf_trace_correlation, + ); + let args = Args::parse(); if args.mode == SupervisorMode::Shared { return shared::run(args); @@ -563,7 +566,7 @@ fn main() -> Result<()> { otlp_layer_provider .as_ref() .map(|provider| openshell_otel::layer(provider, SERVICE_NAME)) - .with_filter(otlp_span_filter(&args.log_level)), + .with_filter(openshell_otel::supervisor_span_filter(&args.log_level)), ) .init(); if !file_available { @@ -845,16 +848,6 @@ fn supervisor_resource_attributes( .collect() } -/// A more verbose log level raises `openshell*` spans; dependency spans stay at -/// INFO so they do not flood the exporter queue. -fn otlp_span_filter(log_level: &str) -> EnvFilter { - let level = log_level - .parse::() - .unwrap_or(LevelFilter::INFO) - .max(LevelFilter::INFO); - EnvFilter::new(format!("info,openshell={level}")) -} - /// Choose the supervisor's exit status for a sandbox run. /// /// The reserved status tells the compute driver that the sandbox rejected the @@ -957,15 +950,6 @@ mod tests { use super::*; - #[test] - fn otlp_span_filter_raises_only_openshell_targets() { - let directives = |log_level: &str| otlp_span_filter(log_level).to_string(); - assert_eq!(directives("warn"), "openshell=info,info"); - assert_eq!(directives("info"), "openshell=info,info"); - assert_eq!(directives("debug"), "openshell=debug,info"); - assert_eq!(directives("info,h2=debug"), "openshell=info,info"); - } - #[test] fn only_workspace_validation_failure_exits_with_reserved_status() { use miette::WrapErr as _; diff --git a/crates/openshell-tui/src/lib.rs b/crates/openshell-tui/src/lib.rs index 133d7d5c0a..0e807cd452 100644 --- a/crates/openshell-tui/src/lib.rs +++ b/crates/openshell-tui/src/lib.rs @@ -1315,6 +1315,16 @@ fn render_policy_lines( // L7 allow rules. for l7 in &ep.rules { if let Some(allow) = &l7.allow { + if ep.protocol.eq_ignore_ascii_case("mcp") { + render_mcp_rule( + &mut lines, + "Allow", + &allow.method, + allow.params.get("name"), + t, + ); + continue; + } let method = if allow.method.is_empty() { "*" } else { @@ -1334,6 +1344,18 @@ fn render_policy_lines( } } + if ep.protocol.eq_ignore_ascii_case("mcp") { + for deny in &ep.deny_rules { + render_mcp_rule( + &mut lines, + "Deny", + &deny.method, + deny.params.get("name"), + t, + ); + } + } + // Access preset (if set instead of explicit rules). if ep.access != 0 && ep.rules.is_empty() { lines.push(Line::from(vec![ @@ -1371,6 +1393,37 @@ fn render_policy_lines( lines } +fn render_mcp_rule( + lines: &mut Vec>, + action: &str, + method: &str, + tool: Option<&openshell_core::proto::L7QueryMatcher>, + theme: &theme::Theme, +) { + use ratatui::text::{Line, Span}; + + let method = if method.is_empty() { + if tool.is_some() { "tools/call" } else { "*" } + } else { + method + }; + lines.push(Line::from(vec![ + Span::styled(format!(" {action}: "), theme.muted), + Span::styled(method.to_string(), theme.text), + ])); + if let Some(tool) = tool { + for name in std::iter::once(&tool.glob) + .filter(|name| !name.is_empty()) + .chain(&tool.any) + { + lines.push(Line::from(vec![ + Span::styled(" Tool: ", theme.muted), + Span::styled(name.clone(), theme.text), + ])); + } + } +} + // --------------------------------------------------------------------------- // Animation helper // --------------------------------------------------------------------------- @@ -3444,3 +3497,173 @@ mod sandbox_notes_tests { assert_eq!(sandbox_notes(&sandbox, String::new()), ""); } } + +#[cfg(test)] +mod policy_render_tests { + use super::*; + + fn policy_lines(yaml: &str) -> Vec> { + let policy = openshell_policy::parse_sandbox_policy(yaml).expect("valid policy"); + render_policy_lines(&policy, &theme::Theme::dark()) + } + + #[test] + fn mcp_tool_allowlist_is_visible_one_tool_per_line() { + let lines = policy_lines( + r" +version: 1 +network_policies: + review: + endpoints: + - host: host.openshell.internal + port: 32443 + protocol: mcp + enforcement: enforce + rules: + - allow: + method: tools/call + tool: + any: + - local_gh_issue_view + - local_gh_pr_checks + - local_gh_pr_details + - local_gh_pr_diff + - local_gh_pr_list + - local_gh_pr_view + - local_gh_repo_view + - local_gh_run_failed_logs + - local_gh_run_view + - local_gh_source_blob + - local_gh_source_tree + - local_gh_version +", + ); + let tools: Vec<_> = lines + .iter() + .map(ToString::to_string) + .filter_map(|line| line.strip_prefix(" Tool: ").map(str::to_string)) + .collect(); + assert_eq!( + tools, + [ + "local_gh_issue_view", + "local_gh_pr_checks", + "local_gh_pr_details", + "local_gh_pr_diff", + "local_gh_pr_list", + "local_gh_pr_view", + "local_gh_repo_view", + "local_gh_run_failed_logs", + "local_gh_run_view", + "local_gh_source_blob", + "local_gh_source_tree", + "local_gh_version", + ] + ); + assert!( + lines + .iter() + .any(|line| line.to_string() == " Allow: tools/call") + ); + assert!( + !lines + .iter() + .any(|line| line.to_string().contains("tools/call *")) + ); + + let mut buffer = ratatui::buffer::Buffer::empty(ratatui::layout::Rect::new(0, 0, 60, 30)); + ratatui::widgets::Widget::render( + ratatui::widgets::Paragraph::new(lines), + buffer.area, + &mut buffer, + ); + for tool in tools { + assert!(buffer.content.chunks(60).any(|row| { + row.iter() + .map(ratatui::buffer::Cell::symbol) + .collect::() + .contains(&tool) + })); + } + } + + #[test] + fn mcp_methods_and_allow_deny_selectors_preserve_their_scope() { + let lines = policy_lines( + r" +version: 1 +network_policies: + review: + endpoints: + - host: mcp.example.com + port: 443 + protocol: mcp + mcp: + allow_all_known_mcp_methods: true + rules: + - allow: + method: tools/list + - allow: + tool: read_source + - allow: + method: tools/call + params: + name: + any: [read_*, list_*] + deny_rules: + - tool: read_private + - method: tools/call + params: + name: + any: [read_secret_*, list_private] + - method: resources/read + profile: + endpoints: + - host: profile.example.com + port: 443 + protocol: mcp + mcp: + allow_all_known_mcp_methods: true + rules: + - allow: {} + deny_rules: + - {} + rest: + endpoints: + - host: api.example.com + port: 443 + protocol: rest + rules: + - allow: {method: GET, path: /repos/**} +", + ); + let text: Vec<_> = lines.iter().map(ToString::to_string).collect(); + assert!(text.iter().any(|line| line == " Allow: *")); + assert!(text.iter().any(|line| line == " Deny: *")); + let mcp_start = text + .iter() + .position(|line| line == " Allow: tools/list") + .unwrap(); + assert_eq!( + &text[mcp_start..mcp_start + 12], + [ + " Allow: tools/list", + " Allow: tools/call", + " Tool: read_source", + " Allow: tools/call", + " Tool: read_*", + " Tool: list_*", + " Deny: tools/call", + " Tool: read_private", + " Deny: tools/call", + " Tool: read_secret_*", + " Tool: list_private", + " Deny: resources/read", + ] + ); + assert!( + text.iter() + .any(|line| line == " Allow: GET /repos/**") + ); + } +} diff --git a/deploy/helm/openshell/README.md b/deploy/helm/openshell/README.md index 58aa132eb6..bc6676b9f1 100644 --- a/deploy/helm/openshell/README.md +++ b/deploy/helm/openshell/README.md @@ -615,6 +615,7 @@ discovery endpoint or its TLS CA. | podLabels | object | `{}` | Extra labels to add to the gateway pod. | | podLifecycle.terminationGracePeriodSeconds | int | `30` | Maximum time, in seconds, Kubernetes waits for the gateway to exit before killing it. The gateway exits as soon as shutdown completes; the limit covers supervisor session cleanup and draining queued OCSF records. | | podSecurityContext.fsGroup | int | `1000` | fsGroup assigned to the gateway pod. | +| priorityClassName | string | `""` | PriorityClass for the gateway pod. The gateway is a control-plane component; when it shares nodes with the workloads it serves, the default priority gives it no advantage under node pressure. Empty = omit the field. | | probes.liveness.failureThreshold | int | `3` | Liveness probe failure threshold before the container is restarted. | | probes.liveness.initialDelaySeconds | int | `2` | Liveness probe initial delay, in seconds. | | probes.liveness.periodSeconds | int | `5` | Liveness probe period, in seconds. | @@ -758,6 +759,7 @@ discovery endpoint or its TLS CA. | supervisor.replicas | int | `1` | Shared supervisor pool members, used only when gatewayConfig selects supervisor_topology = "gateway". The gateway always coordinates the shared supervisor as a high-availability pool: when a member fails or leaves, another member takes over its sandboxes without restarting their workloads. 1 has no failover; use 2 or more to survive losing a member. Only the supervisor Deployment's replica count depends on this value, so changing it neither restarts the gateway nor replaces running members. Ignored when supervisor.autoscaling.enabled is true. | | supervisor.sandboxRuntime.boundaryPort | int | `5500` | Workload boundary TLS listener port. | | tolerations | list | `[]` | Tolerations for the gateway pod. | +| topologySpreadConstraints | list | `[]` | Topology spread constraints for the gateway pod, used to spread replicas across zones or nodes. Empty = omit the field. | | upstreamProxy | object | `{"authAllowInsecure":false,"authSecret":{"key":"","name":""},"caBundle":{"configMapName":"","key":"ca.crt"},"connectByHostname":false,"noProxy":"","url":""}` | Operator-owned corporate forward proxy for policy-approved TLS egress from Kubernetes sandboxes. The workload cannot select or override it. | | upstreamProxy.authAllowInsecure | bool | `false` | Required when authSecret is configured because Basic auth to an HTTP proxy is cleartext. | | upstreamProxy.authSecret.key | string | `""` | Secret key containing the proxy credential. | diff --git a/deploy/helm/openshell/ci/values-local-tracing.yaml b/deploy/helm/openshell/ci/values-local-tracing.yaml new file mode 100644 index 0000000000..68d1f23500 --- /dev/null +++ b/deploy/helm/openshell/ci/values-local-tracing.yaml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Export gateway traces to the collector that `mise run helm:k3s:create` +# installs. Layer only onto clusters that run that collector. +gatewayConfig: + openshell.gateway.otlp: + endpoint: http://openshell-collector.observability.svc.cluster.local:4317 + openshell.gateway.otlp.agent: + enabled: true diff --git a/deploy/helm/openshell/ci/values-skaffold.yaml b/deploy/helm/openshell/ci/values-skaffold.yaml index 1ff2bb2fc4..76e0167e79 100644 --- a/deploy/helm/openshell/ci/values-skaffold.yaml +++ b/deploy/helm/openshell/ci/values-skaffold.yaml @@ -5,16 +5,10 @@ gatewayConfig: openshell.drivers.kubernetes: image_pull_policy: if_not_present - openshell.gateway.otlp: - endpoint: http://openshell-collector.observability.svc.cluster.local:4317 - openshell.gateway.otlp.agent: - enabled: true openshell.gateway.auth: allow_unauthenticated_users: true server: - otlp: - endpoint: http://openshell-collector.observability.svc.cluster.local:4317 # Comment out to enforce mTLS (uses PKI secrets generated by pkiInitJob). disableTls: true auth: diff --git a/deploy/helm/openshell/skaffold.yaml b/deploy/helm/openshell/skaffold.yaml index 3ad4d3b8bf..f505c479fa 100644 --- a/deploy/helm/openshell/skaffold.yaml +++ b/deploy/helm/openshell/skaffold.yaml @@ -124,6 +124,7 @@ deploy: valuesFiles: - values.yaml - ci/values-skaffold.yaml + - ci/values-local-tracing.yaml # Add ci/values-cert-manager.yaml here (and uncomment the cert-manager # release above) to switch TLS generation to cert-manager. #- ci/values-cert-manager.yaml diff --git a/deploy/helm/openshell/templates/_gateway-workload.tpl b/deploy/helm/openshell/templates/_gateway-workload.tpl index 6b73241e6c..b0814a40b8 100644 --- a/deploy/helm/openshell/templates/_gateway-workload.tpl +++ b/deploy/helm/openshell/templates/_gateway-workload.tpl @@ -358,6 +358,9 @@ spec: {{- end }} - name: gateway-state emptyDir: {} + {{- with .Values.priorityClassName }} + priorityClassName: {{ . }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 4 }} @@ -370,4 +373,8 @@ spec: tolerations: {{- toYaml . | nindent 4 }} {{- end }} + {{- with .Values.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 4 }} + {{- end }} {{- end }} diff --git a/deploy/helm/openshell/tests/gateway_scheduling_test.yaml b/deploy/helm/openshell/tests/gateway_scheduling_test.yaml new file mode 100644 index 0000000000..3a1a755d02 --- /dev/null +++ b/deploy/helm/openshell/tests/gateway_scheduling_test.yaml @@ -0,0 +1,107 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +suite: gateway scheduling fields +templates: + - templates/gateway-config.yaml + - templates/statefulset.yaml + - templates/deployment.yaml +release: + name: openshell + namespace: my-namespace + +tests: + - it: omits scheduling overrides by default on the Deployment + template: templates/deployment.yaml + set: + workload.kind: deployment + server.externalDbSecret: openshell-db + asserts: + - notExists: + path: spec.template.spec.priorityClassName + - notExists: + path: spec.template.spec.topologySpreadConstraints + + # priorityClassName — shared pod template, so both workload shapes + - it: omits priorityClassName by default on the StatefulSet + template: templates/statefulset.yaml + asserts: + - notExists: + path: spec.template.spec.priorityClassName + + - it: sets priorityClassName on the StatefulSet when configured + template: templates/statefulset.yaml + set: + priorityClassName: system-cluster-critical + asserts: + - equal: + path: spec.template.spec.priorityClassName + value: system-cluster-critical + - contains: + path: spec.template.spec.volumes + content: + name: gateway-state + emptyDir: {} + + - it: sets priorityClassName on the Deployment when configured + template: templates/deployment.yaml + set: + workload.kind: deployment + server.externalDbSecret: openshell-db + priorityClassName: system-cluster-critical + asserts: + - equal: + path: spec.template.spec.priorityClassName + value: system-cluster-critical + + # topologySpreadConstraints — shared pod template, so both workload shapes + - it: omits topologySpreadConstraints by default on the StatefulSet + template: templates/statefulset.yaml + asserts: + - notExists: + path: spec.template.spec.topologySpreadConstraints + + - it: sets topologySpreadConstraints on the StatefulSet when configured + template: templates/statefulset.yaml + set: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app.kubernetes.io/name: openshell + app.kubernetes.io/instance: openshell + asserts: + - equal: + path: spec.template.spec.topologySpreadConstraints[0].maxSkew + value: 1 + - equal: + path: spec.template.spec.topologySpreadConstraints[0].topologyKey + value: topology.kubernetes.io/zone + - equal: + path: spec.template.spec.topologySpreadConstraints[0].whenUnsatisfiable + value: DoNotSchedule + - equal: + path: spec.template.spec.topologySpreadConstraints[0].labelSelector.matchLabels + value: + app.kubernetes.io/name: openshell + app.kubernetes.io/instance: openshell + + - it: sets topologySpreadConstraints on the Deployment when configured + template: templates/deployment.yaml + set: + workload.kind: deployment + server.externalDbSecret: openshell-db + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app.kubernetes.io/name: openshell + app.kubernetes.io/instance: openshell + asserts: + - equal: + path: spec.template.spec.topologySpreadConstraints[0].topologyKey + value: kubernetes.io/hostname diff --git a/deploy/helm/openshell/values.yaml b/deploy/helm/openshell/values.yaml index 0bbb2cd6b2..987744afbb 100644 --- a/deploy/helm/openshell/values.yaml +++ b/deploy/helm/openshell/values.yaml @@ -389,6 +389,15 @@ tolerations: [] # -- Affinity rules for the gateway pod. affinity: {} +# -- PriorityClass for the gateway pod. The gateway is a control-plane +# component; when it shares nodes with the workloads it serves, the default +# priority gives it no advantage under node pressure. Empty = omit the field. +priorityClassName: "" + +# -- Topology spread constraints for the gateway pod, used to spread replicas +# across zones or nodes. Empty = omit the field. +topologySpreadConstraints: [] + # -- Non-secret gateway application configuration. Top-level keys name TOML # tables and are rendered into the mounted gateway.toml file. Kubernetes # resource inputs remain outside this map; template expressions derive the diff --git a/docs/how-it-works/gateways/configuration.mdx b/docs/how-it-works/gateways/configuration.mdx index 175836ccb4..8d39d6bbaf 100644 --- a/docs/how-it-works/gateways/configuration.mdx +++ b/docs/how-it-works/gateways/configuration.mdx @@ -610,7 +610,7 @@ that the previous process has stopped, not that sharing a SQLite file enables HA ## Database Pool -`[openshell.gateway] database_max_connections` sets the connection ceiling for the gateway's database pool. One pool is shared by every database-backed RPC, so this value is also the gateway's ceiling on concurrent database work. When the pool is saturated, callers queue on connection acquisition and sqlx logs that the time to acquire a connection exceeded its slow threshold. The background readiness monitor draws from the same pool, so sustained saturation can push its database ping past the check timeout and flip `/readyz` to not-ready while the gateway is otherwise serving. +`[openshell.gateway] database_max_connections` sets the connection ceiling for the gateway's database pool. One pool is shared by every database-backed RPC, so this value is also the gateway's ceiling on concurrent database work. When the pool is saturated, callers queue on connection acquisition and sqlx logs that the time to acquire a connection exceeded its slow threshold. The background readiness monitor draws from the same pool, so sustained saturation can push its database ping past the check timeout and flip `/readyz` to not-ready while the gateway is otherwise serving. On Postgres, a caller that gets no connection within 5 seconds fails instead of waiting for the database to answer, usually with a retryable `UNAVAILABLE` error whose reason is `DATABASE_UNAVAILABLE`. Omit the key to use the backend default, which is unchanged from earlier releases: @@ -810,6 +810,7 @@ Each OCSF event becomes one log record: | Timestamp | The OCSF `time`. The observed timestamp is when the gateway received the event. | | Severity | Unknown, Informational, and Other are INFO, Low is WARN, Medium is WARN2, High is ERROR, Critical is ERROR2, and Fatal is FATAL. The severity text is the OCSF severity name. | | Attributes | `ocsf.class_uid`, `ocsf.activity_id`, `ocsf.severity_id`, `ocsf.metadata.uid`, and, for events associated with a sandbox, `openshell.sandbox.id`. | +| Trace context | The trace and span ID of the sampled span active when the event was emitted, for every OCSF class. Absent when no sampled span was active. | `schema_version` in this table is deprecated and ignored; the gateway logs a warning at startup when it is set. diff --git a/docs/how-it-works/policies/network-rules.mdx b/docs/how-it-works/policies/network-rules.mdx index a3d4f5245c..119818a395 100644 --- a/docs/how-it-works/policies/network-rules.mdx +++ b/docs/how-it-works/policies/network-rules.mdx @@ -343,6 +343,10 @@ request, which the read-only preset denies. Run `npm install --no-audit`, or replace `access: read-only` with explicit rules that also allow the audit's `POST` requests. An endpoint cannot combine `access` and `rules`. +To set the option on a live sandbox without replacing the policy, add +`allow-encoded-slash` to the `--add-endpoint` options segment of +`openshell policy update`, which requires a `rest` or `websocket` endpoint. + Verify that a scoped package lookup succeeds: ```shell @@ -408,6 +412,11 @@ instead of broadening the range when an address changes. ### Allow WebSocket Messages +REST endpoints refuse requests carrying an `Upgrade` header with `403 Forbidden` +in both `enforce` and `audit` mode. To migrate a REST endpoint that previously +relayed WebSocket traffic, configure a separate `protocol: websocket` endpoint +on its own path or port with explicit upgrade and message rules. + Use `protocol: websocket` to control WebSocket connections and the messages that clients send over them. This template requires `/usr/bin/node` and a WebSocket service you control. It permits the `/v1/realtime` upgrade and client @@ -443,8 +452,9 @@ frames or messages from the server. If the endpoint carries provider credentials, OpenShell closes the connection with close code `1008` when the client sends a binary frame, because it cannot check the frame for credentials. Set `allow_uninspected_credentials: true` to relay binary frames anyway. Set -`websocket_credential_rewrite: true` only when client text messages contain -OpenShell credential placeholders that must be resolved. +`websocket_credential_rewrite: true` when client text messages contain credential +placeholders that must be resolved. This explicitly authorizes WebSocket text +rewriting for ordinary static credentials, including credentials with HTTP placement metadata. When new network rules take effect, OpenShell closes open WebSocket connections, so the client must reconnect. The client might receive close code `1012`, or the diff --git a/docs/how-it-works/policies/schema.mdx b/docs/how-it-works/policies/schema.mdx index 693efb209c..67d9090e60 100644 --- a/docs/how-it-works/policies/schema.mdx +++ b/docs/how-it-works/policies/schema.mdx @@ -55,6 +55,11 @@ These defaults and the filesystem path restrictions below also apply when the su Each path must be absolute, must not contain `..`, and must not exceed 4096 bytes. `read_write` cannot contain `/`. A policy can list at most 256 paths. +Policy paths can follow system symlinks only when the workload cannot modify +any containing directory. A symlink beneath a directory the workload owns or +can write causes launch to fail, including in `best_effort` mode. List the +target directory directly for links inside a writable workspace. + ```yaml showLineNumbers={false} filesystem_policy: include_workdir: true @@ -69,8 +74,8 @@ filesystem_policy: | `compatibility` | string | `best_effort` | `best_effort` or `hard_requirement` | Both values skip individual paths that are missing or that the workload cannot -open. They differ when none of the listed paths can be applied, or when Landlock -fails to enforce the policy's rules: +open. If none of the listed paths can be applied, launch fails in either mode. +They differ when Landlock fails to build or enforce the policy's rules: | Value | Behavior | |---|---| @@ -153,7 +158,7 @@ the gateway host. | Field | Type | Default | Description | |---|---|---|---| -| `protocol` | string | None | `rest`, `websocket`, `graphql`, `mcp`, or `json-rpc` for request inspection, or `tcp` for a native TCP connection. `graphql`, `mcp`, and `json-rpc` endpoints refuse requests that carry an `Upgrade` header with `403`. Refer to [Connection and Request Checks](/how-it-works/policies/network-rules#connection-and-request-checks). | +| `protocol` | string | None | `rest`, `websocket`, `graphql`, `mcp`, or `json-rpc` for request inspection, or `tcp` for a native TCP connection. `rest`, `graphql`, `mcp`, and `json-rpc` endpoints refuse requests that carry an `Upgrade` header with `403`. Refer to [Connection and Request Checks](/how-it-works/policies/network-rules#connection-and-request-checks). | | `tls` | string | Automatic | `skip` relays traffic without terminating TLS, so OpenShell cannot inspect it. Do not use it with a request protocol. | | `enforcement` | string | `audit` | `enforce` blocks requests that break the endpoint's rules. `audit` logs them and allows the request. | | `access` | string | None | Access preset: `read-only`, `read-write`, or `full`. Refer to [Access Presets](#access-presets). | @@ -166,8 +171,8 @@ the gateway host. | Field | Type | Default | Description | |---|---|---|---| | `credential_binding.provider` | string | None | Binds the static credentials of an attached provider whose profile defines no endpoints. Valid only in a sandbox policy. | -| `request_body_credential_rewrite` | bool | `false` | Replaces credential placeholders in REST request bodies. | -| `websocket_credential_rewrite` | bool | `false` | Replaces credential placeholders in client WebSocket text messages. | +| `request_body_credential_rewrite` | bool | `false` | Replaces ordinary static credential placeholders in REST bodies, including those with declared HTTP placement. | +| `websocket_credential_rewrite` | bool | `false` | Replaces ordinary static credential placeholders in client WebSocket text messages, including those with declared HTTP placement. Requires `protocol: websocket`. | | `allow_uninspected_credentials` | bool | `false` | Allows a provider-credentialed endpoint to omit request inspection or use `tls: skip`. | | `credential_signing` | string | None | AWS request signing: `sigv4`, `sigv4:body`, or `sigv4:no_body`. | | `signing_service` | string | None | AWS service name, such as `bedrock` or `s3`. Required with `credential_signing`. | diff --git a/docs/how-it-works/providers/overview.mdx b/docs/how-it-works/providers/overview.mdx index 73f209738e..aba6269546 100644 --- a/docs/how-it-works/providers/overview.mdx +++ b/docs/how-it-works/providers/overview.mdx @@ -409,6 +409,12 @@ and `openshell sandbox provider detach`. See +Provider attachment checks credential inspection against the latest saved sandbox +policy, falling back to the creation policy only when no revision exists. If a +current rule permits uninspected traffic to a credentialed endpoint, configure +L7 inspection or remove the rule before attaching the provider. Use +`allow_uninspected_credentials: true` only for an intentional exception. + ### Auto-Discovery Shortcut Name a provider with `--provider`. If no provider by that name exists and the @@ -436,15 +442,19 @@ startup, OpenShell replaces each credential with an opaque placeholder token in the agent's environment. When the agent sends an HTTP request containing a placeholder, the proxy resolves it immediately before forwarding the request. -Static credential resolution has two independent authorization boundaries: +Static credential resolution has three independent authorization boundaries: 1. Network policy must allow the calling binary and request destination. 2. The credential binding must include the request host, port, and path. Profile endpoints provide the binding by default. An endpointless profile can use a sandbox policy endpoint that names the attached provider instance through `credential_binding.provider`. +3. The request location must match the declared header, query parameter, or + path-template slot, or an explicitly enabled body/WebSocket rewrite policy. + Without placement metadata, static credentials resolve only in `Authorization` + or through those explicit rewrite policies. -Both checks must pass. A provider profile endpoint does not grant network access +All three checks must pass. A provider profile endpoint does not grant network access unless provider policy composition or the sandbox's own policy allows the request. A plain sandbox policy endpoint does not grant credential use unless the profile already covers it or the endpoint explicitly binds an endpointless @@ -461,21 +471,30 @@ for endpoint matching and migration guidance. ### Supported injection locations -The proxy resolves credential placeholders in the following parts of an HTTP request: +The proxy automatically enforces declared placement when the gateway and +supervisor support it. Moving a header credential into a query parameter, path, +or unrelated header is denied before forwarding. A supporting supervisor defaults +to `Authorization` when placement metadata is absent and rejects malformed +metadata. Custom placements require a supporting gateway and an explicit profile +declaration. Older supervisors keep their previous behavior. | Location | How the agent uses it | Example | |---|---|---| -| Header value | Agent reads `$API_KEY` from env and places it in a header. | `Authorization: Bearer ` | +| Declared header value | Agent reads `$API_KEY` from env and places it in the declared header. | `Authorization: Bearer ` | | Header value (Basic auth) | Agent base64-encodes `user:` in an `Authorization: Basic` header. The proxy decodes, resolves, and re-encodes. | `Authorization: Basic ` | -| Query parameter value | Agent places the placeholder in a URL query parameter. | `GET /api?key=` | -| URL path segment | Agent builds a URL with the placeholder in the path. Supports concatenated patterns. | `POST /bot/sendMessage` | -| Supported request body | An inspected REST endpoint opts in with `request_body_credential_rewrite: true`. | `{"api_key":""}` | -| WebSocket text message | A REST or WebSocket endpoint opts in with `websocket_credential_rewrite: true`. | `{"token":""}` | +| Declared query parameter value | Agent uses the profile's `query_param`. | `GET /api?key=` | +| Declared URL path slot | Agent uses the profile's `path_template`, including concatenated patterns. | `POST /bot/sendMessage` | | AWS SigV4 signing | An endpoint configures `credential_signing`, and the proxy signs with the endpoint-bound AWS credentials. | `credential_signing: sigv4` | The proxy does not rewrite cookies, response content, unsupported request bodies, or WebSocket binary frames. +Explicit endpoint policies can enable credential rewriting in request bodies +or WebSocket text messages. WebSocket text message rewriting requires +`protocol: websocket`. These flags do not authorize arbitrary query +parameters, path slots, or unrelated headers. See [Profiles](/how-it-works/providers/profiles#understand-static-credential-endpoint-binding) +for placement configuration and upgrade requirements. + ### Clients that parse JWT claims A trusted launcher can wrap an issued placeholder in a JWT-shaped alias when a diff --git a/docs/how-it-works/providers/profiles.mdx b/docs/how-it-works/providers/profiles.mdx index 1617e00afe..359b59c486 100644 --- a/docs/how-it-works/providers/profiles.mdx +++ b/docs/how-it-works/providers/profiles.mdx @@ -70,6 +70,7 @@ A request can use a static credential only when all of these checks pass: | The placeholder belongs to the current attached provider state. | Sandbox provider attachment and current provider record. | | The calling binary and destination are allowed. | Effective sandbox network policy. | | The host, port, and canonical request path match the credential binding. | Provider profile endpoints, or an explicit sandbox policy binding for an endpointless profile. | +| The request location matches declared placement, the default `Authorization` header, or an explicit body/WebSocket rewrite policy. | The credential's `auth_style` and its header, query parameter, or path template. | | The HTTP method, path, or protocol operation is allowed when L7 inspection is configured. | Effective sandbox network policy. | | The credential has not expired. | Provider credential expiry metadata. | @@ -94,6 +95,52 @@ glob matching. OpenShell removes the query string and uses a canonical, secret-redacted path for this check, so a credential embedded in a request path does not need to be revealed before authorization. +The supervisor automatically enforces usable static credential auth metadata. +No additional profile flag is required. Header credentials resolve only in the +declared header, query credentials only in the declared parameter, and path +credentials only in the `{credential}` slot of their path template. Header names +are case-insensitive. An `Authorization` binding accepts Bearer, token, and Basic +authentication. + +The credential's `auth_style` selects its placement. Fields for other styles are +ignored, so a bearer credential with unused `query_param` or `path_template` +fields still resolves only in its declared header. An invalid selected placement +is rejected. + +Existing endpoint policies that enable request-body or WebSocket text rewriting +continue to authorize those locations. Those flags do not authorize arbitrary +query parameters, URL paths, or unrelated headers. Trusted AWS signing retains +its existing endpoint authorization. + +Without placement metadata, the supervisor resolves static credentials only in +the `Authorization` header, or through the explicit body/WebSocket policies above. +It rejects malformed placement metadata. A client using `x-api-key`, another +header, a query parameter, or a path slot needs an explicit credential declaration. +For example: + +```yaml showLineNumbers={false} +credentials: + - name: api_key + env_vars: [API_KEY] + auth_style: header + header_name: x-api-key +``` + + +Upgrade the gateway before the supervisor when clients use locations other than +`Authorization`. An older gateway does not forward placement metadata, so a new +supervisor applies the `Authorization` default even if the imported profile +declares another location. Older supervisors keep their previous behavior. + + +Export and update existing imported profiles with the required placement before +upgrading the supervisor. Updating example YAML files does not change profiles +already stored on the gateway. The Codex reference profile declares its access +token and account header; OAuth tokens sent in request bodies still require the +endpoint's explicit body rewrite policy. Trusted AWS signing does not require an +HTTP placement declaration. Credential delivery, wire identities, placeholder +lifecycles, and update behavior remain unchanged. + Use an explicit sandbox policy binding when the profile intentionally defines credentials without defining service endpoints. The policy names the concrete provider instance, not the profile type: @@ -202,7 +249,6 @@ The following provider profile design items are not part of the current behavior | Roadmap item | Current behavior | |---|---| -| General profile-driven credential placement | Static `auth_style`, `header_name`, `query_param`, and `path_template` placement metadata is stored and validated, but static credential injection still depends on environment placeholders generated from provider credentials. Dynamic `token_grant` credentials support `bearer` and `header` placement for matching HTTP endpoints. | | Binary-scoped credential injection | Static placeholder resolution is endpoint-scoped. Dynamic grants in single-route REST and multi-route inspection also require admission by their owning endpoint's policy, including its binary restrictions. L4-only credential forwarding retains endpoint-selector matching. | | Credential verification on create | `openshell provider create` does not yet probe provider verification endpoints or expose `--no-verify`. | | Automatic credential scope extraction | OpenShell does not yet inspect upstream provider responses to discover credential scopes. | @@ -372,12 +418,11 @@ credentials: stable_placeholder: false # Accepted values: basic, bearer, header, query, path. - # These fields describe static credential placement. - # Static runtime injection still uses env placeholder resolution. + # These fields restrict where a static placeholder can resolve. auth_style: bearer header_name: authorization - query_param: api_key - path_template: /v1/{credential}/resources + # For auth_style: query, set query_param: api_key instead. + # For auth_style: path, set path_template: /v1/{credential}/resources instead. refresh: # Accepted values: diff --git a/docs/kubernetes/setup.mdx b/docs/kubernetes/setup.mdx index bbc223e32f..a053a8f709 100644 --- a/docs/kubernetes/setup.mdx +++ b/docs/kubernetes/setup.mdx @@ -300,6 +300,8 @@ The most commonly changed values are: | `global.image.registry` / `global.image.tag` / `global.image.pullPolicy` | Shared registry, tag, and pull policy for the gateway, supervisor, and trusted sandbox runtime. Individual image settings take precedence. | | `gateway.image.registry` / `gateway.image.repository` / `gateway.image.tag` / `gateway.image.digest` | Gateway container image. Defaults to `ghcr.io/nvidia/openshell/gateway:latest`. | | `replicaCount` | Number of gateway replicas. Values above `1` require shared PostgreSQL through `server.externalDbSecret`. | +| `priorityClassName` | Existing Kubernetes PriorityClass for gateway pods. Empty by default. | +| `topologySpreadConstraints` | Kubernetes topology spread constraints for gateway replicas. Empty by default. Set selectors to match the gateway pod labels. | | `workload.kind` | Gateway workload controller. Use `statefulset` for SQLite or `deployment` with `server.externalDbSecret`. | | `workload.allowMultiReplicaStatefulSet` | Allow `replicaCount > 1` with `workload.kind=statefulset`. Prefer Deployment for external database-backed multi-replica gateways. | | `autoscaling.enabled` / `autoscaling.minReplicas` / `autoscaling.maxReplicas` | Render a HorizontalPodAutoscaler instead of a fixed `replicaCount`. Refer to [High Availability](/kubernetes/high-availability#scale-the-gateway). | @@ -566,6 +568,8 @@ The gateway exposes `/healthz` for process liveness and `/readyz` for dependency - `startupProbe` and `livenessProbe` use `/healthz`. - `readinessProbe` uses `/readyz`, which reflects the latest result of an in-process background database check. +A gateway that starts while its database is unreachable keeps retrying the connection. Meanwhile `/healthz` succeeds and `/readyz` reports not ready, so Kubernetes keeps the pod without routing traffic to it. + Each sandbox supervisor Pod uses a `tcpSocket` readiness probe on port 5501. The supervisor accepts connections on that port only while its gateway session is ready. If you add a default-deny ingress policy to sandbox namespaces, allow TCP port 5501 from the node so kubelet can reach supervisor Pods. ## Next Steps diff --git a/docs/observability/accessing-logs.mdx b/docs/observability/accessing-logs.mdx index 9e19c637bf..e671f48558 100644 --- a/docs/observability/accessing-logs.mdx +++ b/docs/observability/accessing-logs.mdx @@ -76,6 +76,10 @@ On reconnect, a client passes the highest cursor it processed as the resume poin The gateway merges the log and platform event sources before emitting, so events normally arrive in ascending cursor order. That applies to the buffered tail you receive when the stream opens, to a replay after a resume, and to live delivery. The gateway does not delay an event to wait for a lower cursor that has not been published yet, so a cursor can still arrive late under concurrent publication. Track the highest cursor seen as the resume point rather than the last one received. +A cursor is one position shared by the log and platform sources; you track a single highest value, not one per source. If you request both logs and platform events, either source can deliver fewer events on connect than its replay depth asked for. This happens when the other source's replay leaves out an event that is newer than part of this source's history, even when both depths are equal. The gateway withholds those events rather than hand out a cursor that one of the two sources cannot fully vouch for, and reports what it withheld with a warning event rather than dropping it silently. Watches that follow only logs, such as `openshell logs`, are not affected. + +Replay depths bound history only. An event published while the stream is opening is always part of the initial batch, so the batch can hold more events than requested. If the gateway's buffer evicts such an event before the batch is read, the stream reports it with a warning instead. + ## Direct Filesystem Access The supervisor writes shorthand logs to `/var/log/openshell.YYYY-MM-DD.log` and, when enabled, OCSF JSONL to `/var/log/openshell-ocsf.YYYY-MM-DD.jsonl` in its own filesystem. `openshell sandbox exec` runs in the workload and does not give access to the supervisor's files. diff --git a/docs/observability/ocsf-json-export.mdx b/docs/observability/ocsf-json-export.mdx index e5cc5e363b..2f4b52c10f 100644 --- a/docs/observability/ocsf-json-export.mdx +++ b/docs/observability/ocsf-json-export.mdx @@ -286,6 +286,37 @@ And a denied connection, produced when `curl https://github.com` runs in a sandb The JSON examples above are formatted for readability. The actual JSONL file contains one JSON object per line with no whitespace formatting. +## Refusal Records + +Every record that describes a refusal sets `action` to `Denied`. On Network and HTTP Activity, `disposition` is `Blocked`, except for FORWARD requests with an unsupported scheme, which report `Rejected`. Besides policy denials, this includes: + +- FORWARD requests the proxy rejects as unparsable, non-canonical, or invalid GraphQL or JSON-RPC. +- L7 evaluation errors under `enforcement: enforce`. Under `enforcement: audit` they report `action` Other and `disposition` Allowed, because the request proceeds. +- Google Cloud metadata server emulation responses with status 403 or 405. +- Fail-closed request, response, and WebSocket middleware, including the HTTP Activity record for a fail-closed response stage and the Detection Findings for a fail-closed stage or chain or a failed post-middleware policy evaluation. +- Response delivery the supervisor refuses because middleware stopped it, its preflight failed, or the upstream response head could not be normalized for it. + +Count these records when you track denials, for example with `select(.action == "Denied")`. + +## Follow a Deny Trace + +HTTP Activity and API Activity records emitted inside a sampled OpenTelemetry span carry `trace.uid` and list `trace` in `metadata.profiles`. These are the classes OCSF 1.8.0 declares the Trace profile for. The UID is the W3C trace ID, 32 lowercase hexadecimal characters. Search for it in your Jaeger or Tempo backend. A sampled ID does not guarantee export or retention in the trace backend. + +```json +{ + "metadata": { "profiles": ["container", "host", "network_proxy", "security_control", "trace"] }, + "trace": { "uid": "0af7651916cd43dd8448eb211c80319c" } +} +``` + +OTLP log export links every OCSF record, whatever its class, to the active sampled span through the log record's own trace and span ID fields. Use those fields for log-to-trace links in your backend. + +With OTLP trace export enabled on a dedicated supervisor, denied HTTP requests can create an INFO `supervisor.egress.deny` span around the audit records for that decision. The span records the policy, reason, and destination when available. At the default log level, every deny span is a root span. At debug level, FORWARD policy, destination, credential-binding, and request middleware denials can join the available connect trace and its authorization, resolve, and dial spans. Other deny spans, including L7 denials inside CONNECT tunnels and response middleware denials, remain root spans. Shared supervisor hosts do not initialize OTLP exporters, so their records carry no trace correlation. Workload `traceparent` headers never supply supervisor correlation. + +To bound telemetry volume when a workload retries blocked destinations, the supervisor creates at most 10 deny spans per destination at once and one more per second after that. A process-wide budget also allows at most 100 spans at once and refills at 100 per second across destinations. Rate-limited decisions create no additional deny span, while all audit records are still emitted. Records can retain correlation to an already active sampled span, including a connect span at debug level. + +Gateway records and their OTLP trace context can inherit the caller's incoming `traceparent`, including before authentication succeeds. The caller also decides whether they are correlated at all, because an unsampled `traceparent` suppresses `trace.uid`. Treat gateway trace IDs as correlation hints, not evidence of the caller's identity, and do not treat a missing `trace.uid` as meaningful. + ## OCSF Event Classes in JSON The `class_uid` field identifies the event type: diff --git a/docs/observability/traces.mdx b/docs/observability/traces.mdx index 0cf8ca3c27..3cbdba70a3 100644 --- a/docs/observability/traces.mdx +++ b/docs/observability/traces.mdx @@ -71,6 +71,8 @@ Give each installation that reports to the same collector its own name, includin The gateway continues a valid W3C `traceparent` sent by the caller, so a client's trace can include the gateway's spans for its requests. Without one, the gateway starts a new trace. +The CLI forwards `TRACEPARENT` and `TRACESTATE` from its environment on gateway requests from its standard commands. A CI pipeline instrumented with an OpenTelemetry integration that exports these variables (for example the Jenkins OpenTelemetry plugin or an OpenTelemetry GitHub Action) therefore parents the gateway's spans for the commands it runs, with no collector or provider configuration on the CI runner. + The gateway also passes its trace context to compute drivers, so driver spans appear in the same trace as the gateway request that caused them. Built-in drivers export their spans through the gateway's process. Managed external drivers receive the trace endpoint, gateway name, and compute driver from the gateway. Drivers that you run yourself own their telemetry configuration. ## Next Steps diff --git a/docs/sdk/api-errors.mdx b/docs/sdk/api-errors.mdx index 5d45369e68..7f11c9ea45 100644 --- a/docs/sdk/api-errors.mdx +++ b/docs/sdk/api-errors.mdx @@ -31,6 +31,7 @@ Recognized gateway reasons include the following. | `INVALID_ARGUMENT` | `INVALID_ARGUMENT` | Correct the fields listed in `BadRequest`. | | `RESOURCE_VERSION_CONFLICT` | `ABORTED` | Read the resource again and construct a new conditional write. `metadata.recovery` is `REFRESH_STATE`; `current_resource_version` is included when known. | | `PROFILE_SOURCE_UNAVAILABLE` | `UNAVAILABLE` | Retry a profile snapshot read after at least the supplied delay. | +| `DATABASE_UNAVAILABLE` | `UNAVAILABLE` | The gateway could not get a database connection within 5 seconds. Retry after at least the supplied delay. | | `MUTATION_LOCK_TIMEOUT` | `UNAVAILABLE` | The request waited more than 10 seconds for a concurrent mutation to release a sandbox, workspace, or gateway-wide lock. Retry after at least the supplied delay. A request that carried a `request_id` leaves its admission unresolved, like any other error, so a retry with that ID returns `REQUEST_OUTCOME_UNCERTAIN`. Observe resource state and reconcile effects before you start a new request with a new ID. | | `REQUEST_ID_PAYLOAD_MISMATCH` | `FAILED_PRECONDITION` | Keep the original payload for that request ID. Inspect the original operation before submitting a different one. | | `REQUEST_OUTCOME_UNCERTAIN` | `FAILED_PRECONDITION` | An attempt is admitted but has no confirmed replayable success. Observe resource state and reconcile effects. Do not switch to a new ID to bypass the admission. | diff --git a/e2e/policy-advisor/README.md b/e2e/policy-advisor/README.md index c5c4e79ca0..adb700306f 100644 --- a/e2e/policy-advisor/README.md +++ b/e2e/policy-advisor/README.md @@ -69,7 +69,7 @@ test runner: nix run .#openshell-test-runner -- \ --environment ubuntu-docker-rootful --installer deb \ --family conformance --package conformance \ - --filter 'binary_id(=conformance::policy_advisor)' + --filter 'test(/^policy_advisor::/)' ``` The GitHub write test above and the regressions below still exercise distinct diff --git a/e2e/python/test_sandbox_providers.py b/e2e/python/test_sandbox_providers.py index 4c39bae95f..6619c4b465 100644 --- a/e2e/python/test_sandbox_providers.py +++ b/e2e/python/test_sandbox_providers.py @@ -236,6 +236,8 @@ def _reply(self): self.send_response(200) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(encoded))) + separator = "&" if "?" in self.path else "?" + self.send_header("Link", f"<{self.path}{separator}page=2>; rel=next") self.end_headers() self.wfile.write(encoded) @@ -725,6 +727,163 @@ def call_native_openai(host: str, port: int) -> str: assert body["model"] == "fixture-openai-model" +@pytest.mark.parametrize( + "placement", + ["authorization", "missing", "header", "query", "path", "unused-fields"], +) +def test_static_header_credentials_reject_reflective_request_locations( + sandbox: Callable[..., Sandbox], + sandbox_client: SandboxClient, + placement: str, +) -> None: + """Missing metadata restricts substitution to Authorization and explicit body rewriting.""" + stub = sandbox_client._stub + profile_id = f"e2e-placement-{time.time_ns()}" + secret = "synthetic-placement-secret" + profile = _native_inference_profile( + profile_id=profile_id, + env_var="API_TOKEN", + port=0, + rules=[sandbox_pb2.L7Rule(allow=sandbox_pb2.L7Allow(method="*", path="/**"))], + ) + credential = profile.credentials[0] + if placement == "missing": + credential.auth_style = "" + credential.header_name = "" + elif placement == "unused-fields": + credential.auth_style = "bearer" + credential.header_name = "Authorization" + credential.query_param = "api_key" + credential.path_template = "/v1/{credential}/resources" + elif placement in {"header", "query", "path"}: + credential.auth_style = placement + credential.header_name = "x-api-key" if placement == "header" else "" + credential.query_param = "api_key" if placement == "query" else "" + credential.path_template = ( + "/credential/{credential}/search" if placement == "path" else "" + ) + profile.endpoints[0].request_body_credential_rewrite = True + + def probe_locations(host: str, port: int, placement: str) -> str: + import json + import os + import urllib.error + import urllib.parse + import urllib.request + + token = os.environ["API_TOKEN"] + root = f"http://{host}:{port}" + query_key = "api_key" if placement == "unused-fields" else "probe" + encoded_token = urllib.parse.quote(token, safe="") + token_path = ( + f"/v1/{encoded_token}/resources" + if placement == "unused-fields" + else f"/{encoded_token}/search" + ) + requests = [ + urllib.request.Request( + root + + "/search?" + + urllib.parse.urlencode( + {"q": "rust", "per_page": "1", query_key: token} + ) + ), + urllib.request.Request( + root + "/search", headers={"X-Other-Credential": token} + ), + urllib.request.Request(root + token_path), + urllib.request.Request( + root + "/search", + data=urllib.parse.urlencode({"token": token}).encode(), + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ), + ] + results = [] + for request in requests: + try: + with urllib.request.urlopen(request, timeout=30) as response: + results.append( + { + "status": response.status, + "body": response.read().decode(), + "link": response.headers.get("Link"), + } + ) + except urllib.error.HTTPError as error: + results.append( + { + "status": error.code, + "body": error.read().decode(), + "link": error.headers.get("Link"), + } + ) + target = "/search?q=rust&per_page=1" + headers = {"Authorization": f"token {token}"} + if placement == "header": + headers = {"x-api-key": token} + elif placement == "query": + target += "&" + urllib.parse.urlencode({"api_key": token}) + headers = {} + elif placement == "path": + target = ( + "/credential/" + + urllib.parse.quote(token, safe="") + + "/search?q=rust&per_page=1" + ) + headers = {} + request = urllib.request.Request(root + target, headers=headers) + with urllib.request.urlopen(request, timeout=30) as response: + valid = { + "body": json.loads(response.read()), + "link": response.headers.get("Link"), + } + return json.dumps({"denied": results, "valid": valid}) + + with native_endpoint_server() as port: + profile.endpoints[0].port = port + with ( + imported_provider_profile( + stub, profile=profile, source=f"{profile_id}.yaml" + ), + provider( + stub, + name=f"{profile_id}-provider", + provider_type=profile_id, + credentials={"API_TOKEN": secret}, + profile_workspace="default", + ) as attached_provider, + ): + spec = datamodel_pb2.SandboxSpec( + policy=_default_policy(), providers=[attached_provider] + ) + with sandbox(spec=spec, delete_on_exit=True) as sb: + result = sb.exec_python( + probe_locations, + args=("host.openshell.internal", port, placement), + timeout_seconds=180, + ) + assert result.exit_code == 0, result.stderr + payload = json.loads(result.stdout) + for result in payload["denied"][:3]: + assert result["status"] == 403, result + assert "credential_placement_mismatch" in result["body"] + assert secret not in result["body"] + assert result["link"] is None + body_result = payload["denied"][3] + assert body_result["status"] == 200, body_result + assert secret in body_result["body"] + valid = payload["valid"] + if placement in {"authorization", "missing", "unused-fields"}: + assert valid["body"]["authorization"] == f"token {secret}" + elif placement == "header": + assert valid["body"]["x_api_key"] == secret + else: + assert secret in valid["body"]["path"] + assert "page=2" in valid["link"] + if placement in {"authorization", "missing", "header", "unused-fields"}: + assert secret not in valid["link"] + + def test_imported_anthropic_profile_allows_native_endpoint_with_attached_provider( sandbox: Callable[..., Sandbox], sandbox_client: SandboxClient, diff --git a/e2e/rust/e2e-kubernetes.sh b/e2e/rust/e2e-kubernetes.sh index 9bdcf4e6a2..fc90a827ee 100755 --- a/e2e/rust/e2e-kubernetes.sh +++ b/e2e/rust/e2e-kubernetes.sh @@ -79,7 +79,8 @@ run_conformance() { return 0 fi - e2e_run_openshell_conformance "Kubernetes" + OPENSHELL_CONFORMANCE_JUNIT="${OPENSHELL_CONFORMANCE_JUNIT:-${ROOT}/results/${OPENSHELL_E2E_REPORT_NAME:-e2e-kubernetes}-conformance.xml}" \ + e2e_run_openshell_conformance "Kubernetes" } # `OPENSHELL_E2E_REPORT_NAME` (default `e2e-kubernetes`) names the report diff --git a/e2e/rust/tests/credential_gating.rs b/e2e/rust/tests/credential_gating.rs index bf0d6ae9b5..110e02cb1e 100644 --- a/e2e/rust/tests/credential_gating.rs +++ b/e2e/rust/tests/credential_gating.rs @@ -4,15 +4,20 @@ #![cfg(feature = "e2e")] //! E2E coverage for credentialed endpoint admission and REST body backstops. +//! +//! Each test installs its own provider and profile under a unique name, so the +//! tests can run in parallel. Each test prints its steps to stderr with the +//! elapsed time, so a timeout shows which step was slow. +use std::fmt::Display; use std::io::Write; use std::process::Stdio; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; -use std::time::Duration; +use std::time::{Duration, Instant}; use base64::Engine as _; -use openshell_e2e::harness::binary::openshell_cmd; +use openshell_e2e::harness::binary::{openshell_bin, openshell_cmd}; use openshell_e2e::harness::sandbox::SandboxGuard; use sha1::{Digest, Sha1}; use tempfile::NamedTempFile; @@ -20,8 +25,6 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream}; use tokio::task::JoinHandle; -const PROFILE_ID: &str = "e2e-credential-gating"; -const PROVIDER_NAME: &str = "e2e-credential-gating"; const TEST_HOST: &str = "host.openshell.internal"; const TOKEN_ENV: &str = "E2E_GATING_TOKEN"; const TEST_SECRET: &str = "e2e-gating-secret-value"; @@ -53,21 +56,68 @@ async fn run_cli(args: &[&str]) -> (bool, String) { ) } +/// Prints a test's steps to stderr with the time since the test started. +#[derive(Clone, Copy)] +struct Progress { + scenario: &'static str, + started: Instant, +} + +impl Progress { + fn start(scenario: &'static str) -> Self { + let progress = Self { + scenario, + started: Instant::now(), + }; + progress.step("start"); + progress + } + + fn step(self, message: impl Display) { + eprintln!( + "credential_gating[{}] +{:.1}s {message}", + self.scenario, + self.started.elapsed().as_secs_f64() + ); + } +} + +fn utf8_path(file: &NamedTempFile) -> Result<&str, String> { + file.path() + .to_str() + .ok_or_else(|| format!("{} is not UTF-8", file.path().display())) +} + /// Retry a delete until it takes effect. /// /// The gateway refuses to delete a provider or a profile that a sandbox still /// references, and sandbox teardown drains asynchronously, so a single attempt -/// can be rejected right after a sandbox was removed. -async fn delete_until_gone(args: &[&str]) -> Result<(), String> { +/// can be rejected right after a sandbox was removed. This runs from `Drop`, +/// so it blocks instead of awaiting. +fn delete_until_gone(args: &[&str]) -> Result<(), String> { const ATTEMPTS: u32 = 40; + let binary = openshell_bin(); let mut last_output = String::new(); for _ in 0..ATTEMPTS { - let (deleted, output) = run_cli(args).await; - if deleted || output.to_lowercase().contains("not found") { - return Ok(()); + match std::process::Command::new(&binary) + .args(args) + .stdin(Stdio::null()) + .output() + { + Ok(output) => { + let combined = format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + if output.status.success() || combined.to_lowercase().contains("not found") { + return Ok(()); + } + last_output = combined; + } + Err(error) => last_output = format!("spawn openshell CLI: {error}"), } - last_output = output; - tokio::time::sleep(Duration::from_millis(500)).await; + std::thread::sleep(Duration::from_millis(500)); } Err(format!( "'{}' still failing after {ATTEMPTS} attempts:\n{last_output}", @@ -75,28 +125,97 @@ async fn delete_until_gone(args: &[&str]) -> Result<(), String> { )) } -/// Strict teardown for the install paths: every test resource must be gone -/// before it is recreated, otherwise creation fails with "already exists". -async fn ensure_provider_resources_absent() -> Result<(), String> { - delete_until_gone(&["provider", "delete", PROVIDER_NAME]).await?; - delete_until_gone(&["profile", "delete", PROFILE_ID]).await +/// A provider and the profile it uses, both under one name unique to the +/// test. Dropping it deletes both, also when the test fails. +struct GatingProvider { + name: String, + progress: Progress, } -/// Best-effort teardown. Never fails the test: it also runs on the failure -/// path, where the original assertion is the interesting one. -async fn cleanup_provider_resources() { - if let Err(error) = ensure_provider_resources_absent().await { - eprintln!("provider cleanup did not settle: {error}"); +impl GatingProvider { + async fn install_with_endpoints( + progress: Progress, + rest_port: u16, + websocket_port: u16, + ) -> Result { + let name = unique_name(progress.scenario); + let profile = write_provider_profile(&name, rest_port, websocket_port)?; + Self::install(progress, name, &profile).await + } + + async fn install_endpointless(progress: Progress) -> Result { + let name = unique_name(progress.scenario); + let profile = write_endpointless_provider_profile(&name)?; + Self::install(progress, name, &profile).await } + + async fn install( + progress: Progress, + name: String, + profile: &NamedTempFile, + ) -> Result { + progress.step(format_args!("importing profile {name}")); + let (imported, output) = + run_cli(&["profile", "import", "--file", utf8_path(profile)?]).await; + if !imported { + return Err(format!("profile import failed:\n{output}")); + } + let provider = Self { name, progress }; + progress.step(format_args!("creating provider {}", provider.name)); + let credential = format!("{TOKEN_ENV}={TEST_SECRET}"); + let (created, output) = run_cli(&[ + "provider", + "create", + "--name", + &provider.name, + "--type", + &provider.name, + "--credential", + &credential, + ]) + .await; + if !created { + return Err(format!("provider create failed:\n{output}")); + } + Ok(provider) + } +} + +impl Drop for GatingProvider { + fn drop(&mut self) { + self.progress + .step(format_args!("deleting provider and profile {}", self.name)); + for kind in ["provider", "profile"] { + if let Err(error) = delete_until_gone(&[kind, "delete", &self.name]) { + self.progress + .step(format_args!("{kind} cleanup did not settle: {error}")); + } + } + self.progress.step("cleanup finished"); + } +} + +/// A provider and profile name that does not collide with parallel tests or +/// earlier runs against the same gateway. +fn unique_name(scenario: &str) -> String { + format!( + "e2e-cg-{scenario}-{}-{:08x}", + std::process::id(), + rand::random::() + ) } -fn write_provider_profile(rest_port: u16, websocket_port: u16) -> Result { +fn write_provider_profile( + profile_id: &str, + rest_port: u16, + websocket_port: u16, +) -> Result { let mut file = tempfile::Builder::new() .suffix(".yaml") .tempfile() .map_err(|error| format!("create profile: {error}"))?; let profile = format!( - r"id: {PROFILE_ID} + r"id: {profile_id} display_name: E2E Credential Gating category: other credentials: @@ -127,42 +246,13 @@ binaries: Ok(file) } -async fn install_provider(rest_port: u16, websocket_port: u16) -> Result<(), String> { - ensure_provider_resources_absent().await?; - let profile = write_provider_profile(rest_port, websocket_port)?; - let profile_path = profile - .path() - .to_str() - .ok_or_else(|| "profile path is not UTF-8".to_string())?; - let (imported, output) = run_cli(&["profile", "import", "--file", profile_path]).await; - if !imported { - return Err(format!("profile import failed:\n{output}")); - } - let credential = format!("{TOKEN_ENV}={TEST_SECRET}"); - let (created, output) = run_cli(&[ - "provider", - "create", - "--name", - PROVIDER_NAME, - "--type", - PROFILE_ID, - "--credential", - &credential, - ]) - .await; - if !created { - return Err(format!("provider create failed:\n{output}")); - } - Ok(()) -} - -fn write_endpointless_provider_profile() -> Result { +fn write_endpointless_provider_profile(profile_id: &str) -> Result { let mut file = tempfile::Builder::new() .suffix(".yaml") .tempfile() .map_err(|error| format!("create endpointless profile: {error}"))?; let profile = format!( - r"id: {PROFILE_ID} + r"id: {profile_id} display_name: E2E Endpointless Credential Gating category: other credentials: @@ -184,35 +274,6 @@ binaries: Ok(file) } -async fn install_endpointless_provider() -> Result<(), String> { - ensure_provider_resources_absent().await?; - let profile = write_endpointless_provider_profile()?; - let profile_path = profile - .path() - .to_str() - .ok_or_else(|| "endpointless profile path is not UTF-8".to_string())?; - let (imported, output) = run_cli(&["profile", "import", "--file", profile_path]).await; - if !imported { - return Err(format!("endpointless profile import failed:\n{output}")); - } - let credential = format!("{TOKEN_ENV}={TEST_SECRET}"); - let (created, output) = run_cli(&[ - "provider", - "create", - "--name", - PROVIDER_NAME, - "--type", - PROFILE_ID, - "--credential", - &credential, - ]) - .await; - if !created { - return Err(format!("endpointless provider create failed:\n{output}")); - } - Ok(()) -} - #[derive(Clone, Copy)] enum EndpointMode { L4, @@ -232,6 +293,7 @@ fn write_policy( port: u16, mode: EndpointMode, credential_source: CredentialSource, + provider: &str, ) -> Result { let mut file = NamedTempFile::new().map_err(|error| format!("create policy: {error}"))?; let endpoint_options = match mode { @@ -250,11 +312,11 @@ fn write_policy( let credential_binding = match credential_source { CredentialSource::ProviderProfile => String::new(), CredentialSource::PolicyBinding => { - format!(" credential_binding:\n provider: {PROVIDER_NAME}\n") + format!(" credential_binding:\n provider: {provider}\n") } }; let policy = format!( - r#"version: 1 + r"version: 1 filesystem_policy: include_workdir: true read_only: [/usr, /lib, /proc, /dev/urandom, /app, /etc, /var/log] @@ -274,7 +336,7 @@ network_policies: - path: /usr/bin/python* - path: /usr/local/bin/python* - path: /sandbox/.uv/python/*/bin/python* -"#, +", ); file.write_all(policy.as_bytes()) .map_err(|error| format!("write policy: {error}"))?; @@ -631,7 +693,15 @@ with socket.create_connection((host, port), timeout=10) as sock: ) } -async fn sandbox_create_failure(policy: &NamedTempFile) -> String { +async fn sandbox_create_failure( + progress: Progress, + label: &str, + provider: &str, + policy: &NamedTempFile, +) -> String { + progress.step(format_args!( + "{label}: creating sandbox, expecting rejection" + )); let policy_path = policy.path().to_str().expect("policy path is UTF-8"); let (success, output) = run_cli(&[ "sandbox", @@ -639,54 +709,71 @@ async fn sandbox_create_failure(policy: &NamedTempFile) -> String { "--policy", policy_path, "--provider", - PROVIDER_NAME, + provider, "--", "echo", "must-not-run", ]) .await; assert!(!success, "sandbox create unexpectedly succeeded:\n{output}"); + progress.step(format_args!("{label}: rejected")); output } +/// Create a sandbox, run one command in it, and delete it. +/// +/// Returns the create output followed by the command output. +async fn run_in_sandbox( + progress: Progress, + label: &str, + create_args: &[&str], + command: &[&str], +) -> Result { + progress.step(format_args!("{label}: creating sandbox")); + let mut sandbox = SandboxGuard::create(create_args).await?; + progress.step(format_args!( + "{label}: sandbox {} ready, running command", + sandbox.name + )); + let output = sandbox.exec(command).await; + progress.step(format_args!("{label}: deleting sandbox {}", sandbox.name)); + sandbox.cleanup().await; + progress.step(format_args!("{label}: sandbox deleted")); + Ok(format!("{}{}", sandbox.create_output, output?)) +} + async fn assert_gateway_admission( + progress: Progress, + provider: &str, port: u16, credential_source: CredentialSource, ) -> Result<(), String> { - let l4 = write_policy(port, EndpointMode::L4, credential_source)?; - let l4_error = sandbox_create_failure(&l4).await; + let l4 = write_policy(port, EndpointMode::L4, credential_source, provider)?; + let l4_error = sandbox_create_failure(progress, "L4-only policy", provider, &l4).await; assert!(l4_error.contains("credentialed endpoint"), "{l4_error}"); assert!(l4_error.contains("L4-only"), "{l4_error}"); - let tls_skip = write_policy(port, EndpointMode::TlsSkip, credential_source)?; - let tls_error = sandbox_create_failure(&tls_skip).await; + let tls_skip = write_policy(port, EndpointMode::TlsSkip, credential_source, provider)?; + let tls_error = sandbox_create_failure(progress, "tls: skip policy", provider, &tls_skip).await; assert!(tls_error.contains("credentialed endpoint"), "{tls_error}"); assert!( tls_error.contains("tls:") && tls_error.contains("skip"), "{tls_error}" ); - let opt_in = write_policy(port, EndpointMode::L4OptIn, credential_source)?; - let opt_in_path = opt_in - .path() - .to_str() - .ok_or_else(|| "opt-in policy path is not UTF-8".to_string())?; + let opt_in = write_policy(port, EndpointMode::L4OptIn, credential_source, provider)?; let accepted_marker = match credential_source { CredentialSource::ProviderProfile => "OPT_IN_ACCEPTED", CredentialSource::PolicyBinding => "ENDPOINTLESS_OPT_IN_ACCEPTED", }; - let mut sandbox = SandboxGuard::create(&[ - "--policy", - opt_in_path, - "--provider", - PROVIDER_NAME, - "--", - "echo", - accepted_marker, - ]) + let output = run_in_sandbox( + progress, + "opt-in policy", + &["--policy", utf8_path(&opt_in)?, "--provider", provider], + &["echo", accepted_marker], + ) .await?; - assert!(sandbox.create_output.contains(accepted_marker)); - sandbox.cleanup().await; + assert!(output.contains(accepted_marker), "{output}"); Ok(()) } @@ -712,168 +799,120 @@ async fn wait_for_provider_key_presence( )) } -async fn assert_endpointless_provider_env_live_update(port: u16) -> Result<(), String> { +async fn set_policy(progress: Progress, sandbox: &str, policy_path: &str) -> Result<(), String> { + let (success, output) = run_cli(&[ + "policy", + "set", + sandbox, + "--policy", + policy_path, + "--wait", + "--timeout", + "120", + ]) + .await; + if !success { + return Err(format!("policy update failed:\n{output}")); + } + progress.step("policy applied"); + Ok(()) +} + +async fn assert_endpointless_provider_env_live_update( + progress: Progress, + provider: &str, + port: u16, +) -> Result<(), String> { let unbound = write_policy( port, EndpointMode::RestBody { rewrite: false }, CredentialSource::ProviderProfile, + provider, )?; let bound = write_policy( port, EndpointMode::RestBody { rewrite: false }, CredentialSource::PolicyBinding, + provider, )?; - let unbound_path = unbound - .path() - .to_str() - .ok_or_else(|| "unbound policy path is not UTF-8".to_string())?; - let bound_path = bound - .path() - .to_str() - .ok_or_else(|| "bound policy path is not UTF-8".to_string())?; + let unbound_path = utf8_path(&unbound)?; + let bound_path = utf8_path(&bound)?; + progress.step("creating long-running sandbox with the unbound policy"); let mut sandbox = SandboxGuard::create_keep_with_args( - &[ - "--policy", - unbound_path, - "--provider", - PROVIDER_NAME, - "--no-tty", - ], + &["--policy", unbound_path, "--provider", provider, "--no-tty"], &["sh", "-c", "echo PROVIDER_UPDATE_READY && sleep infinity"], "PROVIDER_UPDATE_READY", ) .await?; let result = async { + progress.step(format_args!( + "sandbox {} ready, waiting for the token to be absent", + sandbox.name + )); wait_for_provider_key_presence(&sandbox, false).await?; - let (success, output) = run_cli(&[ - "policy", - "set", - &sandbox.name, - "--policy", - bound_path, - "--wait", - "--timeout", - "120", - ]) - .await; - if !success { - return Err(format!("binding policy update failed:\n{output}")); - } + progress.step("binding the provider with policy set --wait"); + set_policy(progress, &sandbox.name, bound_path) + .await + .map_err(|error| format!("binding {error}"))?; + progress.step("waiting for the token placeholder"); wait_for_provider_key_presence(&sandbox, true).await?; - let (success, output) = run_cli(&[ - "policy", - "set", - &sandbox.name, - "--policy", - unbound_path, - "--wait", - "--timeout", - "120", - ]) - .await; - if !success { - return Err(format!("unbinding policy update failed:\n{output}")); - } + progress.step("unbinding the provider with policy set --wait"); + set_policy(progress, &sandbox.name, unbound_path) + .await + .map_err(|error| format!("unbinding {error}"))?; + progress.step("waiting for the token to be absent"); wait_for_provider_key_presence(&sandbox, false).await } .await; + progress.step(format_args!("deleting sandbox {}", sandbox.name)); sandbox.cleanup().await; + progress.step("sandbox deleted"); result } async fn run_body_sandbox( + progress: Progress, + provider: &str, port: u16, mode: EndpointMode, credential_source: CredentialSource, ) -> Result { - let policy = write_policy(port, mode, credential_source)?; - let policy_path = policy - .path() - .to_str() - .ok_or_else(|| "body policy path is not UTF-8".to_string())?; - let script = body_client_script(port); - let mut sandbox = SandboxGuard::create(&[ - "--policy", - policy_path, - "--provider", - PROVIDER_NAME, - "--", - "python3", - "-c", - &script, - ]) - .await?; - let output = sandbox.create_output.clone(); - sandbox.cleanup().await; - Ok(output) -} - -async fn run_profile_body_sandbox(port: u16) -> Result { - let policy = write_policy( - port, - EndpointMode::RestBody { rewrite: false }, - CredentialSource::ProviderProfile, - )?; - let policy_path = policy - .path() - .to_str() - .ok_or_else(|| "body policy path is not UTF-8".to_string())?; + let policy = write_policy(port, mode, credential_source, provider)?; let script = body_client_script(port); - let mut sandbox = SandboxGuard::create(&[ - "--policy", - policy_path, - "--provider", - PROVIDER_NAME, - "--", - "python3", - "-c", - &script, - ]) - .await?; - let output = sandbox.create_output.clone(); - sandbox.cleanup().await; - Ok(output) -} - -async fn assert_rest_body_preserves_placeholder(server: &HttpProbeServer) -> Result<(), String> { - let output = run_profile_body_sandbox(server.port).await?; - assert!(output.contains("BODY_TEXT")); - let observations = server.wait_for_observations(1).await; - assert_eq!(observations.len(), 1, "observations: {observations:?}"); - assert!(observations[0].saw_placeholder); - assert!(!observations[0].saw_secret); - Ok(()) + run_in_sandbox( + progress, + "REST body client", + &["--policy", utf8_path(&policy)?, "--provider", provider], + &["python3", "-c", &script], + ) + .await } -async fn assert_websocket_binary_denied(server: &BinaryWebSocketProbeServer) -> Result<(), String> { +async fn assert_websocket_binary_denied( + progress: Progress, + provider: &str, + server: &BinaryWebSocketProbeServer, +) -> Result<(), String> { let policy = write_policy( server.port, EndpointMode::WebSocket, CredentialSource::ProviderProfile, + provider, )?; - let policy_path = policy - .path() - .to_str() - .ok_or_else(|| "WebSocket policy path is not UTF-8".to_string())?; let script = binary_websocket_client_script(server.port); - let mut sandbox = SandboxGuard::create(&[ - "--policy", - policy_path, - "--provider", - PROVIDER_NAME, - "--", - "python3", - "-c", - &script, - ]) + let output = run_in_sandbox( + progress, + "WebSocket client", + &["--policy", utf8_path(&policy)?, "--provider", provider], + &["python3", "-c", &script], + ) .await?; - assert!(sandbox.create_output.contains("BINARY_DENIED")); - sandbox.cleanup().await; + assert!(output.contains("BINARY_DENIED"), "{output}"); assert!( server.wait_for_handshake().await, "upstream should receive the WebSocket handshake" @@ -885,56 +924,134 @@ async fn assert_websocket_binary_denied(server: &BinaryWebSocketProbeServer) -> Ok(()) } -#[tokio::test] -async fn credentialed_endpoint_gates_work_end_to_end() { +/// Start both probe servers and install a provider whose profile declares +/// their endpoints. +async fn profile_endpoint_fixture( + progress: Progress, +) -> (HttpProbeServer, BinaryWebSocketProbeServer, GatingProvider) { let server = HttpProbeServer::start().await.expect("start HTTP probe"); let websocket_server = BinaryWebSocketProbeServer::start() .await .expect("start WebSocket probe"); - install_provider(server.port, websocket_server.port) + let provider = + GatingProvider::install_with_endpoints(progress, server.port, websocket_server.port) + .await + .expect("install credentialed provider"); + (server, websocket_server, provider) +} + +/// Start the HTTP probe server and install a provider whose profile declares +/// no endpoints, so policies must bind it explicitly. +async fn endpointless_fixture(progress: Progress) -> (HttpProbeServer, GatingProvider) { + let server = HttpProbeServer::start().await.expect("start HTTP probe"); + let provider = GatingProvider::install_endpointless(progress) .await - .expect("install credentialed provider"); + .expect("install endpointless provider"); + (server, provider) +} - let result = async { - assert_rest_body_preserves_placeholder(&server).await?; - assert_websocket_binary_denied(&websocket_server).await - } - .await; +#[tokio::test] +async fn profile_rest_endpoint_keeps_body_placeholder() { + let progress = Progress::start("profile-rest"); + let (server, _websocket_server, provider) = profile_endpoint_fixture(progress).await; - cleanup_provider_resources().await; - result.expect("credential gating E2E"); + let output = run_body_sandbox( + progress, + &provider.name, + server.port, + EndpointMode::RestBody { rewrite: false }, + CredentialSource::ProviderProfile, + ) + .await + .expect("run REST body client"); + assert!(output.contains("BODY_TEXT"), "{output}"); + let observations = server.wait_for_observations(1).await; + assert_eq!(observations.len(), 1, "observations: {observations:?}"); + assert!(observations[0].saw_placeholder); + assert!(!observations[0].saw_secret); + progress.step("checks passed"); +} - install_endpointless_provider() +#[tokio::test] +async fn profile_websocket_endpoint_denies_binary_frames() { + let progress = Progress::start("profile-websocket"); + let (_server, websocket_server, provider) = profile_endpoint_fixture(progress).await; + + assert_websocket_binary_denied(progress, &provider.name, &websocket_server) .await - .expect("install endpointless provider"); - let endpointless_result = async { - assert_gateway_admission(server.port, CredentialSource::PolicyBinding).await?; - let literal = run_body_sandbox( - server.port, - EndpointMode::RestBody { rewrite: false }, - CredentialSource::PolicyBinding, - ) - .await?; - assert!(literal.contains("BODY_TEXT")); - let rewritten = run_body_sandbox( - server.port, - EndpointMode::RestBody { rewrite: true }, - CredentialSource::PolicyBinding, - ) - .await?; - assert!(rewritten.contains("BODY_REWRITTEN")); - assert!(!rewritten.contains(TEST_SECRET)); - assert!(!rewritten.contains(PLACEHOLDER_PREFIX)); - let observations = server.wait_for_observations(3).await; - assert_eq!(observations.len(), 3, "observations: {observations:?}"); - assert!(observations[1].saw_placeholder); - assert!(!observations[1].saw_secret); - assert!(!observations[2].saw_placeholder); - assert!(observations[2].saw_secret); - assert_endpointless_provider_env_live_update(server.port).await?; - Ok::<(), String>(()) - } - .await; - cleanup_provider_resources().await; - endpointless_result.expect("endpointless provider credential gating E2E"); + .expect("run WebSocket client"); + progress.step("checks passed"); +} + +#[tokio::test] +async fn policy_binding_requires_inspection_or_opt_in() { + let progress = Progress::start("binding-admission"); + let (server, provider) = endpointless_fixture(progress).await; + + assert_gateway_admission( + progress, + &provider.name, + server.port, + CredentialSource::PolicyBinding, + ) + .await + .expect("gateway admission"); + progress.step("checks passed"); +} + +#[tokio::test] +async fn policy_binding_rest_body_keeps_placeholder_without_rewrite() { + let progress = Progress::start("binding-body"); + let (server, provider) = endpointless_fixture(progress).await; + + let literal = run_body_sandbox( + progress, + &provider.name, + server.port, + EndpointMode::RestBody { rewrite: false }, + CredentialSource::PolicyBinding, + ) + .await + .expect("run REST body client"); + assert!(literal.contains("BODY_TEXT"), "{literal}"); + let observations = server.wait_for_observations(1).await; + assert_eq!(observations.len(), 1, "observations: {observations:?}"); + assert!(observations[0].saw_placeholder); + assert!(!observations[0].saw_secret); + progress.step("checks passed"); +} + +#[tokio::test] +async fn policy_binding_rest_body_rewrite_sends_secret_upstream() { + let progress = Progress::start("binding-rewrite"); + let (server, provider) = endpointless_fixture(progress).await; + + let rewritten = run_body_sandbox( + progress, + &provider.name, + server.port, + EndpointMode::RestBody { rewrite: true }, + CredentialSource::PolicyBinding, + ) + .await + .expect("run REST body client"); + assert!(rewritten.contains("BODY_REWRITTEN")); + assert!(!rewritten.contains(TEST_SECRET)); + assert!(!rewritten.contains(PLACEHOLDER_PREFIX)); + let observations = server.wait_for_observations(1).await; + assert_eq!(observations.len(), 1, "observations: {observations:?}"); + assert!(!observations[0].saw_placeholder); + assert!(observations[0].saw_secret); + progress.step("checks passed"); +} + +#[tokio::test] +async fn policy_binding_updates_provider_environment_live() { + let progress = Progress::start("binding-live-update"); + let (server, provider) = endpointless_fixture(progress).await; + + assert_endpointless_provider_env_live_update(progress, &provider.name, server.port) + .await + .expect("provider environment live update"); + progress.step("checks passed"); } diff --git a/e2e/rust/tests/forward_proxy_l7_bypass.rs b/e2e/rust/tests/forward_proxy_l7_bypass.rs index d726b84721..f274f20e93 100644 --- a/e2e/rust/tests/forward_proxy_l7_bypass.rs +++ b/e2e/rust/tests/forward_proxy_l7_bypass.rs @@ -19,6 +19,13 @@ async fn start_test_server(alias: &str) -> Result { class Handler(BaseHTTPRequestHandler): def do_GET(self): + if self.headers.get("Upgrade"): + self.send_response(101) + self.send_header("Connection", "Upgrade") + self.send_header("Upgrade", "websocket") + self.send_header("Sec-WebSocket-Accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=") + self.end_headers() + return self.send_response(200) self.end_headers() self.wfile.write(b'{"ok":true}') @@ -178,3 +185,55 @@ except Exception as e: guard.create_output ); } + +/// A permitted GET cannot switch a REST endpoint into an uninspected tunnel. +#[tokio::test] +async fn rest_upgrade_is_refused_in_enforce_and_audit_modes() { + let server = start_test_server("rest-upgrade.openshell.test") + .await + .expect("start backend"); + for enforcement in ["enforce", "audit"] { + let source = write_policy_with_l7_rules(&server.host, server.port).unwrap(); + let policy = std::fs::read_to_string(source.path()).unwrap().replace( + "enforcement: enforce", + &format!("enforcement: {enforcement}"), + ); + let mut file = NamedTempFile::new().unwrap(); + file.write_all(policy.as_bytes()).unwrap(); + file.flush().unwrap(); + let policy_path = file.path().to_str().unwrap(); + let script = format!( + r#" +import socket +for target in ["/allowed", "http://{host}:{port}/allowed"]: + s = socket.create_connection(("{host}", {port}), timeout=15) + request = ("GET " + target + " HTTP/1.1\r\nHost: {host}:{port}\r\n" + "Connection: Upgrade\r\nUpgrade: websocket\r\n" + "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n") + s.sendall(request.encode()) + response = b"" + while True: + chunk = s.recv(4096) + if not chunk: + break + response += chunk + s.close() + assert response.startswith(b"HTTP/1.1 403"), response + assert b"unsupported_l7_protocol" in response, response + assert b"REST endpoints" in response, response +print("REST_UPGRADE_REFUSED") +"#, + host = server.host, + port = server.port + ); + let guard = + SandboxGuard::create(&["--policy", policy_path, "--", "python3", "-c", &script]) + .await + .expect("sandbox create"); + assert!( + guard.create_output.contains("REST_UPGRADE_REFUSED"), + "{enforcement}: {}", + guard.create_output + ); + } +} diff --git a/e2e/rust/tests/landlock.rs b/e2e/rust/tests/landlock.rs index ae1e16601f..cfbdcd5846 100644 --- a/e2e/rust/tests/landlock.rs +++ b/e2e/rust/tests/landlock.rs @@ -6,6 +6,7 @@ //! End-to-end coverage for Landlock filesystem enforcement. use std::io::Write; +use std::time::Duration; use openshell_e2e::harness::sandbox::SandboxGuard; use tempfile::NamedTempFile; @@ -13,6 +14,10 @@ use tempfile::NamedTempFile; const SUCCESS_MARKER: &str = "landlock-hard-requirement-ok"; fn write_hard_requirement_policy() -> Result { + write_policy("hard_requirement", "/sandbox") +} + +fn write_policy(compatibility: &str, extra_path: &str) -> Result { let mut file = NamedTempFile::new().map_err(|error| format!("create temporary policy: {error}"))?; let policy = r#"version: 1 @@ -45,6 +50,15 @@ network_policies: - path: "/**" "#; + let policy = policy + .replace( + "compatibility: hard_requirement", + &format!("compatibility: {compatibility}"), + ) + .replace( + " - /sandbox\n", + &format!(" - /sandbox\n - {extra_path}\n"), + ); file.write_all(policy.as_bytes()) .map_err(|error| format!("write temporary policy: {error}"))?; file.flush() @@ -72,3 +86,65 @@ async fn hard_requirement_accepts_enriched_device_path() { assert!(sandbox.create_output.contains(SUCCESS_MARKER)); sandbox.cleanup().await; } + +async fn check_policy_path_replacement(compatibility: &str, replace_ancestor: bool) { + let policy = write_policy(compatibility, "/sandbox/policy-parent/leaf") + .expect("write filesystem policy"); + let policy_path = policy.path().to_string_lossy().into_owned(); + let mut sandbox = SandboxGuard::create(&["--policy", &policy_path]) + .await + .expect("create sandbox with filesystem policy"); + + sandbox + .exec(&["sh", "-c", "mkdir -p /sandbox/policy-parent/leaf"]) + .await + .expect("create declared directory under writable workspace"); + sandbox + .exec(&[ + "sh", + "-c", + "echo allowed > /sandbox/policy-parent/leaf/check", + ]) + .await + .expect("normal directory should remain usable on the next exec"); + + let script = if replace_ancestor { + "mv /sandbox/policy-parent /sandbox/original-parent; ln -s /var/tmp /sandbox/policy-parent" + } else { + "mv /sandbox/policy-parent/leaf /sandbox/original-leaf; ln -s /var/tmp /sandbox/policy-parent/leaf" + }; + sandbox + .exec(&["sh", "-c", script]) + .await + .expect("replace workspace directory"); + let result = tokio::time::timeout(Duration::from_secs(20), sandbox.exec(&["true"])).await; + // Delete through the control plane even when further workload launches fail. + sandbox.cleanup().await; + let error = result + .expect("rejected launch must close its SSH channel promptly") + .expect_err("mutable policy symlink must prevent the next launch"); + assert!( + error.to_lowercase().contains("symlink"), + "unexpected launch error: {error}" + ); +} + +#[tokio::test] +async fn best_effort_rejects_replaced_policy_path() { + check_policy_path_replacement("best_effort", false).await; +} + +#[tokio::test] +async fn hard_requirement_rejects_replaced_policy_path() { + check_policy_path_replacement("hard_requirement", false).await; +} + +#[tokio::test] +async fn best_effort_rejects_replaced_policy_ancestor() { + check_policy_path_replacement("best_effort", true).await; +} + +#[tokio::test] +async fn hard_requirement_rejects_replaced_policy_ancestor() { + check_policy_path_replacement("hard_requirement", true).await; +} diff --git a/e2e/support/conformance.sh b/e2e/support/conformance.sh index 26cb3ed9f6..56ce5a50f1 100644 --- a/e2e/support/conformance.sh +++ b/e2e/support/conformance.sh @@ -16,30 +16,62 @@ e2e_require_openshell_bin() { return 2 fi - # Cargo runs each test from its package directory, so resolve the candidate - # relative to the caller's working directory before passing it to the tests. + # Tests may run from another directory, so resolve the candidate relative + # to the caller's working directory before passing it to them. local binary_dir binary_dir="$(cd "$(dirname "${OPENSHELL_BIN}")" && pwd -P)" || return export OPENSHELL_BIN="${binary_dir}/$(basename "${OPENSHELL_BIN}")" } +# Print the path of the openshell-conformance program: OPENSHELL_CONFORMANCE_BIN +# if set, else a debug build from this checkout. +e2e_openshell_conformance_bin() { + local root=$1 + + if [ -n "${OPENSHELL_CONFORMANCE_BIN:-}" ]; then + if [ ! -x "${OPENSHELL_CONFORMANCE_BIN}" ]; then + echo "ERROR: openshell-conformance is not executable: ${OPENSHELL_CONFORMANCE_BIN}" >&2 + return 2 + fi + echo "${OPENSHELL_CONFORMANCE_BIN}" + return + fi + + # Cargo reports where it put the binary, which depends on the target + # directory and on CARGO_BUILD_TARGET. Diagnostics still go to stderr. + local messages program + messages="$(cargo build \ + --locked \ + --message-format=json-render-diagnostics \ + --manifest-path "${root}/tests/suites/conformance/Cargo.toml" \ + --bin openshell-conformance)" || return + program="$(printf '%s\n' "${messages}" \ + | sed -n 's/.*"executable":"\([^"]*\)".*/\1/p' | tail -n 1)" + if [ -z "${program}" ] || [ ! -x "${program}" ]; then + echo "ERROR: cargo did not report the openshell-conformance binary" >&2 + return 2 + fi + echo "${program}" +} + +# Run the conformance suite with openshell-conformance. The JUnit report goes +# to OPENSHELL_CONFORMANCE_JUNIT, else results/conformance-.xml. e2e_run_openshell_conformance() { local gateway_label=${1:-OpenShell} - local root + local root program junit root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" e2e_require_openshell_bin || return + program="$(e2e_openshell_conformance_bin "${root}")" || return + junit="${OPENSHELL_CONFORMANCE_JUNIT:-${root}/results/conformance-$(echo "${gateway_label}" | tr '[:upper:]' '[:lower:]').xml}" + mkdir -p "$(dirname "${junit}")" || return - # The tests find the gateway as the CLI does: OPENSHELL_GATEWAY_ENDPOINT, + # The program finds the gateway as the CLI does: OPENSHELL_GATEWAY_ENDPOINT, # else OPENSHELL_GATEWAY, else the active gateway. The gateway wrappers set # these for the gateway they started. echo "==> Running conformance suites against the ${gateway_label} gateway" - cargo nextest run \ - --locked \ - --manifest-path "${root}/tests/suites/conformance/Cargo.toml" \ - --test-threads 1 \ - --no-fail-fast \ - --no-tests fail + "${program}" ready || return + "${program}" run --junit "${junit}" } if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then diff --git a/proto/openshell.proto b/proto/openshell.proto index b40ec68263..24c57498b0 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -2144,9 +2144,17 @@ message WatchSandboxRequest { bool follow_events = 4; // Replay the last N log lines (best-effort) before following. + // + // When following both sources, the replay can return fewer lines: events + // older than one the other source's replay left out are withheld and + // reported with a warning, even if both depths are equal. + // + // The depth bounds history only. Events published while the stream is + // opening are always included, so the replay can return more than N. uint32 log_tail_lines = 5; // Replay the last N platform events (best-effort) before following. + // Defaults to 0. Can return fewer or more events; see log_tail_lines. uint32 event_tail = 6; // Stop streaming once the sandbox reaches READY or a terminal result phase @@ -2180,6 +2188,8 @@ message WatchSandboxRequest { // empty resume_after_cursor, because retrying the same token fails // identically. A cursor this server could not have issued is rejected with // INVALID_ARGUMENT. + // + // One cursor covers both log and platform events. string resume_after_cursor = 12; } diff --git a/providers/aws-s3.yaml b/providers/aws-s3.yaml index 89a60a3f30..e85369951f 100644 --- a/providers/aws-s3.yaml +++ b/providers/aws-s3.yaml @@ -32,6 +32,8 @@ id: aws-s3 display_name: AWS S3 description: AWS S3 storage via STS temporary credentials category: other +# These credentials feed trusted SigV4 signing rather than a direct HTTP +# token header, so no bearer/header placement is declared. credentials: - name: access_key_id description: AWS access key ID (gateway-minted via STS) diff --git a/providers/aws.yaml b/providers/aws.yaml index 087b3ac1d4..f3f1cc568b 100644 --- a/providers/aws.yaml +++ b/providers/aws.yaml @@ -29,6 +29,8 @@ id: aws display_name: AWS description: Generic AWS access via STS temporary credentials category: other +# These credentials feed trusted SigV4 signing rather than a direct HTTP +# token header, so no bearer/header placement is declared. credentials: - name: access_key_id description: AWS access key ID (gateway-minted via STS) diff --git a/providers/codex.yaml b/providers/codex.yaml index bafc5bd063..3698a2bfe4 100644 --- a/providers/codex.yaml +++ b/providers/codex.yaml @@ -33,6 +33,10 @@ credentials: description: Codex OAuth access token env_vars: [CODEX_AUTH_ACCESS_TOKEN] required: true + auth_style: bearer + header_name: authorization + # OAuth refresh uses a request body, not an HTTP authorization header. + # Body substitution requires the endpoint's explicit rewrite policy. - name: refresh_token description: Codex OAuth refresh token env_vars: [CODEX_AUTH_REFRESH_TOKEN] @@ -41,6 +45,9 @@ credentials: description: Codex account identifier env_vars: [CODEX_AUTH_ACCOUNT_ID] required: true + auth_style: header + header_name: chatgpt-account-id + # OIDC identity claims are not an API authorization credential. - name: id_token description: Codex OAuth ID token env_vars: [CODEX_AUTH_ID_TOKEN] diff --git a/skills/debug-inference/SKILL.md b/skills/debug-inference/SKILL.md index 50937104a1..8629499476 100644 --- a/skills/debug-inference/SKILL.md +++ b/skills/debug-inference/SKILL.md @@ -103,8 +103,9 @@ inference request using the provider's documented API shape. | A retired managed endpoint fails DNS resolution | Client still uses the removed managed endpoint | Configure the provider's native base URL and attach an endpoint-bearing provider profile | | Direct request is denied | Missing attachment, endpoint policy, HTTP rule, or binary authorization | Inspect the attached provider profile and sandbox effective policy | | `credential_endpoint_mismatch` | Credential profile does not authorize the request recipient | Correct the host/port/path or import a narrowly scoped profile for the intended endpoint | +| `credential_placement_mismatch` | A placeholder is outside declared placement or the metadata-free `Authorization` default | Match the profile placement. For other headers, query parameters, or path slots, update the imported profile and upgrade the gateway before the supervisor; explicit body/WebSocket rewrite policies remain supported. | | `request_authority_mismatch` | HTTP authority differs from the CONNECT destination | Use the same host and effective port in both authorities | -| Credential variable is absent | Provider was not attached when this process launched, or profiles collide on a key | Attach the provider and launch a new process; resolve duplicate keys explicitly | +| Credential variable is absent | Provider was not attached when this process launched, profiles collide on a key, or credential delivery failed | Check provider readiness, then launch a new process; resolve duplicate keys explicitly. Missing placement metadata or an older component alone does not withhold credentials. | | Upstream rejects the model or body | Client relied on removed model/request rewriting | Configure the real model and provider-native request format in the application | | `127.0.0.1` works on the host but not in the sandbox | Loopback refers to different runtime | Use `host.openshell.internal` or another gateway-reachable endpoint and profile | | Host-local request times out | Server bind address, gateway topology, or host firewall blocks container-to-host traffic | Verify the listener and permit only the required gateway network path and port | diff --git a/skills/debug-openshell-cluster/SKILL.md b/skills/debug-openshell-cluster/SKILL.md index ec8c9dee6f..23d58efa25 100644 --- a/skills/debug-openshell-cluster/SKILL.md +++ b/skills/debug-openshell-cluster/SKILL.md @@ -530,6 +530,14 @@ kubectl -n openshell logs -c openshell-gateway --tail=200 `server.telemetryEnabled` renders `OPENSHELL_TELEMETRY_ENABLED` on the gateway pod, and the gateway propagates the effective value to sandbox supervisors. +Gateway pod scheduling accepts top-level `nodeSelector`, `affinity`, +`tolerations`, `priorityClassName`, and `topologySpreadConstraints` values for +both StatefulSet and Deployment workloads. The last two default to unset. +If a gateway pod stays Pending, inspect its scheduling events, confirm the +named PriorityClass exists (`kubectl get priorityclass`), and check that topology +spread selectors match the gateway pod labels and that eligible nodes have the +required topology labels. These values apply only to gateway pods. + #### Additional destination CA ConfigMap When `supervisor.network.additionalCaConfigMapName` is set, Helm must mount the @@ -1582,7 +1590,7 @@ credential failures. | `openshell status` fails | Gateway endpoint unreachable or auth mismatch | `openshell gateway info`, gateway logs | | Collector receives no OCSF log records, or `openshell.ocsf.gap` records | `[openshell.gateway.otlp.ocsf]` missing or without `enabled = true`, an unreachable endpoint or one that does not speak the table's `protocol`, or export failures and queue pressure | Check `[openshell.gateway.otlp.ocsf]` for `enabled = true` and the endpoint it resolves to, the gateway's `OTLP exporting enabled` startup log for `ocsf_endpoint` and `ocsf_protocol`, export warnings, and `openshell_ocsf_otlp_*` metrics. See the published [gateway configuration reference](https://docs.nvidia.com/openshell/latest/how-it-works/gateways/configuration). | | Gateway OCSF JSONL stops growing, has gaps, or is rejected by a SIEM | File errors, queue pressure, or shipper falling behind retention | Inspect gateway warnings and `openshell_ocsf_log_*` metrics; check `[openshell.gateway.ocsf_log]`, directory permissions, free space, per-replica paths, and shipper rotation checkpoints. See the published [gateway configuration reference](https://docs.nvidia.com/openshell/latest/how-it-works/gateways/configuration). | -| `BatchSpanProcessor.ExportError` repeatedly reports connection refused on `127.0.0.1:4317` | The local gateway started with OTLP configured but the collector forwarding task later stopped, or the config was created manually | Restart `gateway:docker`, `gateway:podman`, or `gateway:vm` so it re-detects the listener; inspect the generated `gateway.toml` for `[openshell.gateway.otlp]` | +| `BatchSpanProcessor.ExportError` repeatedly reports connection refused on `127.0.0.1:4317` | The local gateway started with `OPENSHELL_DEV_OTLP_ENDPOINT` set but no collector is listening, for example because `helm:k3s:forward` stopped | Start the collector forwarding again, or restart the gateway task without `OPENSHELL_DEV_OTLP_ENDPOINT`; inspect the generated `gateway.toml` for `[openshell.gateway.otlp]` | | Sandbox log shows `OTLP agent trace relay inactive: not enabled by the gateway` at startup, then `agent OTLP export attempted but the agent trace relay is not enabled` when the agent exports | The gateway does not enable `[openshell.gateway.otlp.agent]`, so the supervisor refuses the workload's OTLP relay address | Set `enabled = true` in `[openshell.gateway.otlp.agent]` (Helm: `gatewayConfig."openshell.gateway.otlp.agent".enabled`) with an endpoint from that table or `[openshell.gateway.otlp]`, and recreate the sandbox; `openshell logs --source sandbox` then shows `OTLP agent trace relay enabled` | | Gateway starts but sandbox create fails | Compute driver cannot reach runtime | Docker/Podman/Kubernetes/VM driver logs | | Docker or Podman sandbox never registers | Wrong gateway endpoint, unavailable host networking, or supervisor startup failure | Gateway logs and supervisor container logs | @@ -1593,6 +1601,8 @@ credential failures. | Kubernetes sandbox pod stuck pending, workspace PVC unbound | Cluster has no default `StorageClass` and OpenShell does not set `storageClassName` on the workspace PVC (clusters with a default `StorageClass` bind fine without it) | `kubectl -n openshell describe pvc`; set `server.workspaceStorageClass` (gateway config `workspace_storage_class`) to a valid `StorageClass` | | Kubernetes gateway pod crash loops | Missing secret, bad DB URL, bad TLS config | `kubectl -n openshell logs deployment/openshell -c openshell-gateway` or `kubectl -n openshell logs statefulset/openshell -c openshell-gateway` | | Mutating RPCs return `UNAVAILABLE` with "timed out waiting for a concurrent mutation" | Advisory-lock contention, a full lock pool on one replica (4 connections unless `database_lock_max_connections` is set), a slow PostgreSQL, or older gateways still running during an upgrade | `openshell_server_mutation_lock_timeouts_total`, `openshell_server_mutation_lock_connections_in_use` against its capacity, the `detail` field of `mutation lock acquisition timed out` in gateway logs, `mutation lock held longer than the lock wait timeout` warnings and `openshell_server_mutation_lock_hold_seconds`, the `pg_locks` query in Step 6 | +| RPCs fail after about 5 seconds with `UNAVAILABLE` `DATABASE_UNAVAILABLE` or "database unavailable: pool timed out" | PostgreSQL unreachable or not answering, or every data-pool connection busy | `database health check` warnings and `/readyz` on the gateway, PostgreSQL logs, `database_max_connections` and the connection sizing in the High Availability guide | +| Kubernetes gateway pod Running but never Ready, logging `database unreachable; retrying` | The gateway started while PostgreSQL was unreachable and waits for it | PostgreSQL pod and Service endpoints, the host and port in the database URL, network policy between the gateway and PostgreSQL | | Mutating RPCs return `INTERNAL` with "could not open a mutation lock connection" | PostgreSQL out of connection slots, restarting, or unreachable | `openshell_server_mutation_lock_errors_total`, PostgreSQL logs, `SELECT count(*) FROM pg_stat_activity` against `max_connections`, `database_max_connections` and `database_lock_max_connections`, the connection sizing in the High Availability guide | | `helm upgrade` fails with an `autoscaling.*` message | HPA values invalid: missing `resources.requests` (or `resources.limits`), `maxReplicas` above 1 without `server.externalDbSecret` (or on a StatefulSet without `workload.allowMultiReplicaStatefulSet`), no metric target, or min/max out of order. "`minReplicas` and `maxReplicas` are not set" means `--reuse-values` kept a release without the chart's autoscaling defaults | Fix the values named in the error; upgrade with `--reset-then-reuse-values` instead of `--reuse-values` | | HPA shows `` targets | No metrics-server for CPU/memory, or the metrics adapter does not serve the custom metric | `kubectl -n openshell describe hpa openshell`, `kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1` | diff --git a/skills/generate-sandbox-policy/SKILL.md b/skills/generate-sandbox-policy/SKILL.md index 80690c45d1..864e9d61f3 100644 --- a/skills/generate-sandbox-policy/SKILL.md +++ b/skills/generate-sandbox-policy/SKILL.md @@ -246,6 +246,7 @@ Only needed for the **Moderate** and **Full** tiers. Translate API path paramete | All sub-paths under `/api/v1/` | `/api/v1/**` | In REST rule paths, `*` stops at `/`, while a whole-segment `**` crosses `/`. +REST endpoints refuse HTTP upgrades in both enforce and audit modes. For WebSocket traffic, use a separate `protocol: websocket` endpoint with explicit upgrade and message rules; `websocket_credential_rewrite` requires that protocol. For example, `/api/v1/**` covers descendants but not `/api/v1` itself; add an explicit rule for that path when needed. `?` matches one character except `/`, and bracket classes such as `[0-9]` are supported. @@ -519,6 +520,10 @@ network_policies: ``` The `filesystem_policy` and `landlock` sections above are sensible defaults. +Use actual target paths for symlinks inside writable workspaces. Policy paths +that traverse symlinks beneath workload-owned or writable directories fail +launch even in `best_effort` mode; system aliases through protected directories +remain supported. A policy with no usable filesystem paths also fails launch. Process identity is omitted so the selected compute driver can choose it. For Docker and Podman, each omitted identity field falls back to the image's OCI `USER`. Tell the user these are defaults and may need adjustment for their diff --git a/skills/openshell-cli/SKILL.md b/skills/openshell-cli/SKILL.md index 351d7e3b84..b757172b7a 100644 --- a/skills/openshell-cli/SKILL.md +++ b/skills/openshell-cli/SKILL.md @@ -150,6 +150,19 @@ when a placeholder is present but requests receive `credential_endpoint_mismatch`. A profileless static provider fails closed because the gateway cannot construct a binding. +Usable credential placement is enforced automatically when both gateway and +supervisor support it. `credential_placement_mismatch` means a placeholder is +outside its declared header, query parameter, or path-template slot. Match the +profile. Explicit body/WebSocket rewrite policies retain their documented +behavior for ordinary credentials. Without placement metadata, a supporting +supervisor permits only `Authorization` and those explicit rewrite policies; +malformed metadata is rejected. For custom headers, query parameters, or path +slots, update the imported profile with explicit placement and upgrade the +gateway before the supervisor. Older gateways cannot forward placement metadata; +older supervisors retain their previous behavior. Updating example YAML does not +update an imported profile. Check provider readiness after changing placement and +launch a new process if retained references were revoked. + When an inspected request receives `request_authority_mismatch`, compare its HTTP authority with the CONNECT tunnel endpoint. The host and effective port must match. For a tunnel to `api.example.com:8443`, send diff --git a/tasks/scripts/check-conformance-program.sh b/tasks/scripts/check-conformance-program.sh new file mode 100755 index 0000000000..6d054405f7 --- /dev/null +++ b/tasks/scripts/check-conformance-program.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Check that `openshell-conformance list` names the same tests that nextest +# lists for the conformance crate, so the program runs the whole suite. +# Neither command needs a gateway. + +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +manifest="${root}/tests/suites/conformance/Cargo.toml" +work="$(mktemp -d)" +trap 'rm -rf "${work}"' EXIT + +cargo run --quiet --locked --manifest-path "${manifest}" --bin openshell-conformance -- list \ + | awk '{print $1}' | sort > "${work}/program" +# --run-ignored all: the program lists ignored tests too, so nextest must as well. +cargo nextest list --locked --color never --run-ignored all --manifest-path "${manifest}" --message-format oneline \ + | awk '{print $2}' | sort > "${work}/nextest" + +if [[ ! -s "${work}/nextest" ]]; then + echo "error: could not read test names from nextest's list; has its oneline format changed?" >&2 + exit 1 +fi +if ! diff -u "${work}/nextest" "${work}/program"; then + echo "error: openshell-conformance list differs from nextest's list (-nextest +program)" >&2 + exit 1 +fi +if [[ ! -s "${work}/program" ]]; then + echo "error: openshell-conformance lists no tests" >&2 + exit 1 +fi +echo "openshell-conformance lists the same $(wc -l < "${work}/program" | tr -d ' ') tests as nextest" diff --git a/tasks/scripts/gateway-docker.sh b/tasks/scripts/gateway-docker.sh index c7e71abfed..5a951eb28d 100644 --- a/tasks/scripts/gateway-docker.sh +++ b/tasks/scripts/gateway-docker.sh @@ -18,6 +18,7 @@ # OPENSHELL_SANDBOX_IMAGE=ghcr.io/... mise run gateway:docker # OPENSHELL_SUPERVISOR_IMAGE=ghcr.io/... mise run gateway:docker # OPENSHELL_SANDBOX_RUNTIME_IMAGE=ghcr.io/... mise run gateway:docker +# OPENSHELL_DEV_OTLP_ENDPOINT=http://127.0.0.1:4317 mise run gateway:docker # # After the gateway is running, point the CLI at it with either: # openshell --gateway docker-dev @@ -83,27 +84,6 @@ ensure_docker_runtime_image() { fi } -append_local_otlp_config_if_available() { - local config_path=$1 - if ! port_is_in_use 4317; then - echo "OTLP collector not detected on 127.0.0.1:4317; trace export, OCSF log export, and the agent trace relay disabled." - return - fi - - cat >>"${config_path}" <<'EOF' - -[openshell.gateway.otlp] -endpoint = "http://127.0.0.1:4317" - -[openshell.gateway.otlp.ocsf] -enabled = true - -[openshell.gateway.otlp.agent] -enabled = true -EOF - echo "OTLP trace export, OCSF log export, and the agent trace relay enabled for http://127.0.0.1:4317." -} - register_gateway_metadata() { local name=$1 local endpoint=$2 @@ -231,7 +211,7 @@ if [[ -n "${OPENSHELL_PROVIDER_SPIFFE_WORKLOAD_API_SOCKET+x}" ]]; then printf 'provider_spiffe_workload_api_socket = "%s"\n' "$(toml_escape "${OPENSHELL_PROVIDER_SPIFFE_WORKLOAD_API_SOCKET}")" >>"${CONFIG_PATH}" fi -append_local_otlp_config_if_available "${CONFIG_PATH}" +append_dev_otlp_config "${CONFIG_PATH}" GATEWAY_ENDPOINT="http://127.0.0.1:${PORT}" register_gateway_metadata "${GATEWAY_NAME}" "${GATEWAY_ENDPOINT}" "${PORT}" diff --git a/tasks/scripts/gateway-podman.sh b/tasks/scripts/gateway-podman.sh index c032cdad80..bb837e464a 100644 --- a/tasks/scripts/gateway-podman.sh +++ b/tasks/scripts/gateway-podman.sh @@ -19,6 +19,7 @@ # OPENSHELL_SANDBOX_IMAGE=ghcr.io/... mise run gateway:podman # OPENSHELL_SUPERVISOR_IMAGE=ghcr.io/... mise run gateway:podman # OPENSHELL_SANDBOX_RUNTIME_IMAGE=ghcr.io/... mise run gateway:podman +# OPENSHELL_DEV_OTLP_ENDPOINT=http://127.0.0.1:4317 mise run gateway:podman set -euo pipefail @@ -112,27 +113,6 @@ port_is_in_use() { (echo >/dev/tcp/127.0.0.1/"${port}") >/dev/null 2>&1 } -append_local_otlp_config_if_available() { - local config_path=$1 - if ! port_is_in_use 4317; then - echo "OTLP collector not detected on 127.0.0.1:4317; trace export, OCSF log export, and the agent trace relay disabled." - return - fi - - cat >>"${config_path}" <<'EOF' - -[openshell.gateway.otlp] -endpoint = "http://127.0.0.1:4317" - -[openshell.gateway.otlp.ocsf] -enabled = true - -[openshell.gateway.otlp.agent] -enabled = true -EOF - echo "OTLP trace export, OCSF log export, and the agent trace relay enabled for http://127.0.0.1:4317." -} - register_gateway_metadata() { local name=$1 local endpoint=$2 @@ -273,7 +253,7 @@ if [[ -n "${OPENSHELL_SANDBOX_PROXY_CA_BUNDLE+x}" ]]; then printf 'proxy_ca_bundle = "%s"\n' "$(toml_escape "${OPENSHELL_SANDBOX_PROXY_CA_BUNDLE}")" >>"${CONFIG_PATH}" fi -append_local_otlp_config_if_available "${CONFIG_PATH}" +append_dev_otlp_config "${CONFIG_PATH}" GATEWAY_ENDPOINT="http://${CLI_ENDPOINT_HOST}:${PORT}" register_gateway_metadata "${GATEWAY_NAME}" "${GATEWAY_ENDPOINT}" "${PORT}" diff --git a/tasks/scripts/gateway-toml.sh b/tasks/scripts/gateway-toml.sh index f9b1e1916b..ce53b639a9 100644 --- a/tasks/scripts/gateway-toml.sh +++ b/tasks/scripts/gateway-toml.sh @@ -17,3 +17,27 @@ toml_escape() { value=${value//$'\t'/\\t} printf '%s' "${value}" } + +# Append OTLP export to a gateway config when OPENSHELL_DEV_OTLP_ENDPOINT is +# set. Export stays off otherwise so no collector is assumed. +append_dev_otlp_config() { + local config_path=$1 + local endpoint=${OPENSHELL_DEV_OTLP_ENDPOINT:-} + if [[ -z "${endpoint}" ]]; then + echo "OTLP export disabled; set OPENSHELL_DEV_OTLP_ENDPOINT to enable it." + return + fi + + cat >>"${config_path}" </dev/tcp/127.0.0.1/"${port}") >/dev/null 2>&1 } -append_local_otlp_config_if_available() { - local config_path=$1 - if ! port_is_in_use 4317; then - echo "OTLP collector not detected on 127.0.0.1:4317; trace export, OCSF log export, and the agent trace relay disabled." - return - fi - - cat >>"${config_path}" <<'EOF' - -[openshell.gateway.otlp] -endpoint = "http://127.0.0.1:4317" - -[openshell.gateway.otlp.ocsf] -enabled = true - -[openshell.gateway.otlp.agent] -enabled = true -EOF - echo "OTLP trace export, OCSF log export, and the agent trace relay enabled for http://127.0.0.1:4317." -} - invoking_user() { if [ -n "${SUDO_USER:-}" ] && [ "${SUDO_USER}" != "root" ]; then printf '%s\n' "${SUDO_USER}" @@ -397,7 +377,7 @@ if [[ -n "${OPENSHELL_PROVIDER_SPIFFE_ALLOW_GUEST_TCP+x}" ]]; then printf 'provider_spiffe_allow_guest_tcp = %s\n' "${OPENSHELL_PROVIDER_SPIFFE_ALLOW_GUEST_TCP}" >>"${CONFIG_PATH}" fi -append_local_otlp_config_if_available "${CONFIG_PATH}" +append_dev_otlp_config "${CONFIG_PATH}" GATEWAY_ENDPOINT="http://127.0.0.1:${PORT}" register_gateway_metadata "${GATEWAY_NAME}" "${GATEWAY_ENDPOINT}" "${PORT}" "${VM_DRIVER_STATE_DIR}" diff --git a/tasks/scripts/gateway.sh b/tasks/scripts/gateway.sh index 3b04ee2684..1fc9d9e9f8 100644 --- a/tasks/scripts/gateway.sh +++ b/tasks/scripts/gateway.sh @@ -18,6 +18,8 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" # shellcheck source=tasks/scripts/gateway-pull-policy.sh source "${ROOT}/tasks/scripts/gateway-pull-policy.sh" +# shellcheck source=tasks/scripts/gateway-toml.sh +source "${ROOT}/tasks/scripts/gateway-toml.sh" GATEWAY_BIN="${OPENSHELL_GATEWAY_BIN:-${ROOT}/target/debug/openshell-gateway}" usage() { @@ -41,6 +43,9 @@ Environment: or ::1 for Podman Machine on macOS. OPENSHELL_SERVER_PORT Gateway port. Defaults to 8080 for Kubernetes, 18080 for Podman/Docker, and 18081 for VM. + OPENSHELL_DEV_OTLP_ENDPOINT + OTLP collector for trace export, OCSF log export, + and the agent trace relay. Unset disables export. Docker, Podman, and VM runs delegate to their gateway: setup scripts. EOF } @@ -262,15 +267,6 @@ name = "${GATEWAY_NAME}" compute_driver = "${DRIVER}" disable_tls = true -[openshell.gateway.otlp] -endpoint = "http://127.0.0.1:4317" - -[openshell.gateway.otlp.ocsf] -enabled = true - -[openshell.gateway.otlp.agent] -enabled = true - [openshell.gateway.auth] allow_unauthenticated_users = true @@ -293,6 +289,8 @@ if [[ -n "${GRPC_ENDPOINT}" ]]; then printf 'grpc_endpoint = "%s"\n' "${GRPC_ENDPOINT}" >>"${CONFIG_PATH}" fi +append_dev_otlp_config "${CONFIG_PATH}" + GATEWAY_ENDPOINT="http://127.0.0.1:${PORT}" register_gateway_metadata "${GATEWAY_NAME}" "${GATEWAY_ENDPOINT}" "${PORT}" diff --git a/tasks/test.toml b/tasks/test.toml index 4309ad95b7..2839c1cf0c 100644 --- a/tasks/test.toml +++ b/tasks/test.toml @@ -134,6 +134,7 @@ run = [ "cargo nextest run --config-file .config/nextest.toml --manifest-path tests/tmachine/Cargo.toml", "cargo nextest run --config-file .config/nextest.toml --manifest-path tests/runner/Cargo.toml", "cargo nextest run --config-file .config/nextest.toml --manifest-path tests/support/Cargo.toml", + "tasks/scripts/check-conformance-program.sh", ] run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-msvc.ps1 test-precommit native" hide = true diff --git a/tests/runner/src/main.rs b/tests/runner/src/main.rs index 1503a4e0d7..9bac397824 100644 --- a/tests/runner/src/main.rs +++ b/tests/runner/src/main.rs @@ -38,7 +38,7 @@ struct Cli { #[arg(long = "package", short = 'p', value_name = "PACKAGE")] packages: Vec, /// A nextest filterset narrowing the tests that run, e.g. - /// `binary(smoke)`. + /// `test(/^smoke::/)`. #[arg(long, value_name = "FILTERSET")] filter: Option, /// The gateway name the installer registers with the CLI. The packaged diff --git a/tests/suites/conformance/Cargo.lock b/tests/suites/conformance/Cargo.lock index f45f1866ee..ea04cd150d 100644 --- a/tests/suites/conformance/Cargo.lock +++ b/tests/suites/conformance/Cargo.lock @@ -54,6 +54,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.104" @@ -382,6 +432,46 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "cmake" version = "0.1.58" @@ -397,6 +487,12 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -679,6 +775,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "escape8259" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f0d310082fd5de539d4f41601182241147498444a7695c520288a0cdbead6" + [[package]] name = "etcetera" version = "0.11.0" @@ -1395,6 +1497,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.14.0" @@ -1641,6 +1749,38 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libtest-mimic" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e6ba06f0ade6e504aff834d7c34298e5155c6baca353cc6a4aaff2f9fd7f33" +dependencies = [ + "anstream", + "anstyle", + "clap", + "escape8259", +] + +[[package]] +name = "linkme" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3045e122bd98aef8ec3ad58ce84f0791f64e70163d1a02710af4aa11a4d54cc5" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77060ebe535362c3da75682cd17b0431017b6e7c5661e714fc69a7ad017d1301" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1758,6 +1898,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "newtype-uuid" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa56da58097c7ae893c4097bb6d91bb5f7fa330053e07e033349db4d5f83ba8" +dependencies = [ + "uuid", +] + [[package]] name = "nix" version = "0.29.0" @@ -1873,6 +2022,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "openshell-bootstrap" version = "0.0.0" @@ -1995,11 +2150,16 @@ dependencies = [ name = "openshell-testkit" version = "0.0.0" dependencies = [ + "clap", "futures", + "libc", + "libtest-mimic", + "linkme", "openshell-bootstrap", "openshell-core", "openshell-sdk", "openshell-testkit-macros", + "quick-junit", "rand 0.9.4", "serde", "serde_json", @@ -2357,6 +2517,30 @@ dependencies = [ "pulldown-cmark", ] +[[package]] +name = "quick-junit" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae23b8e1a59a90f7bf98caa2f4c8c2eedf6df011c6c88ed633e85a87c84526e" +dependencies = [ + "chrono", + "indexmap", + "newtype-uuid", + "quick-xml", + "strip-ansi-escapes", + "thiserror 2.0.20", + "uuid", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.45" @@ -3134,6 +3318,15 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.11.1" @@ -3720,6 +3913,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.23.1" @@ -3743,6 +3942,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + [[package]] name = "want" version = "0.3.1" diff --git a/tests/suites/conformance/Cargo.toml b/tests/suites/conformance/Cargo.toml index 40541d88c4..99274d8dfa 100644 --- a/tests/suites/conformance/Cargo.toml +++ b/tests/suites/conformance/Cargo.toml @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# The conformance family is one crate; each file in `tests/` is a suite. +# The conformance family is one crate; each module in `src/` is a suite. [package] name = "conformance" description = "Conformance suite: sandbox behavior on every compute driver" @@ -12,6 +12,14 @@ license = "Apache-2.0" repository = "https://github.com/NVIDIA/OpenShell" publish = false +# `openshell-conformance`: the suite as one program to run against a gateway. +# No test target of its own: cargo test and nextest run the tests through the +# library. +[[bin]] +name = "openshell-conformance" +path = "src/main.rs" +test = false + [dependencies] openshell-testkit = { path = "../../testkit/testkit" } serde_json = "1" diff --git a/tests/suites/conformance/README.md b/tests/suites/conformance/README.md index ee0502d072..c756107cc1 100644 --- a/tests/suites/conformance/README.md +++ b/tests/suites/conformance/README.md @@ -2,12 +2,14 @@ The `conformance` crate holds the testkit suites for sandbox behavior that every compute driver must provide. The family is this one crate, its own workspace. -Each file in `tests/` is one suite. +It is a library: each module in `src/` is one suite, and `cargo test` and +nextest run the tests as the library's unit tests. The crate also builds the +`openshell-conformance` program, which runs the same tests. | Suite | Covers | |---|---| | `smoke` | Create a sandbox, run `echo` in it, delete it | -| `lifecycle` | SDK: long-running and run-to-completion sandboxes, the default command, restart of a stopped sandbox, deleting a stopped sandbox | +| `lifecycle` | SDK: long-running and run-to-completion sandboxes, prompt delete when the workload is slow to handle TERM, the default command, restart of a stopped sandbox, deleting a stopped sandbox | | `cli` | The same lifecycle through the `openshell` CLI: status, create, get, list, exec, stop, start, delete | | `persistent-workspace` | Files in `/sandbox` survive stop and start | | `policy-advisor` | `policy.local` accepts a proposed rule; a denied address or hostname yields one draft rule | @@ -26,11 +28,30 @@ environments). [TESTING.md](../../../TESTING.md) covers running it against a local Docker gateway and in a tmachine VM, as CI does. +### The `openshell-conformance` program + +`openshell-conformance` runs the suite against a gateway without cargo or +nextest, so a driver author or operator can check their own gateway. It finds +the gateway and the CLI as the tests do. Build it and run it with: + +```shell +cargo build --locked --manifest-path tests/suites/conformance/Cargo.toml --bin openshell-conformance +tests/suites/conformance/target/debug/openshell-conformance list +tests/suites/conformance/target/debug/openshell-conformance ready +tests/suites/conformance/target/debug/openshell-conformance run --junit conformance.xml +``` + +The program runs only this suite: passing it means a gateway meets the +conformance contract below. Feature and driver suites are not part of it. The +[testkit README](../../testkit/README.md#test-programs) describes the commands +and exit codes. `tasks/scripts/check-conformance-program.sh` checks that it +lists the same tests as nextest. + ## Adding a test -Add a `#[testkit::test]` function to a suite, or a new file in `tests/` for a new -area. No playbook, test archive or workflow entry is needed: the runner builds -and runs every test in the crate. +Add a `#[testkit::test]` function to a suite, or for a new area, a new file in +`src/` and a `mod` line for it in `src/lib.rs`. `openshell-conformance` +includes every test in the crate. ## What a test may assert diff --git a/tests/suites/conformance/tests/cli.rs b/tests/suites/conformance/src/cli.rs similarity index 100% rename from tests/suites/conformance/tests/cli.rs rename to tests/suites/conformance/src/cli.rs diff --git a/tests/suites/conformance/tests/file_transfer.rs b/tests/suites/conformance/src/file_transfer.rs similarity index 100% rename from tests/suites/conformance/tests/file_transfer.rs rename to tests/suites/conformance/src/file_transfer.rs diff --git a/tests/suites/conformance/src/lib.rs b/tests/suites/conformance/src/lib.rs new file mode 100644 index 0000000000..00a4d45027 --- /dev/null +++ b/tests/suites/conformance/src/lib.rs @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Conformance suite: sandbox behavior that every compute driver must +//! provide. Each module is one suite. +//! +//! `cargo test` and nextest run the tests as this library's unit tests. +//! `#[testkit::test]` also registers them, so the `openshell-conformance` +//! program in `src/main.rs` runs them by linking this library. + +mod cli; +mod file_transfer; +mod lifecycle; +mod persistent_workspace; +mod policy_advisor; +mod smoke; diff --git a/tests/suites/conformance/tests/lifecycle.rs b/tests/suites/conformance/src/lifecycle.rs similarity index 66% rename from tests/suites/conformance/tests/lifecycle.rs rename to tests/suites/conformance/src/lifecycle.rs index 02e4f6f117..dd47c0b4d6 100644 --- a/tests/suites/conformance/tests/lifecycle.rs +++ b/tests/suites/conformance/src/lifecycle.rs @@ -7,6 +7,8 @@ //! NVIDIA/OpenShell#4629 by Evan Lezar. Tests that need a driver with a //! persistent workspace live in `persistent_workspace.rs`. +use std::time::Duration; + use testkit::Context; use testkit::sdk::{SandboxPhase, SandboxRestartPolicy}; @@ -40,6 +42,76 @@ async fn long_running_exec_delete(cx: &mut Context) -> testkit::Result { cx.expect_absent(&sandbox).await } +/// Long enough that the workload is still handling TERM when sandbox shutdown +/// begins, and below the supervisor's 5s wait before it kills the workload. +const TERM_HANDLING_DELAY: Duration = Duration::from_secs(3); + +/// Well below the 30s termination grace period that a shutdown hang waits out. +const DELETE_DEADLINE: Duration = Duration::from_secs(10); + +/// Many workloads do not exit immediately on TERM, for example while they run +/// cleanup handlers. Deleting such a running sandbox should still complete +/// promptly, rather than waiting out the runtime's termination grace period. +// +// Unignore this when the supervisor half of #5025 lands: on Podman the +// supervisor container still waits out its stop timeout. +#[testkit::test] +#[ignore = "https://github.com/NVIDIA/OpenShell/issues/5025"] +async fn delete_is_prompt_when_workload_is_slow_to_handle_term( + cx: &mut Context, +) -> testkit::Result { + // `sleep & wait` rather than a foreground `sleep`: sh defers traps until a + // foreground child exits, but `wait` returns on the signal. + let script = format!( + "trap 'sleep {}; exit 0' TERM; \ + touch /tmp/delayed-term-ready; \ + while :; do sleep 3600 & wait $!; done", + TERM_HANDLING_DELAY.as_secs() + ); + let sandbox = cx + .sandbox("dt") + .command(["sh", "-c", script.as_str()]) + .create() + .await?; + sandbox.wait_ready().await?; + + // Ready alone does not prove that the workload has installed its trap. + testkit::poll_until( + "workload TERM handler readiness", + Duration::from_secs(10), + Duration::from_millis(100), + async || { + let output = sandbox + .exec(["sh", "-c", "test -f /tmp/delayed-term-ready"]) + .await?; + Ok(if output.exit_code == 0 { + testkit::Poll::Ready(()) + } else { + testkit::Poll::Pending(format!( + "readiness check exited {}; stderr: {}", + output.exit_code, output.stderr + )) + }) + }, + ) + .await?; + + // Use one deadline starting before delete, not a fresh timeout per step. + // Boundary containment may kill the workload before its handler finishes; + // this asserts bounded deletion, not a graceful workload exit. + tokio::time::timeout(DELETE_DEADLINE, async { + sandbox.delete().await?; + cx.expect_absent(&sandbox).await + }) + .await + .map_err(|_| { + testkit::Error::msg(format!( + "sandbox {} was not fully deleted within {DELETE_DEADLINE:?}", + sandbox.name() + )) + })? +} + /// A sandbox whose main command exits 0, with restart policy Never, reaches /// Completed with exit code 0 and is not restarted. It is gone from get and /// list after delete. diff --git a/tests/suites/conformance/src/main.rs b/tests/suites/conformance/src/main.rs new file mode 100644 index 0000000000..7b4e899c60 --- /dev/null +++ b/tests/suites/conformance/src/main.rs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! `openshell-conformance`: the conformance suite as one program, to run +//! against a gateway without cargo or nextest. +//! +//! The tests are registered by `#[testkit::test]` in this crate's library; +//! see `testkit::program` for the commands. + +use std::process::ExitCode; + +// A dependency that is never named is not linked, and its tests would be +// missing. +use conformance as _; + +fn main() -> ExitCode { + testkit::main() +} diff --git a/tests/suites/conformance/tests/persistent_workspace.rs b/tests/suites/conformance/src/persistent_workspace.rs similarity index 100% rename from tests/suites/conformance/tests/persistent_workspace.rs rename to tests/suites/conformance/src/persistent_workspace.rs diff --git a/tests/suites/conformance/tests/policy_advisor.rs b/tests/suites/conformance/src/policy_advisor.rs similarity index 100% rename from tests/suites/conformance/tests/policy_advisor.rs rename to tests/suites/conformance/src/policy_advisor.rs diff --git a/tests/suites/conformance/tests/smoke.rs b/tests/suites/conformance/src/smoke.rs similarity index 100% rename from tests/suites/conformance/tests/smoke.rs rename to tests/suites/conformance/src/smoke.rs diff --git a/tests/suites/drivers/podman/Cargo.lock b/tests/suites/drivers/podman/Cargo.lock index 452ee563cf..6633a3e703 100644 --- a/tests/suites/drivers/podman/Cargo.lock +++ b/tests/suites/drivers/podman/Cargo.lock @@ -54,6 +54,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.104" @@ -382,6 +432,46 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "cmake" version = "0.1.58" @@ -397,6 +487,12 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -678,6 +774,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "escape8259" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f0d310082fd5de539d4f41601182241147498444a7695c520288a0cdbead6" + [[package]] name = "etcetera" version = "0.11.0" @@ -1394,6 +1496,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.14.0" @@ -1640,6 +1748,38 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libtest-mimic" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e6ba06f0ade6e504aff834d7c34298e5155c6baca353cc6a4aaff2f9fd7f33" +dependencies = [ + "anstream", + "anstyle", + "clap", + "escape8259", +] + +[[package]] +name = "linkme" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3045e122bd98aef8ec3ad58ce84f0791f64e70163d1a02710af4aa11a4d54cc5" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77060ebe535362c3da75682cd17b0431017b6e7c5661e714fc69a7ad017d1301" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1757,6 +1897,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "newtype-uuid" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa56da58097c7ae893c4097bb6d91bb5f7fa330053e07e033349db4d5f83ba8" +dependencies = [ + "uuid", +] + [[package]] name = "nix" version = "0.29.0" @@ -1872,6 +2021,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "openshell-bootstrap" version = "0.0.0" @@ -1994,11 +2149,16 @@ dependencies = [ name = "openshell-testkit" version = "0.0.0" dependencies = [ + "clap", "futures", + "libc", + "libtest-mimic", + "linkme", "openshell-bootstrap", "openshell-core", "openshell-sdk", "openshell-testkit-macros", + "quick-junit", "rand 0.9.4", "serde", "serde_json", @@ -2373,6 +2533,30 @@ dependencies = [ "pulldown-cmark", ] +[[package]] +name = "quick-junit" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae23b8e1a59a90f7bf98caa2f4c8c2eedf6df011c6c88ed633e85a87c84526e" +dependencies = [ + "chrono", + "indexmap", + "newtype-uuid", + "quick-xml", + "strip-ansi-escapes", + "thiserror 2.0.20", + "uuid", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.45" @@ -3150,6 +3334,15 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.11.1" @@ -3736,6 +3929,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.23.1" @@ -3759,6 +3958,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + [[package]] name = "want" version = "0.3.1" diff --git a/tests/suites/features/Cargo.lock b/tests/suites/features/Cargo.lock index 4362c9cacd..bb1a1427ee 100644 --- a/tests/suites/features/Cargo.lock +++ b/tests/suites/features/Cargo.lock @@ -54,6 +54,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.104" @@ -382,6 +432,46 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "cmake" version = "0.1.58" @@ -397,6 +487,12 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -678,6 +774,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "escape8259" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f0d310082fd5de539d4f41601182241147498444a7695c520288a0cdbead6" + [[package]] name = "etcetera" version = "0.11.0" @@ -1062,6 +1164,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "host-gateway" +version = "0.0.0" +dependencies = [ + "openshell-testkit", + "tempfile", + "tokio", +] + [[package]] name = "hostname" version = "0.4.2" @@ -1394,6 +1505,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.14.0" @@ -1658,6 +1775,38 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libtest-mimic" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e6ba06f0ade6e504aff834d7c34298e5155c6baca353cc6a4aaff2f9fd7f33" +dependencies = [ + "anstream", + "anstyle", + "clap", + "escape8259", +] + +[[package]] +name = "linkme" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3045e122bd98aef8ec3ad58ce84f0791f64e70163d1a02710af4aa11a4d54cc5" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77060ebe535362c3da75682cd17b0431017b6e7c5661e714fc69a7ad017d1301" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1775,6 +1924,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "newtype-uuid" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa56da58097c7ae893c4097bb6d91bb5f7fa330053e07e033349db4d5f83ba8" +dependencies = [ + "uuid", +] + [[package]] name = "nix" version = "0.29.0" @@ -1900,6 +2058,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "openshell-bootstrap" version = "0.0.0" @@ -2034,11 +2198,16 @@ dependencies = [ name = "openshell-testkit" version = "0.0.0" dependencies = [ + "clap", "futures", + "libc", + "libtest-mimic", + "linkme", "openshell-bootstrap", "openshell-core", "openshell-sdk", "openshell-testkit-macros", + "quick-junit", "rand 0.9.4", "serde", "serde_json", @@ -2396,6 +2565,30 @@ dependencies = [ "pulldown-cmark", ] +[[package]] +name = "quick-junit" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae23b8e1a59a90f7bf98caa2f4c8c2eedf6df011c6c88ed633e85a87c84526e" +dependencies = [ + "chrono", + "indexmap", + "newtype-uuid", + "quick-xml", + "strip-ansi-escapes", + "thiserror 2.0.20", + "uuid", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.45" @@ -3173,6 +3366,15 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.11.1" @@ -3759,6 +3961,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.23.1" @@ -3782,6 +3990,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + [[package]] name = "want" version = "0.3.1" diff --git a/tests/suites/features/Cargo.toml b/tests/suites/features/Cargo.toml index 5912e9176f..af907b4b15 100644 --- a/tests/suites/features/Cargo.toml +++ b/tests/suites/features/Cargo.toml @@ -3,4 +3,4 @@ [workspace] resolver = "2" -members = ["keycloak-provider-refresh", "kubernetes-updates", "oci-image"] +members = ["host-gateway", "keycloak-provider-refresh", "kubernetes-updates", "oci-image"] diff --git a/tests/suites/features/host-gateway/Cargo.toml b/tests/suites/features/host-gateway/Cargo.toml new file mode 100644 index 0000000000..d81b6cafb2 --- /dev/null +++ b/tests/suites/features/host-gateway/Cargo.toml @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "host-gateway" +description = "Features suite: sandboxes reach a host server through host.openshell.internal" +version = "0.0.0" +edition = "2024" +rust-version = "1.94" +license = "Apache-2.0" +repository = "https://github.com/NVIDIA/OpenShell" +publish = false + +[dependencies] +openshell-testkit = { path = "../../../testkit/testkit" } +tempfile = "3" +tokio = { version = "1.43", features = ["io-util", "net", "rt"] } diff --git a/tests/suites/features/host-gateway/tests/host_gateway.rs b/tests/suites/features/host-gateway/tests/host_gateway.rs new file mode 100644 index 0000000000..024b1cd84b --- /dev/null +++ b/tests/suites/features/host-gateway/tests/host_gateway.rs @@ -0,0 +1,209 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! `host.openshell.internal` names the host from inside a sandbox, on every +//! compute driver. +//! +//! The test starts an HTTP server where the tests run, which is the host the +//! gateway runs its sandboxes on, and runs a client in a sandbox from the +//! Python test image ([`testkit::python_image`]) that requests a page from that +//! server through the name. + +use testkit::{Context, Error, Result, ResultExt as _}; +use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::task::JoinHandle; + +/// The image's system Python, which needs nothing beyond its standard library. +/// The policy names its real path, `/usr/bin/python3.*`, because a rule matches +/// the executable behind a symlink. +const PYTHON: &str = "/usr/bin/python3"; + +/// Requests `/` from `host.openshell.internal` on the port in `argv[1]` and +/// prints the response. A failed connection is retried for 20 seconds, so a +/// slow start doesn't fail the test, and reported on stderr. +const CLIENT: &str = r#" +import socket, sys, time + +host, port = "host.openshell.internal", int(sys.argv[1]) +deadline = time.monotonic() + 20 +while True: + try: + conn = socket.create_connection((host, port), timeout=10) + break + except OSError as error: + print(f"connect {host}:{port}: {error}", file=sys.stderr, flush=True) + if time.monotonic() > deadline: + sys.exit(1) + time.sleep(1) +with conn: + conn.sendall(f"GET / HTTP/1.1\r\nHost: {host}:{port}\r\nConnection: close\r\n\r\n".encode()) + response = b"" + while chunk := conn.recv(4096): + response += chunk +print(response.decode(errors="replace")) +"#; + +/// A sandbox from the Python test image resolves `host.openshell.internal` +/// and gets a response from a server the test runs on the host. The default +/// policy denies all network access, so the test passes one that lets only the +/// image's Python reach the server's port. +#[testkit::test] +async fn python_sandbox_reaches_host_server(cx: &mut Context) -> testkit::Result { + let marker = format!("host-gateway-{}", cx.run_id()); + let server = HostServer::start(marker.clone()).await?; + let policy = PolicyFile::new(&policy(server.port))?; + let port = server.port.to_string(); + let image = testkit::python_image(); + + // The client runs through `sandbox exec` in a Ready sandbox, not as the + // main process. A gateway with several replicas (k3s-ha) can refuse to + // attach to a main process that already finished with "sandbox is not + // ready" when the request reaches a replica that doesn't hold the + // sandbox's session. + let name = cx.name("hg"); + cx.track(&name); + cx.cli([ + "sandbox", + "create", + "--name", + &name, + "--from", + &image, + "--policy", + policy.path()?, + "--detach", + "--no-tty", + "--", + "sh", + "-c", + "exec sleep infinity", + ]) + .await? + .success() + .context(format!("create a sandbox from {image}"))?; + cx.adopt(&name).await?.wait_ready().await?; + + let client = cx + .cli([ + "sandbox", "exec", "--name", &name, "--no-tty", "--", PYTHON, "-c", CLIENT, &port, + ]) + .await? + .success() + .context("run the client in the sandbox")?; + testkit::ensure!( + client.stdout.contains(&marker), + "the client did not get the host server's response:\n{}{}", + client.stdout, + client.stderr + ); + Ok(()) +} + +/// A sandbox policy that allows the image's system Python ([`PYTHON`]) to +/// connect to `host.openshell.internal` on `port`. The endpoint names only the +/// host and port, as the policy docs describe, so the driver's address for the +/// name is what makes it reachable, not an `allowed_ips` range. +fn policy(port: u16) -> String { + format!( + r#"version: 1 + +filesystem_policy: + include_workdir: true + read_only: [/usr, /lib, /lib64, /proc, /dev/urandom, /etc] + read_write: [/sandbox, /tmp, /dev/null] +landlock: + compatibility: best_effort + +process: + run_as_user: sandbox + run_as_group: sandbox + +network_policies: + host_server: + name: host_server + endpoints: + - host: host.openshell.internal + port: {port} + binaries: + - path: /usr/bin/python3.* +"# + ) +} + +/// An HTTP server on the host that answers every request with `body`. It +/// stops when dropped. +struct HostServer { + port: u16, + task: JoinHandle<()>, +} + +impl HostServer { + /// Listen on every host address, since drivers map + /// `host.openshell.internal` to different ones. + async fn start(body: String) -> Result { + let listener = TcpListener::bind(("0.0.0.0", 0)) + .await + .map_err(|error| Error::from_error(&error)) + .context("bind the host server")?; + let port = listener + .local_addr() + .map_err(|error| Error::from_error(&error)) + .context("read the host server's address")? + .port(); + let task = tokio::spawn(async move { + while let Ok((stream, _)) = listener.accept().await { + tokio::spawn(respond(stream, body.clone())); + } + }); + Ok(Self { port, task }) + } +} + +impl Drop for HostServer { + fn drop(&mut self) { + self.task.abort(); + } +} + +/// Read one request's headers from `stream` and answer with `body`. +async fn respond(mut stream: TcpStream, body: String) { + let _ = stream.set_nodelay(true); + let mut request = Vec::new(); + let mut buf = [0_u8; 1024]; + while !request.windows(4).any(|window| window == b"\r\n\r\n") { + match stream.read(&mut buf).await { + Ok(read) if read > 0 && request.len() < 64 * 1024 => { + request.extend_from_slice(&buf[..read]); + } + _ => return, + } + } + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ); + let _ = stream.write_all(response.as_bytes()).await; + let _ = stream.shutdown().await; +} + +/// A sandbox policy written to a temporary file for `sandbox create --policy`. +struct PolicyFile(tempfile::NamedTempFile); + +impl PolicyFile { + fn new(contents: &str) -> Result { + let file = tempfile::NamedTempFile::new() + .map_err(|error| Error::from_error(&error)) + .context("create policy file")?; + std::fs::write(file.path(), contents) + .map_err(|error| Error::from_error(&error)) + .context(format!("write {}", file.path().display()))?; + Ok(Self(file)) + } + + fn path(&self) -> Result<&str> { + let path = self.0.path(); + path.to_str() + .ok_or_else(|| Error::msg(format!("{} is not UTF-8", path.display()))) + } +} diff --git a/tests/support/Cargo.lock b/tests/support/Cargo.lock index a7e717b011..f8b591438f 100644 --- a/tests/support/Cargo.lock +++ b/tests/support/Cargo.lock @@ -54,6 +54,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.104" @@ -382,6 +432,46 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "cmake" version = "0.1.58" @@ -397,6 +487,12 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -678,6 +774,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "escape8259" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f0d310082fd5de539d4f41601182241147498444a7695c520288a0cdbead6" + [[package]] name = "etcetera" version = "0.11.0" @@ -1394,6 +1496,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.14.0" @@ -1649,6 +1757,38 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libtest-mimic" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e6ba06f0ade6e504aff834d7c34298e5155c6baca353cc6a4aaff2f9fd7f33" +dependencies = [ + "anstream", + "anstyle", + "clap", + "escape8259", +] + +[[package]] +name = "linkme" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3045e122bd98aef8ec3ad58ce84f0791f64e70163d1a02710af4aa11a4d54cc5" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77060ebe535362c3da75682cd17b0431017b6e7c5661e714fc69a7ad017d1301" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1766,6 +1906,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "newtype-uuid" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa56da58097c7ae893c4097bb6d91bb5f7fa330053e07e033349db4d5f83ba8" +dependencies = [ + "uuid", +] + [[package]] name = "nix" version = "0.29.0" @@ -1881,6 +2030,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "openshell-bootstrap" version = "0.0.0" @@ -2003,11 +2158,16 @@ dependencies = [ name = "openshell-testkit" version = "0.0.0" dependencies = [ + "clap", "futures", + "libc", + "libtest-mimic", + "linkme", "openshell-bootstrap", "openshell-core", "openshell-sdk", "openshell-testkit-macros", + "quick-junit", "rand 0.9.4", "serde", "serde_json", @@ -2373,6 +2533,30 @@ dependencies = [ "pulldown-cmark", ] +[[package]] +name = "quick-junit" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae23b8e1a59a90f7bf98caa2f4c8c2eedf6df011c6c88ed633e85a87c84526e" +dependencies = [ + "chrono", + "indexmap", + "newtype-uuid", + "quick-xml", + "strip-ansi-escapes", + "thiserror 2.0.20", + "uuid", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.45" @@ -3150,6 +3334,15 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.11.1" @@ -3736,6 +3929,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.23.1" @@ -3759,6 +3958,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + [[package]] name = "want" version = "0.3.1" diff --git a/tests/testkit/Cargo.lock b/tests/testkit/Cargo.lock index 01973c00e4..db2e79b6ca 100644 --- a/tests/testkit/Cargo.lock +++ b/tests/testkit/Cargo.lock @@ -54,6 +54,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.104" @@ -382,6 +432,46 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "cmake" version = "0.1.58" @@ -397,6 +487,12 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -669,6 +765,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "escape8259" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f0d310082fd5de539d4f41601182241147498444a7695c520288a0cdbead6" + [[package]] name = "etcetera" version = "0.11.0" @@ -1385,6 +1487,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.14.0" @@ -1631,6 +1739,38 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libtest-mimic" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e6ba06f0ade6e504aff834d7c34298e5155c6baca353cc6a4aaff2f9fd7f33" +dependencies = [ + "anstream", + "anstyle", + "clap", + "escape8259", +] + +[[package]] +name = "linkme" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3045e122bd98aef8ec3ad58ce84f0791f64e70163d1a02710af4aa11a4d54cc5" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77060ebe535362c3da75682cd17b0431017b6e7c5661e714fc69a7ad017d1301" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1748,6 +1888,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "newtype-uuid" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa56da58097c7ae893c4097bb6d91bb5f7fa330053e07e033349db4d5f83ba8" +dependencies = [ + "uuid", +] + [[package]] name = "nix" version = "0.29.0" @@ -1863,6 +2012,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "openshell-bootstrap" version = "0.0.0" @@ -1985,14 +2140,20 @@ dependencies = [ name = "openshell-testkit" version = "0.0.0" dependencies = [ + "clap", "futures", + "libc", + "libtest-mimic", + "linkme", "openshell-bootstrap", "openshell-core", "openshell-sdk", "openshell-testkit-macros", + "quick-junit", "rand 0.9.4", "serde", "serde_json", + "tempfile", "tokio", "tonic", ] @@ -2348,6 +2509,30 @@ dependencies = [ "pulldown-cmark", ] +[[package]] +name = "quick-junit" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae23b8e1a59a90f7bf98caa2f4c8c2eedf6df011c6c88ed633e85a87c84526e" +dependencies = [ + "chrono", + "indexmap", + "newtype-uuid", + "quick-xml", + "strip-ansi-escapes", + "thiserror 2.0.20", + "uuid", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.45" @@ -3125,6 +3310,15 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.11.1" @@ -3711,6 +3905,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.23.1" @@ -3734,6 +3934,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + [[package]] name = "want" version = "0.3.1" diff --git a/tests/testkit/README.md b/tests/testkit/README.md index 1aea174049..c02ad01c25 100644 --- a/tests/testkit/README.md +++ b/tests/testkit/README.md @@ -7,7 +7,7 @@ This is a separate Cargo workspace with its own lockfile: | Crate | Purpose | |---|---| -| `testkit` | Finds and connects to the gateway, creates and cleans up sandboxes, runs the CLI. Also builds `testkit-ready`. | +| `testkit` | Finds and connects to the gateway, creates and cleans up sandboxes, runs the CLI, and runs test programs. Also builds `testkit-ready`. | | `testkit-macros` | The `#[testkit::test]` attribute. | ## Writing a test @@ -75,6 +75,10 @@ fails. A killed test process skips cleanup, so names are unique to the run. compute driver. Driver suites call it first. - `testkit::base_image()` for the image to create sandboxes from or build on: `OPENSHELL_TEST_BASE_IMAGE`, or OpenShell's default sandbox image. +- `testkit::python_image()` for sandboxes that run `python3`, curl, git, or + openssl: `OPENSHELL_TEST_PYTHON_IMAGE`, or `openshell/e2e-python:dev` + (`e2e/python/Dockerfile.workload`). The tmachine installers load it into + every environment; `mise run e2e:workload:build` builds it locally. - `cx.cli([...])` to run the installed CLI against the same gateway, with `success()`, `failure()`, and `json()` on the output. `cx.cli_handle()` returns that CLI for code that can't borrow `cx`, such as a cleanup hook. @@ -123,6 +127,67 @@ In a tmachine VM, the test runner (`tests/runner/`) does the same: it sets `OPENSHELL_BIN` to the installed CLI, then runs the suites with nextest. See [TESTING.md](../../TESTING.md). +## Test programs + +`#[testkit::test]` also registers the test with testkit. A binary whose +`main` calls `testkit::main()` runs every registered test it links, without +cargo or nextest: + +```rust +use conformance as _; + +fn main() -> std::process::ExitCode { + testkit::main() +} +``` + +The program must name each suite crate it runs, as `use conformance as _;` does +above: Rust doesn't link a dependency that is never named, and its tests would +be missing. A program that links no tests exits 2 with an error under every +command. `openshell-conformance`, a binary in the conformance crate, is the +program for the conformance suite; see +[its README](../suites/conformance/README.md). + +| Command | What it does | +|---|---| +| `list [FILTER]...` | Prints each test's name and description. Ignored tests are included and marked. | +| `ready [--timeout SECS]` | Waits for the gateway, like `testkit-ready` (default 60 seconds). Exits 2 if it is not ready in time. | +| `run [FILTER]...` | Runs the tests one at a time, each in its own process. Unix only. | + +A filter selects tests whose name contains it. `run` also takes: + +- `--include-ignored`: runs ignored tests too, instead of reporting them as + skipped. +- `--timeout SECS` (default 900): a test that runs longer gets SIGTERM, then + SIGKILL after `--grace SECS` (default 10). +- `--junit PATH`: writes a JUnit report with one test suite per family. + +Each test runs in its own process group. When the test's process exits, `run` +kills anything left in the group, such as CLI processes the test started. +Interrupting `run` stops the current test the same way and runs no more +tests; a second interrupt skips the grace period. If `run` itself is killed +with SIGKILL, the current test's process group keeps running. In a container +where `run` is PID 1, start it under an init, for example with +`docker run --init`. + +`run` exits 0 when every test passed, 1 when a test failed, timed out, or +crashed, and 2 when tests could not run: no tests matched, a test process +could not start or run its test, or the run was interrupted. + +A test that is stopped does not clean up: sandboxes it created stay on the +gateway. Run against a gateway you can throw away, or delete them by hand. + +Without a subcommand, the program takes libtest's arguments instead. A +`[[test]]` target with `harness = false` therefore also runs under `cargo test` +and nextest. In that mode tests run in one process, one at a time unless +`--test-threads` says otherwise. + +A test's name is its module path without the crate name, then its function +name, as in nextest. Its family, which groups tests in the JUnit report, is +the first directory under the last `suites` directory in the crate's path: +`conformance` for `tests/suites/conformance`, `features` for +`tests/suites/features/`. + ## Checks ```shell diff --git a/tests/testkit/testkit-macros/src/lib.rs b/tests/testkit/testkit-macros/src/lib.rs index fc3dbbdb68..b8a614f3c6 100644 --- a/tests/testkit/testkit-macros/src/lib.rs +++ b/tests/testkit/testkit-macros/src/lib.rs @@ -13,12 +13,28 @@ //! async fn expose_service(cx: &mut Context) -> testkit::Result { /* ... */ } //! ``` //! -//! The attribute expands to a `#[tokio::test]` wrapper with the same name, so -//! the nextest test name is the function name. The original function is nested -//! in the wrapper and passed to `testkit::run`. Other attributes, such as -//! `#[ignore]` or `#[cfg(...)]`, move to the wrapper. `#[should_panic]` is -//! rejected: a testkit test panics on any failure, so it would pass for the -//! wrong reason. Check the expected failure in the body instead. +//! The attribute generates three items: +//! +//! - a hidden `async fn` that runs the original function through +//! `testkit::run`, with the original function nested in it; +//! - under `#[cfg(test)]`, a `#[tokio::test]` wrapper with the same name that +//! calls it, so `cargo test` and nextest run the test like any other and the +//! test name is the function name; +//! - an entry in `testkit`'s test registry, with the test's family, name, +//! description, and `#[ignore]`, which `testkit::main()` runs. +//! +//! Other attributes, such as `#[ignore]` or `#[cfg(...)]`, move to the +//! wrapper. `#[cfg(...)]` also applies to the other two items, and lint +//! attributes (`allow`, `warn`, `deny`, `forbid`) also to the hidden +//! function. `#[should_panic]` is rejected: a testkit test panics on any +//! failure, so it would pass for the wrong reason. Check the expected failure +//! in the body instead. `#[cfg_attr]` and `#[expect]` are rejected too. +//! +//! A test's family is the first directory under `tests/suites/` in its +//! crate's path: `conformance` for `tests/suites/conformance`, `features` for +//! `tests/suites/features/`. + +use std::path::Path; use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; @@ -27,12 +43,17 @@ use syn::{Attribute, Expr, ExprLit, ItemFn, Lit, LitStr, Meta}; #[proc_macro_attribute] pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream { - expand(attr.into(), item.into()) + let family = std::env::var_os("CARGO_MANIFEST_DIR").and_then(|dir| family_of(Path::new(&dir))); + expand(attr.into(), item.into(), family.as_deref()) .unwrap_or_else(syn::Error::into_compile_error) .into() } -fn expand(attr: TokenStream2, item: TokenStream2) -> syn::Result { +fn expand( + attr: TokenStream2, + item: TokenStream2, + family: Option<&str>, +) -> syn::Result { let mut body: ItemFn = syn::parse2(item)?; if !attr.is_empty() { return Err(syn::Error::new_spanned( @@ -58,17 +79,24 @@ fn expand(attr: TokenStream2, item: TokenStream2) -> syn::Result { "`#[testkit::test]` already makes this a test; remove this attribute", )); } - if let Some(attr) = body - .attrs - .iter() - .find(|attr| attr.path().is_ident("should_panic")) - { - return Err(syn::Error::new_spanned( - attr, + for attr in &body.attrs { + let message = if attr.path().is_ident("should_panic") { "testkit tests panic on any failure, so `#[should_panic]` would pass for the \ wrong reason; check the expected failure in the body, for example with \ - `CliOutput::failure` and `testkit::ensure!`", - )); + `CliOutput::failure` and `testkit::ensure!`" + } else if attr.path().is_ident("cfg_attr") { + // The registration reads `#[ignore]` and `#[cfg]` directly, so it + // would not see them inside `cfg_attr`. + "`#[cfg_attr]` is not supported on testkit tests; use `#[cfg]` or `#[ignore]` \ + directly" + } else if attr.path().is_ident("expect") { + // The lint attributes are copied to two functions, and only one + // of them would raise the lint. + "`#[expect]` is not supported on testkit tests; use `#[allow]`" + } else { + continue; + }; + return Err(syn::Error::new_spanned(attr, message)); } let description = doc_comment(&body.attrs); if description.is_empty() { @@ -77,20 +105,46 @@ fn expand(attr: TokenStream2, item: TokenStream2) -> syn::Result { "testkit tests need a doc comment describing the behavior they verify", )); } + let ignore = ignore(&body.attrs)?; let name = body.sig.ident.clone(); let name_str = LitStr::new(&name.to_string(), name.span()); let description = LitStr::new(&description, name.span()); + let family = match family { + Some(family) => quote!(::core::option::Option::Some(#family)), + None => quote!(::core::option::Option::None), + }; + let (ignored, ignore_reason) = match ignore { + None => (false, quote!(::core::option::Option::None)), + Some(None) => (true, quote!(::core::option::Option::None)), + Some(Some(reason)) => (true, quote!(::core::option::Option::Some(#reason))), + }; let vis = body.vis.clone(); let attrs = std::mem::take(&mut body.attrs); + let cfgs: Vec<&Attribute> = attrs + .iter() + .filter(|attr| attr.path().is_ident("cfg")) + .collect(); + // `#[cfg]` and lint attributes such as `#[allow(...)]` must reach the + // body, which is nested in the hidden function. Anything else is for the + // test itself, so goes on the wrapper only. + let body_attrs: Vec<&Attribute> = attrs + .iter() + .filter(|attr| { + ["cfg", "allow", "warn", "deny", "forbid"] + .iter() + .any(|name| attr.path().is_ident(name)) + }) + .collect(); body.vis = syn::Visibility::Inherited; body.sig.ident = format_ident!("body"); + let entry = format_ident!("__testkit_{}", name); Ok(quote! { - #(#attrs)* - #[::testkit::__private::tokio::test(crate = "::testkit::__private::tokio")] - #vis async fn #name() { + #(#body_attrs)* + #[doc(hidden)] + async fn #entry() { #body ::testkit::run( ::testkit::TestInfo { @@ -101,6 +155,30 @@ fn expand(attr: TokenStream2, item: TokenStream2) -> syn::Result { ) .await; } + + #(#attrs)* + #[cfg(test)] + #[::testkit::__private::tokio::test(crate = "::testkit::__private::tokio")] + #vis async fn #name() { + #entry().await; + } + + #(#cfgs)* + const _: () = { + #[::testkit::__private::linkme::distributed_slice(::testkit::__private::TESTS)] + #[linkme(crate = ::testkit::__private::linkme)] + static TEST: ::testkit::Test = ::testkit::Test { + info: ::testkit::TestInfo { + name: #name_str, + description: #description, + }, + family: #family, + module_path: ::core::module_path!(), + ignored: #ignored, + ignore_reason: #ignore_reason, + run: || ::std::boxed::Box::pin(#entry()), + }; + }; }) } @@ -112,6 +190,31 @@ fn is_test_attr(attr: &Attribute) -> bool { .is_some_and(|segment| segment.ident == "test") } +/// `None` if the test is not ignored, else the `#[ignore = "..."]` reason if +/// it has one. +fn ignore(attrs: &[Attribute]) -> syn::Result>> { + let Some(attr) = attrs.iter().find(|attr| attr.path().is_ident("ignore")) else { + return Ok(None); + }; + match &attr.meta { + Meta::Path(_) => Ok(Some(None)), + Meta::NameValue(meta) => match &meta.value { + Expr::Lit(ExprLit { + lit: Lit::Str(reason), + .. + }) => Ok(Some(Some(reason.clone()))), + value => Err(syn::Error::new_spanned( + value, + "the `#[ignore]` reason must be a string literal", + )), + }, + Meta::List(_) => Err(syn::Error::new_spanned( + attr, + "use `#[ignore]` or `#[ignore = \"reason\"]`", + )), + } +} + /// The doc comment, joined into one line. Empty if there is none. fn doc_comment(attrs: &[Attribute]) -> String { attrs @@ -131,21 +234,40 @@ fn doc_comment(attrs: &[Attribute]) -> String { .join(" ") } +/// The family of a suite crate in `manifest_dir`: the first directory under +/// the nearest enclosing `suites/`. +fn family_of(manifest_dir: &Path) -> Option { + let components: Vec<_> = manifest_dir + .components() + .filter_map(|component| component.as_os_str().to_str()) + .collect(); + let suites = components + .iter() + .rposition(|component| *component == "suites")?; + components + .get(suites + 1) + .map(|family| (*family).to_string()) +} + #[cfg(test)] mod tests { - use super::expand; + use std::path::Path; + + use super::{expand, family_of}; + + const OK_FN: &str = "async fn t(cx: &mut Context) -> testkit::Result { Ok(()) }"; // A suite only compiles correct tests, so it never reaches these errors. + // `testkit/tests/macro.rs` checks what correct tests expand to. #[test] fn rejects_misuse() { - let ok_fn = "async fn t(cx: &mut Context) -> testkit::Result { Ok(()) }"; for (attr, item, error) in [ ( "id = \"t-1\"", - format!("/// Doc.\n{ok_fn}"), + format!("/// Doc.\n{OK_FN}"), "takes no arguments", ), - ("", ok_fn.to_string(), "doc comment"), + ("", OK_FN.to_string(), "doc comment"), ( "", "/// Doc.\nfn t(cx: &mut Context) -> testkit::Result { Ok(()) }".to_string(), @@ -158,17 +280,53 @@ mod tests { ), ( "", - format!("/// Doc.\n#[tokio::test]\n{ok_fn}"), + format!("/// Doc.\n#[tokio::test]\n{OK_FN}"), "already makes this a test", ), ( "", - format!("/// Doc.\n#[should_panic]\n{ok_fn}"), + format!("/// Doc.\n#[should_panic]\n{OK_FN}"), "`#[should_panic]`", ), + ( + "", + format!("/// Doc.\n#[ignore(later)]\n{OK_FN}"), + "`#[ignore]` or", + ), + ( + "", + format!("/// Doc.\n#[ignore = 1]\n{OK_FN}"), + "string literal", + ), + ( + "", + format!("/// Doc.\n#[cfg_attr(unix, ignore)]\n{OK_FN}"), + "`#[cfg_attr]` is not supported", + ), + ( + "", + format!("/// Doc.\n#[expect(unused)]\n{OK_FN}"), + "`#[expect]` is not supported", + ), ] { - let err = expand(attr.parse().unwrap(), item.parse().unwrap()).unwrap_err(); + let err = expand(attr.parse().unwrap(), item.parse().unwrap(), None).unwrap_err(); assert!(err.to_string().contains(error), "{item}: {err}"); } } + + #[test] + fn family_is_the_directory_under_suites() { + for (dir, family) in [ + ("/repo/tests/suites/conformance", Some("conformance")), + ( + "/repo/tests/suites/features/provider-refresh/keycloak", + Some("features"), + ), + ("/a/suites/x/tests/suites/drivers/podman", Some("drivers")), + ("/repo/tests/testkit/testkit", None), + ("/repo/tests/suites", None), + ] { + assert_eq!(family_of(Path::new(dir)).as_deref(), family, "{dir}"); + } + } } diff --git a/tests/testkit/testkit/Cargo.toml b/tests/testkit/testkit/Cargo.toml index b8570fbe2c..47ff56c13c 100644 --- a/tests/testkit/testkit/Cargo.toml +++ b/tests/testkit/testkit/Cargo.toml @@ -19,11 +19,25 @@ openshell-bootstrap = { path = "../../../crates/openshell-bootstrap" } openshell-core = { path = "../../../crates/openshell-core", default-features = false } openshell-sdk = { path = "../../../crates/openshell-sdk" } openshell-testkit-macros = { path = "../testkit-macros" } +clap = { version = "4", features = ["derive"] } futures = "0.3" +libtest-mimic = "0.8" +linkme = "0.3" +quick-junit = "0.8" rand = "0.9" serde = "1" serde_json = "1" tokio = { version = "1.43", features = ["macros", "process", "rt", "time"] } +[target.'cfg(unix)'.dependencies] +libc = "0.2" + [dev-dependencies] +tempfile = "3" tonic = "0.14" + +# A test program, run by `cargo test` and nextest through `testkit::main`'s +# libtest interface, that checks the program's own subcommands. +[[test]] +name = "program" +harness = false diff --git a/tests/testkit/testkit/src/bin/testkit-ready.rs b/tests/testkit/testkit/src/bin/testkit-ready.rs index 4f41b3bed4..03db9d3547 100644 --- a/tests/testkit/testkit/src/bin/testkit-ready.rs +++ b/tests/testkit/testkit/src/bin/testkit-ready.rs @@ -6,13 +6,12 @@ //! Usage: `testkit-ready [TIMEOUT_SECS]` (default 60), with the gateway //! selected as for tests. Run it once per environment before the test //! binaries; exits non-zero with the last error if the gateway is not healthy -//! in time, or if the CLI does not reach it. +//! in time, or if the CLI does not reach it. A test program's `ready` +//! subcommand does the same. use std::process::ExitCode; use std::time::Duration; -use testkit::{Cli, Gateway}; - const DEFAULT_TIMEOUT_SECS: u64 = 60; const USAGE: &str = "usage: testkit-ready [TIMEOUT_SECS] (default 60)"; @@ -32,25 +31,9 @@ async fn main() -> ExitCode { } }, }; - let result = async { - let gateway = Gateway::from_env()?; - eprintln!("testkit-ready: {}", gateway.describe()); - let health = gateway.wait_ready(Duration::from_secs(timeout)).await?; - Cli::from_env(&gateway).check_connected().await?; - testkit::Result::Ok(health) - } - .await; - match result { - Ok(health) => { - eprintln!( - "testkit-ready: gateway {} is healthy and the CLI reaches it", - health.version - ); - ExitCode::SUCCESS - } - Err(error) => { - eprintln!("testkit-ready: {error}"); - ExitCode::FAILURE - } + if testkit::check_ready("testkit-ready", Duration::from_secs(timeout)).await { + ExitCode::SUCCESS + } else { + ExitCode::FAILURE } } diff --git a/tests/testkit/testkit/src/cli.rs b/tests/testkit/testkit/src/cli.rs index abba6065c3..5ac54e4108 100644 --- a/tests/testkit/testkit/src/cli.rs +++ b/tests/testkit/testkit/src/cli.rs @@ -21,7 +21,7 @@ use crate::gateway::{ENDPOINT_ENV, GATEWAY_ENV, Gateway, WORKSPACE_ENV}; /// How long one CLI invocation may take. pub const CLI_TIMEOUT: Duration = Duration::from_mins(5); /// The CLI binary to run. Defaults to `openshell` on PATH. -pub const BIN_ENV: &str = "OPENSHELL_BIN"; +pub(crate) const BIN_ENV: &str = "OPENSHELL_BIN"; /// Runs the CLI against the gateway and workspace the tests use. #[derive(Clone, Debug)] diff --git a/tests/testkit/testkit/src/gateway.rs b/tests/testkit/testkit/src/gateway.rs index ee3e333500..6513fcfc4e 100644 --- a/tests/testkit/testkit/src/gateway.rs +++ b/tests/testkit/testkit/src/gateway.rs @@ -21,9 +21,9 @@ use openshell_sdk::{ClientConfig, ClientIdentity, Health, OpenShellClient, Servi use crate::error::{Error, Result, ResultExt}; -pub const ENDPOINT_ENV: &str = "OPENSHELL_GATEWAY_ENDPOINT"; -pub const GATEWAY_ENV: &str = "OPENSHELL_GATEWAY"; -pub const WORKSPACE_ENV: &str = "OPENSHELL_WORKSPACE"; +pub(crate) const ENDPOINT_ENV: &str = "OPENSHELL_GATEWAY_ENDPOINT"; +pub(crate) const GATEWAY_ENV: &str = "OPENSHELL_GATEWAY"; +pub(crate) const WORKSPACE_ENV: &str = "OPENSHELL_WORKSPACE"; const DEFAULT_WORKSPACE: &str = "default"; // Mirrors the SDK's endpoint settings for channels testkit builds itself. diff --git a/tests/testkit/testkit/src/image.rs b/tests/testkit/testkit/src/image.rs index 4e398800b1..26bb2278fe 100644 --- a/tests/testkit/testkit/src/image.rs +++ b/tests/testkit/testkit/src/image.rs @@ -1,18 +1,35 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! The base image tests create sandboxes from or build their images on. +//! The images tests create sandboxes from or build their images on. /// Overrides [`DEFAULT_BASE_IMAGE`], e.g. with a mirror. -pub const BASE_IMAGE_ENV: &str = "OPENSHELL_TEST_BASE_IMAGE"; +const BASE_IMAGE_ENV: &str = "OPENSHELL_TEST_BASE_IMAGE"; /// OpenShell's built-in default sandbox image. Pulling it needs access to /// nvcr.io. -pub const DEFAULT_BASE_IMAGE: &str = "nvcr.io/nvidia/base/ubuntu:24.04"; +const DEFAULT_BASE_IMAGE: &str = "nvcr.io/nvidia/base/ubuntu:24.04"; + +/// Overrides [`DEFAULT_PYTHON_IMAGE`]. +const PYTHON_IMAGE_ENV: &str = "OPENSHELL_TEST_PYTHON_IMAGE"; +/// The workload image for tests that run `python3`, curl, git or openssl in a +/// sandbox, built from `e2e/python/Dockerfile.workload`. It isn't published: +/// the tmachine installers load it into every environment, and +/// `mise run e2e:workload:build` builds it locally. +const DEFAULT_PYTHON_IMAGE: &str = "openshell/e2e-python:dev"; /// The image named by [`BASE_IMAGE_ENV`], or [`DEFAULT_BASE_IMAGE`]. pub fn base_image() -> String { - std::env::var(BASE_IMAGE_ENV) + image_from_env(BASE_IMAGE_ENV, DEFAULT_BASE_IMAGE) +} + +/// The image named by [`PYTHON_IMAGE_ENV`], or [`DEFAULT_PYTHON_IMAGE`]. +pub fn python_image() -> String { + image_from_env(PYTHON_IMAGE_ENV, DEFAULT_PYTHON_IMAGE) +} + +fn image_from_env(var: &str, default: &str) -> String { + std::env::var(var) .ok() .filter(|image| !image.trim().is_empty()) - .unwrap_or_else(|| DEFAULT_BASE_IMAGE.to_string()) + .unwrap_or_else(|| default.to_string()) } diff --git a/tests/testkit/testkit/src/lib.rs b/tests/testkit/testkit/src/lib.rs index 10db7d0e33..834f28c1e7 100644 --- a/tests/testkit/testkit/src/lib.rs +++ b/tests/testkit/testkit/src/lib.rs @@ -21,6 +21,10 @@ //! //! Tests find the gateway the same way the `openshell` CLI does (see //! [`gateway`]). +//! +//! `#[testkit::test]` also registers each test, so a binary that calls +//! [`main`] is a test program that lists, runs, and reports its tests without +//! Cargo (see [`program`]). mod cleanup; mod cli; @@ -29,23 +33,31 @@ mod error; pub mod gateway; mod image; mod poll; +pub mod program; +mod ready; +mod registry; mod run; pub use cleanup::CLEANUP_TIMEOUT; -pub use cli::{BIN_ENV, CLI_TIMEOUT, Cli, CliOutput}; +pub use cli::{CLI_TIMEOUT, Cli, CliOutput}; pub use context::{ Context, DELETE_TIMEOUT, EXEC_TIMEOUT, ExecOutput, READY_TIMEOUT, STOP_TIMEOUT, Sandbox, SandboxBuilder, }; pub use error::{Error, Result, ResultExt}; pub use gateway::Gateway; -pub use image::{BASE_IMAGE_ENV, DEFAULT_BASE_IMAGE, base_image}; +pub use image::{base_image, python_image}; pub use openshell_sdk as sdk; pub use poll::{Poll, poll_until}; +pub use program::main; +pub use ready::check as check_ready; +pub use registry::Test; pub use run::{TestInfo, run}; pub use testkit_macros::test; #[doc(hidden)] pub mod __private { + pub use crate::registry::TESTS; + pub use linkme; pub use tokio; } diff --git a/tests/testkit/testkit/src/program/capture.rs b/tests/testkit/testkit/src/program/capture.rs new file mode 100644 index 0000000000..329c22ac68 --- /dev/null +++ b/tests/testkit/testkit/src/program/capture.rs @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Copying a test's output to the console while keeping it for the report. + +use std::collections::VecDeque; +use std::io::{self, Read, Write}; +use std::sync::{Arc, Mutex, PoisonError}; +use std::thread::JoinHandle; +use std::time::{Duration, Instant}; + +/// How much of each stream is kept for the report. Output beyond this is +/// still shown; the report keeps the end, where failures are. +const KEPT_BYTES: usize = 4 << 20; + +/// How long to wait for a stream to close after the test's process group is +/// gone. A process that left the group can hold it open. +const CLOSE_TIMEOUT: Duration = Duration::from_secs(5); + +/// One stream being copied. +pub struct Capture { + kept: Arc>, + thread: JoinHandle<()>, +} + +#[derive(Default)] +struct Kept { + bytes: VecDeque, + dropped: usize, +} + +impl Capture { + /// Copy `stream` to `console` on a thread, keeping a copy. + pub fn start( + mut stream: impl Read + Send + 'static, + mut console: impl Write + Send + 'static, + ) -> Self { + let kept = Arc::new(Mutex::new(Kept::default())); + let thread = std::thread::spawn({ + let kept = Arc::clone(&kept); + move || { + let mut buffer = [0; 8192]; + loop { + let read = match stream.read(&mut buffer) { + Ok(0) => return, + Ok(read) => read, + Err(error) if error.kind() == io::ErrorKind::Interrupted => continue, + Err(_) => return, + }; + let chunk = &buffer[..read]; + let _ = console.write_all(chunk).and_then(|()| console.flush()); + kept.lock() + .unwrap_or_else(PoisonError::into_inner) + .push(chunk); + } + } + }); + Self { kept, thread } + } + + /// The output kept, once the stream closes or after a short wait. + pub fn finish(self) -> Vec { + let deadline = Instant::now() + CLOSE_TIMEOUT; + while !self.thread.is_finished() && Instant::now() < deadline { + std::thread::sleep(Duration::from_millis(10)); + } + if self.thread.is_finished() { + let _ = self.thread.join(); + } + let kept = self.kept.lock().unwrap_or_else(PoisonError::into_inner); + let mut bytes = Vec::with_capacity(kept.bytes.len() + 64); + if kept.dropped > 0 { + bytes.extend(format!("[{} earlier bytes not kept]\n", kept.dropped).as_bytes()); + } + bytes.extend(&kept.bytes); + bytes + } +} + +impl Kept { + fn push(&mut self, chunk: &[u8]) { + self.bytes.extend(chunk); + let excess = self.bytes.len().saturating_sub(KEPT_BYTES); + self.bytes.drain(..excess); + self.dropped += excess; + } +} + +#[cfg(test)] +mod tests { + use super::{Capture, KEPT_BYTES, Kept}; + + #[test] + fn copies_and_keeps_the_stream() { + let capture = Capture::start(&b"one\ntwo\n"[..], std::io::sink()); + assert_eq!(capture.finish(), b"one\ntwo\n"); + } + + #[test] + fn keeps_the_end_of_long_output() { + let mut kept = Kept::default(); + kept.push(&vec![b'a'; KEPT_BYTES]); + kept.push(b"end"); + assert_eq!(kept.bytes.len(), KEPT_BYTES); + let tail: Vec = kept.bytes.iter().rev().take(4).rev().copied().collect(); + assert_eq!(tail, b"aend"); + assert_eq!(kept.dropped, 3); + } +} diff --git a/tests/testkit/testkit/src/program/junit.rs b/tests/testkit/testkit/src/program/junit.rs new file mode 100644 index 0000000000..0a1e66d648 --- /dev/null +++ b/tests/testkit/testkit/src/program/junit.rs @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! The `JUnit` report of a run. +//! +//! One `` per family. Each `` is named by the test name, +//! with the module path as its class name and the description and family as +//! properties. + +use std::collections::BTreeMap; +use std::path::Path; + +use quick_junit::{NonSuccessKind, Property, Report, TestCase, TestCaseStatus, TestSuite}; + +use super::supervisor::{Outcome, TestRun}; + +pub fn report(program: &str, runs: &[TestRun<'_>]) -> Report { + let mut suites: BTreeMap<&str, TestSuite> = BTreeMap::new(); + for run in runs { + let family = run.test.family(); + suites + .entry(family) + .or_insert_with(|| TestSuite::new(family)) + .add_test_case(test_case(run)); + } + let mut report = Report::new(program); + report.add_test_suites(suites.into_values()); + report +} + +fn test_case(run: &TestRun<'_>) -> TestCase { + let status = match &run.outcome { + Outcome::Passed => TestCaseStatus::success(), + Outcome::Skipped => { + let mut status = TestCaseStatus::skipped(); + status.set_message(match run.test.ignore_reason { + Some(reason) => format!("ignored: {reason}"), + None => "ignored".to_string(), + }); + status + } + Outcome::Failed(message) => non_success(NonSuccessKind::Failure, "failure", message), + Outcome::TimedOut(message) => non_success(NonSuccessKind::Failure, "timeout", message), + Outcome::Error(message) => non_success(NonSuccessKind::Error, "error", message), + Outcome::Interrupted => non_success(NonSuccessKind::Error, "interrupted", "interrupted"), + }; + let mut case = TestCase::new(run.test.name(), status); + case.set_classname(run.test.module_path) + .add_property(Property::new("family", run.test.family())) + .add_property(Property::new("description", run.test.info.description)); + if let Some(duration) = run.duration { + case.set_time(duration); + } + if !run.stdout.is_empty() { + case.set_system_out_lossy(&run.stdout); + } + if !run.stderr.is_empty() { + case.set_system_err_lossy(&run.stderr); + } + case +} + +fn non_success(kind: NonSuccessKind, ty: &str, message: &str) -> TestCaseStatus { + let mut status = TestCaseStatus::non_success(kind); + status.set_type(ty).set_message(message); + status +} + +pub fn write(path: &Path, report: &Report) -> std::io::Result<()> { + if let Some(parent) = path + .parent() + .filter(|parent| !parent.as_os_str().is_empty()) + { + std::fs::create_dir_all(parent)?; + } + let xml = report.to_string().map_err(std::io::Error::other)?; + std::fs::write(path, xml) +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use super::report; + use crate::program::supervisor::{Outcome, TestRun}; + use crate::registry::Test; + use crate::registry::tests::test; + + const PASSED: Test = test("sandbox::smoke", "create_exec_delete", false); + const FAILED: Test = test("sandbox::lifecycle", "stop_start", false); + const SKIPPED: Test = Test { + ignore_reason: Some("https://github.com/NVIDIA/OpenShell/issues/1"), + ..test("sandbox::lifecycle", "later", true) + }; + + #[test] + fn reports_every_outcome() { + let run = |test, outcome, duration| TestRun { + test, + outcome, + duration, + stdout: b"out\n".to_vec(), + stderr: Vec::new(), + }; + let runs = [ + run(&PASSED, Outcome::Passed, Some(Duration::from_millis(1500))), + run( + &FAILED, + Outcome::TimedOut("timed out after 1s".into()), + Some(Duration::from_secs(1)), + ), + run(&SKIPPED, Outcome::Skipped, None), + ]; + let xml = report("openshell-conformance", &runs).to_string().unwrap(); + for expected in [ + r#""#, + r#""#, + r#""#, + r#""#, + r#""#, + r#""#, + r#""#, + "out\n", + ] { + assert!(xml.contains(expected), "missing {expected} in\n{xml}"); + } + } +} diff --git a/tests/testkit/testkit/src/program/mod.rs b/tests/testkit/testkit/src/program/mod.rs new file mode 100644 index 0000000000..c7ca98f3ba --- /dev/null +++ b/tests/testkit/testkit/src/program/mod.rs @@ -0,0 +1,383 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! `testkit::main()`: a test program anyone can run against their gateway. +//! +//! A program is a binary whose `main` calls [`main`], linked with the crates +//! holding its `#[testkit::test]`s: +//! +//! ```ignore +//! fn main() -> std::process::ExitCode { +//! testkit::main() +//! } +//! ``` +//! +//! It has three subcommands: +//! +//! - `list [FILTER]...` prints each test's name and description. +//! - `ready [--timeout SECS]` waits for the gateway, as `testkit-ready` does. +//! - `run [FILTER]...` runs tests one at a time, each in its own process with +//! a timeout, and can write a `JUnit` report. Unix only. +//! +//! Any other arguments are libtest's, so the program also works as a +//! `harness = false` test target under `cargo test` and nextest: it answers +//! `--list --format terse`, `--ignored`, and ` --exact --nocapture`. +//! In that mode tests run in the program's own process, one at a time unless +//! `--test-threads` says otherwise. + +#[cfg(unix)] +mod capture; +#[cfg(unix)] +mod junit; +#[cfg(unix)] +mod process; +#[cfg(unix)] +mod supervisor; + +use std::ffi::OsString; +use std::path::PathBuf; +use std::process::ExitCode; +use std::time::Duration; + +use clap::{Parser, Subcommand}; + +use crate::registry::{RESERVED_NAMES, Selection, TESTS, Test, validate}; + +/// Exit code when a test failed. +const FAILED_EXIT: u8 = 1; +/// Exit code when tests could not run, including usage errors. +const ERROR_EXIT: u8 = 2; +/// Set by `run` for the test processes it starts. +const SUPERVISED_ENV: &str = "TESTKIT_SUPERVISED"; + +/// Run the program: the subcommands, or libtest's interface. +pub fn main() -> ExitCode { + main_with(&TESTS) +} + +/// [`main`] over `tests` instead of every registered test. +#[doc(hidden)] +pub fn main_with(tests: &'static [Test]) -> ExitCode { + let args: Vec = std::env::args_os().collect(); + let program = program_name(&args); + // A suite crate that the program never names is not linked, and its + // tests are silently missing. Fail instead of passing with nothing. + if tests.is_empty() { + eprintln!( + "{program}: no tests are linked into this program; it must name each suite crate \ + it runs, for example with `use as _;`" + ); + return ExitCode::from(ERROR_EXIT); + } + if let Err(error) = validate(tests) { + eprintln!("{program}: {error}"); + return ExitCode::from(ERROR_EXIT); + } + let first = args.get(1).and_then(|arg| arg.to_str()); + if first.is_some_and(|arg| RESERVED_NAMES.contains(&arg) || arg == "-h" || arg == "--help") { + let cli = match Cli::try_parse_from(&args) { + Ok(cli) => cli, + Err(error) => { + let _ = error.print(); + return ExitCode::from(if error.use_stderr() { ERROR_EXIT } else { 0 }); + } + }; + command(&program, tests, cli.command) + } else { + libtest(tests, args) + } +} + +#[derive(Parser)] +#[command( + about = "Run OpenShell tests against a gateway", + after_help = "The gateway is the one the openshell CLI selects: OPENSHELL_GATEWAY_ENDPOINT, \ + OPENSHELL_GATEWAY, or the active gateway. OPENSHELL_WORKSPACE selects the \ + workspace and OPENSHELL_BIN the CLI. Only plaintext and mTLS gateways are \ + supported.\n\nWithout a subcommand the program takes libtest's arguments, \ + as under cargo test and nextest." +)] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + /// Print each test's name and description, including ignored tests. + List { + /// Show tests whose name contains any of these (all if none). + filters: Vec, + }, + /// Wait until the gateway is healthy and the CLI reaches it. Exits 2 if + /// it is not ready in time. + Ready { + /// How long to wait, in seconds. + #[arg(long, default_value = "60", value_name = "SECS", value_parser = seconds)] + timeout: Duration, + }, + /// Run tests one at a time, each in its own process. Exits 1 if a test + /// failed and 2 if tests could not run. + Run { + /// Run tests whose name contains any of these (all if none). + filters: Vec, + /// Run ignored tests too. + #[arg(long)] + include_ignored: bool, + /// Stop a test that runs longer than this, in seconds. Longer than + /// testkit's own step timeouts, so those report first. + #[arg(long, default_value = "900", value_name = "SECS", value_parser = seconds)] + timeout: Duration, + /// How long a stopped test has between SIGTERM and SIGKILL, in seconds. + #[arg(long, default_value = "10", value_name = "SECS", value_parser = seconds)] + grace: Duration, + /// Write a JUnit report here. + #[arg(long, value_name = "PATH")] + junit: Option, + }, +} + +/// A positive, finite number of seconds. +fn seconds(value: &str) -> Result { + let seconds: f64 = value + .parse() + .map_err(|_| format!("{value:?} is not a number"))?; + Duration::try_from_secs_f64(seconds) + .ok() + .filter(|duration| !duration.is_zero()) + .ok_or_else(|| format!("{value:?} is not a positive number of seconds")) +} + +fn command(program: &str, tests: &'static [Test], command: Command) -> ExitCode { + match command { + Command::List { filters } => { + list(tests, filters); + ExitCode::SUCCESS + } + Command::Ready { timeout } => ready(program, timeout), + Command::Run { + filters, + include_ignored, + timeout, + grace, + junit, + } => run( + program, + tests, + Selection { + filters, + include_ignored, + }, + timeout, + grace, + junit, + ), + } +} + +#[cfg(unix)] +fn run( + program: &str, + tests: &'static [Test], + selection: Selection, + timeout: Duration, + grace: Duration, + junit: Option, +) -> ExitCode { + let exe = match std::env::current_exe() { + Ok(exe) => exe, + Err(error) => { + eprintln!("{program}: find this program's path: {error}"); + return ExitCode::from(ERROR_EXIT); + } + }; + let options = supervisor::Options { + selection, + timeout, + grace, + junit, + }; + supervisor::run(program, &exe, tests, &options) +} + +#[cfg(not(unix))] +fn run( + program: &str, + _tests: &'static [Test], + _selection: Selection, + _timeout: Duration, + _grace: Duration, + _junit: Option, +) -> ExitCode { + eprintln!( + "{program}: `run` is only supported on Unix; run the tests with cargo test or nextest" + ); + ExitCode::from(ERROR_EXIT) +} + +/// One line per test: name and description, in aligned columns. +/// An ignored test's description ends with `[ignored]` or its reason. +fn list(tests: &[Test], filters: Vec) { + let selection = Selection { + filters, + include_ignored: true, + }; + let rows: Vec<_> = selection + .apply(tests) + .into_iter() + .map(|(test, _)| (test.name(), describe(test))) + .collect(); + let name_width = rows.iter().map(|row| row.0.len()).max().unwrap_or(0); + for (name, description) in rows { + println!("{name:name_width$} {description}"); + } +} + +fn describe(test: &Test) -> String { + let description = test.info.description; + match (test.ignored, test.ignore_reason) { + (false, _) => description.to_string(), + (true, None) => format!("{description} [ignored]"), + (true, Some(reason)) => format!("{description} [ignored: {reason}]"), + } +} + +fn ready(program: &str, timeout: Duration) -> ExitCode { + let runtime = match tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + { + Ok(runtime) => runtime, + Err(error) => { + eprintln!("{program}: start the async runtime: {error}"); + return ExitCode::from(ERROR_EXIT); + } + }; + // A gateway that is not ready means the tests cannot run. + if runtime.block_on(crate::ready::check(program, timeout)) { + ExitCode::SUCCESS + } else { + ExitCode::from(ERROR_EXIT) + } +} + +/// libtest's interface, for `cargo test` and nextest, and for `run`'s test +/// processes. +fn libtest(tests: &'static [Test], args: Vec) -> ExitCode { + let mut args = libtest_mimic::Arguments::from_iter(args); + // Tests share one gateway; run them one at a time unless asked otherwise. + if args.test_threads.is_none() { + args.test_threads = Some(1); + } + let mut tests: Vec<&'static Test> = tests.iter().collect(); + tests.sort_by_cached_key(|test| test.name()); + let trials = tests + .into_iter() + .map(|test| { + libtest_mimic::Trial::test(test.name(), move || { + run_in_process(test); + Ok(()) + }) + .with_ignored_flag(test.ignored) + }) + .collect(); + let conclusion = libtest_mimic::run(&args, trials); + // `run` starts this process for exactly one test. Running none would + // look like a pass. + if std::env::var_os(SUPERVISED_ENV).is_some() + && !args.list + && conclusion.num_passed + conclusion.num_failed != 1 + { + eprintln!( + "testkit: expected to run exactly one test, ran {}", + conclusion.num_passed + conclusion.num_failed + ); + return ExitCode::from(ERROR_EXIT); + } + conclusion.exit_code() +} + +/// Run one test on a fresh current-thread runtime, as `#[tokio::test]` does. +/// Panics to fail it. +fn run_in_process(test: &Test) { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("start the async runtime") + .block_on((test.run)()); +} + +fn program_name(args: &[OsString]) -> String { + args.first() + .map(PathBuf::from) + .and_then(|path| { + path.file_name() + .map(|name| name.to_string_lossy().into_owned()) + }) + .unwrap_or_else(|| "testkit".to_string()) +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use clap::Parser; + + use super::{Cli, Command}; + + #[test] + fn cli_is_consistent() { + ::command().debug_assert(); + } + + #[test] + fn run_parses_selection_and_limits() { + let cli = Cli::try_parse_from([ + "p", + "run", + "smoke", + "life", + "--include-ignored", + "--timeout", + "1.5", + "--junit", + "out/junit.xml", + ]) + .unwrap(); + let Command::Run { + filters, + include_ignored, + timeout, + grace, + junit, + } = cli.command + else { + panic!("not run"); + }; + assert_eq!(filters, ["smoke", "life"]); + assert!(include_ignored); + assert_eq!(timeout, Duration::from_millis(1500)); + assert_eq!(grace, Duration::from_secs(10)); + assert_eq!(junit.unwrap().to_str(), Some("out/junit.xml")); + + let Command::Ready { timeout } = Cli::try_parse_from(["p", "ready"]).unwrap().command + else { + panic!("not ready"); + }; + assert_eq!(timeout, Duration::from_secs(60)); + } + + #[test] + fn rejects_bad_limits() { + for bad in ["0", "-1", "soon", "inf", "NaN"] { + assert!( + Cli::try_parse_from(["p", "run", "--timeout", bad]).is_err(), + "{bad}" + ); + assert!( + Cli::try_parse_from(["p", "ready", "--timeout", bad]).is_err(), + "{bad}" + ); + } + } +} diff --git a/tests/testkit/testkit/src/program/process.rs b/tests/testkit/testkit/src/program/process.rs new file mode 100644 index 0000000000..1959452953 --- /dev/null +++ b/tests/testkit/testkit/src/program/process.rs @@ -0,0 +1,216 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! One test process: started in its own process group, waited for with a +//! timeout, and stopped with `SIGTERM`, a grace period, then `SIGKILL`. +//! +//! The process group is killed whenever the test process ends, so a test's +//! helpers, such as CLI processes, cannot outlive it. + +use std::io; +use std::os::unix::process::CommandExt as _; +use std::process::{Child, Command, ExitStatus}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::{Duration, Instant}; + +const POLL_INTERVAL: Duration = Duration::from_millis(20); + +/// How the test process ended. +#[derive(Debug)] +pub enum Ended { + Exited(ExitStatus), + /// Stopped after the timeout. + TimedOut, + /// Stopped because the supervisor was asked to stop. + Interrupted, +} + +/// Start `command` in its own process group. +pub fn spawn(command: &mut Command) -> io::Result { + command.process_group(0).spawn() +} + +/// Wait for `child`, stopping it after `timeout` or once the supervisor has +/// been asked to stop (see [`install_stop_handlers`]). The child is reaped +/// and its group killed on every path, including errors. +pub fn wait(child: &mut Child, timeout: Duration, grace: Duration) -> io::Result { + let result = wait_or_stop(child, timeout, grace); + if result.is_err() { + let _ = reap(child); + } + result +} + +fn wait_or_stop(child: &mut Child, timeout: Duration, grace: Duration) -> io::Result { + let deadline = Instant::now() + timeout; + loop { + if has_exited(child)? { + return Ok(Ended::Exited(reap(child)?)); + } + if stop_requested() { + stop(child, grace)?; + return Ok(Ended::Interrupted); + } + if Instant::now() >= deadline { + stop(child, grace)?; + return Ok(Ended::TimedOut); + } + std::thread::sleep(POLL_INTERVAL); + } +} + +/// `SIGTERM` the group, wait up to `grace`, then `SIGKILL` it. A stop signal +/// during the grace period, or a second one in all, cuts it short. +fn stop(child: &mut Child, grace: Duration) -> io::Result { + signal_group(child, libc::SIGTERM); + let deadline = Instant::now() + grace; + let before = stop_requests(); + let cut_short = || { + let now = stop_requests(); + now > before || now >= 2 + }; + while Instant::now() < deadline && !cut_short() && !has_exited(child)? { + std::thread::sleep(POLL_INTERVAL); + } + reap(child) +} + +/// Kill whatever is left of the group, then collect the exit status. +fn reap(child: &mut Child) -> io::Result { + signal_group(child, libc::SIGKILL); + child.wait() +} + +static STOP_REQUESTS: AtomicUsize = AtomicUsize::new(0); + +fn stop_requests() -> usize { + STOP_REQUESTS.load(Ordering::SeqCst) +} + +/// Whether the supervisor has been asked to stop since it started. +pub fn stop_requested() -> bool { + stop_requests() > 0 +} + +fn pid(child: &Child) -> libc::pid_t { + libc::pid_t::try_from(child.id()).expect("pid fits in pid_t") +} + +/// Whether the child has exited, without reaping it. While it is unreaped +/// its PID, and so its process group ID, cannot be reused, so signalling the +/// group cannot reach an unrelated process. +#[cfg(target_os = "linux")] +fn has_exited(child: &mut Child) -> io::Result { + // SAFETY: `info` is a valid out-pointer, and `WNOWAIT` leaves the child + // for `Child::wait` to reap. + unsafe { + let mut info: libc::siginfo_t = std::mem::zeroed(); + let result = libc::waitid( + libc::P_PID, + child.id(), + &raw mut info, + libc::WEXITED | libc::WNOHANG | libc::WNOWAIT, + ); + if result == -1 { + return Err(io::Error::last_os_error()); + } + Ok(info.si_pid() != 0) + } +} + +/// Whether the child has exited, reaping it. `waitid` with `WNOWAIT` is not +/// reliable outside Linux (see golang/go#19314), so the group is killed just +/// after the leader is reaped. Its ID could in principle be reused in that +/// window, though a new process would have to get the same PID and lead a +/// group. +#[cfg(not(target_os = "linux"))] +fn has_exited(child: &mut Child) -> io::Result { + Ok(child.try_wait()?.is_some()) +} + +fn signal_group(child: &Child, signal: libc::c_int) { + // SAFETY: plain syscall to the child's own group (see `spawn`). ESRCH, an + // empty group, is fine. + unsafe { + libc::kill(-pid(child), signal); + } +} + +extern "C" fn on_stop(_signal: libc::c_int) { + STOP_REQUESTS.fetch_add(1, Ordering::SeqCst); +} + +/// Count `SIGINT`, `SIGTERM`, and `SIGHUP` as stop requests instead of dying. +/// Tests run in their own process group, so a Ctrl-C at the terminal reaches +/// only the supervisor, which must stop the test. +pub fn install_stop_handlers() -> io::Result<()> { + for signal in [libc::SIGINT, libc::SIGTERM, libc::SIGHUP] { + // SAFETY: the handler only touches an atomic, which is + // async-signal-safe. + unsafe { + let mut action: libc::sigaction = std::mem::zeroed(); + action.sa_sigaction = on_stop as *const () as libc::sighandler_t; + action.sa_flags = libc::SA_RESTART; + libc::sigemptyset(&raw mut action.sa_mask); + if libc::sigaction(signal, &raw const action, std::ptr::null_mut()) == -1 { + return Err(io::Error::last_os_error()); + } + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::process::{Command, Stdio}; + use std::time::{Duration, Instant}; + + use super::{Ended, spawn, wait}; + + fn sh(script: &str) -> Command { + let mut command = Command::new("sh"); + command.args(["-c", script]).stdin(Stdio::null()); + command + } + + #[test] + fn exit_status_is_kept() { + let mut child = spawn(&mut sh("exit 3")).unwrap(); + let ended = wait(&mut child, Duration::from_secs(10), Duration::from_secs(1)).unwrap(); + assert!( + matches!(ended, Ended::Exited(status) if status.code() == Some(3)), + "{ended:?}" + ); + } + + #[test] + fn timeout_terminates_the_test() { + let started = Instant::now(); + let mut child = spawn(&mut sh("sleep 30")).unwrap(); + let ended = wait( + &mut child, + Duration::from_millis(200), + Duration::from_secs(10), + ) + .unwrap(); + assert!(matches!(ended, Ended::TimedOut), "{ended:?}"); + // `sleep` dies on SIGTERM, well inside the grace period. + assert!(started.elapsed() < Duration::from_secs(5)); + } + + #[test] + fn a_test_ignoring_sigterm_is_killed_after_the_grace_period() { + let started = Instant::now(); + let mut child = spawn(&mut sh("trap '' TERM; sleep 30")).unwrap(); + let ended = wait( + &mut child, + Duration::from_millis(200), + Duration::from_millis(500), + ) + .unwrap(); + assert!(matches!(ended, Ended::TimedOut), "{ended:?}"); + let elapsed = started.elapsed(); + assert!(elapsed >= Duration::from_millis(700), "{elapsed:?}"); + assert!(elapsed < Duration::from_secs(5), "{elapsed:?}"); + } +} diff --git a/tests/testkit/testkit/src/program/supervisor.rs b/tests/testkit/testkit/src/program/supervisor.rs new file mode 100644 index 0000000000..6dced12ae1 --- /dev/null +++ b/tests/testkit/testkit/src/program/supervisor.rs @@ -0,0 +1,334 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! `run`: run the selected tests one at a time, each in a new process. +//! +//! The program starts itself once per test as ` --exact --nocapture`, +//! the same interface nextest uses, in a new process group. A test that runs +//! past its timeout gets `SIGTERM`, a grace period, then `SIGKILL`. Output is +//! shown as it comes and kept for the `JUnit` report. +//! +//! A test stopped this way does not run its cleanup: testkit tests do not +//! handle `SIGTERM` yet, so sandboxes named `test--*` can be left +//! behind. + +use std::fmt::Write as _; +use std::path::{Path, PathBuf}; +use std::process::{Command, ExitCode, ExitStatus, Stdio}; +use std::time::{Duration, Instant}; + +use super::capture::Capture; +use super::process::{self, Ended}; +use super::{ERROR_EXIT, FAILED_EXIT, SUPERVISED_ENV, junit}; +use crate::registry::{Disposition, Selection, Test}; + +/// libtest's exit code when a test failed. +const LIBTEST_FAILED: i32 = 101; + +pub struct Options { + pub selection: Selection, + pub timeout: Duration, + pub grace: Duration, + pub junit: Option, +} + +/// One test's result. +pub struct TestRun<'a> { + pub test: &'a Test, + pub outcome: Outcome, + /// `None` for a test that did not run. + pub duration: Option, + pub stdout: Vec, + pub stderr: Vec, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Outcome { + Passed, + /// Ignored and not asked for. + Skipped, + Failed(String), + TimedOut(String), + /// The test could not be run. + Error(String), + /// Stopped because the run was interrupted. + Interrupted, +} + +impl Outcome { + fn label(&self) -> &'static str { + match self { + Self::Passed => "PASS", + Self::Skipped => "SKIP", + Self::Failed(_) => "FAIL", + Self::TimedOut(_) => "TIMEOUT", + Self::Error(_) | Self::Interrupted => "ERROR", + } + } + + fn message(&self) -> Option<&str> { + match self { + Self::Passed | Self::Skipped => None, + Self::Failed(message) | Self::TimedOut(message) | Self::Error(message) => Some(message), + Self::Interrupted => Some("interrupted"), + } + } +} + +pub fn run(program: &str, exe: &Path, tests: &[Test], options: &Options) -> ExitCode { + let selected = options.selection.apply(tests); + let to_run = selected + .iter() + .filter(|(_, disposition)| *disposition == Disposition::Run) + .count(); + if to_run == 0 { + eprintln!("{program}: no tests to run; `{program} list` shows the tests"); + return ExitCode::from(ERROR_EXIT); + } + if let Err(error) = process::install_stop_handlers() { + eprintln!("{program}: install signal handlers: {error}"); + return ExitCode::from(ERROR_EXIT); + } + let skipped = selected.len() - to_run; + eprintln!( + "{program}: running {to_run} test{} one at a time{}", + plural(to_run), + if skipped > 0 { + format!(", skipping {skipped} ignored") + } else { + String::new() + } + ); + + let started = Instant::now(); + let selected_count = selected.len(); + let mut runs = Vec::with_capacity(selected_count); + let mut position = 0; + for (test, disposition) in selected { + // A stop signal that arrives between tests still ends the run. + if process::stop_requested() { + break; + } + if disposition == Disposition::Skip { + runs.push(TestRun { + test, + outcome: Outcome::Skipped, + duration: None, + stdout: Vec::new(), + stderr: Vec::new(), + }); + continue; + } + position += 1; + let name = test.name(); + eprintln!("==> [{position}/{to_run}] {name}"); + let run = run_one(exe, test, options); + let elapsed = run.duration.unwrap_or_default(); + match run.outcome.message() { + None => eprintln!("<== {} {name} ({elapsed:.1?})", run.outcome.label()), + Some(message) => eprintln!( + "<== {} {name} ({elapsed:.1?}): {message}", + run.outcome.label() + ), + } + runs.push(run); + } + + let not_run = selected_count - runs.len(); + let mut status = summarize(program, &runs, not_run, started.elapsed()); + if let Some(path) = &options.junit { + match junit::write(path, &junit::report(program, &runs)) { + Ok(()) => eprintln!("{program}: wrote {}", path.display()), + Err(error) => { + eprintln!("{program}: write {}: {error}", path.display()); + status = ERROR_EXIT; + } + } + } + ExitCode::from(status) +} + +fn run_one<'a>(exe: &Path, test: &'a Test, options: &Options) -> TestRun<'a> { + let name = test.name(); + let mut command = Command::new(exe); + command + .args([name.as_str(), "--exact", "--nocapture"]) + .env(SUPERVISED_ENV, "1") + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + if test.ignored { + command.arg("--ignored"); + } + + let started = Instant::now(); + let mut child = match process::spawn(&mut command) { + Ok(child) => child, + Err(error) => { + return TestRun { + test, + outcome: Outcome::Error(format!("start {}: {error}", exe.display())), + duration: Some(started.elapsed()), + stdout: Vec::new(), + stderr: Vec::new(), + }; + } + }; + let stdout = child + .stdout + .take() + .map(|stream| Capture::start(stream, std::io::stdout())); + let stderr = child + .stderr + .take() + .map(|stream| Capture::start(stream, std::io::stderr())); + let ended = process::wait(&mut child, options.timeout, options.grace); + let duration = started.elapsed(); + let outcome = match ended { + Ok(ended) => outcome(ended, options.timeout), + Err(error) => Outcome::Error(format!("wait for the test process: {error}")), + }; + TestRun { + test, + outcome, + duration: Some(duration), + stdout: stdout.map(Capture::finish).unwrap_or_default(), + stderr: stderr.map(Capture::finish).unwrap_or_default(), + } +} + +fn outcome(ended: Ended, timeout: Duration) -> Outcome { + match ended { + Ended::Exited(status) => exited(status), + Ended::TimedOut => Outcome::TimedOut(format!( + "timed out after {}; stopped without cleanup", + seconds(timeout) + )), + Ended::Interrupted => Outcome::Interrupted, + } +} + +fn exited(status: ExitStatus) -> Outcome { + match status.code() { + Some(0) => Outcome::Passed, + Some(LIBTEST_FAILED) => Outcome::Failed("test failed".to_string()), + Some(code) if code == i32::from(ERROR_EXIT) => { + Outcome::Error("the test process could not run the test; see its output".to_string()) + } + Some(code) => Outcome::Failed(format!("test process exited with code {code}")), + None => Outcome::Failed(format!("test process ended by {status}")), + } +} + +/// Print the totals and the tests that did not pass; return the exit code: +/// 1 if a test failed, 2 if one could not run or the run was interrupted +/// before `not_run` tests. +fn summarize(program: &str, runs: &[TestRun<'_>], not_run: usize, elapsed: Duration) -> u8 { + let count = + |predicate: fn(&Outcome) -> bool| runs.iter().filter(|run| predicate(&run.outcome)).count(); + let passed = count(|outcome| matches!(outcome, Outcome::Passed)); + let failed = count(|outcome| matches!(outcome, Outcome::Failed(_) | Outcome::TimedOut(_))); + let errors = count(|outcome| matches!(outcome, Outcome::Error(_) | Outcome::Interrupted)); + let skipped = count(|outcome| matches!(outcome, Outcome::Skipped)); + let mut summary = format!("{program}: {passed} passed, {failed} failed"); + if errors > 0 { + let _ = write!(summary, ", {errors} could not run"); + } + if skipped > 0 { + let _ = write!(summary, ", {skipped} skipped"); + } + let _ = write!(summary, " in {elapsed:.1?}"); + eprintln!("{summary}"); + for run in runs { + if let Some(message) = run.outcome.message() { + eprintln!(" {} {}: {message}", run.outcome.label(), run.test.name()); + } + } + if not_run > 0 { + eprintln!( + "{program}: interrupted; {not_run} later test{} did not run", + plural(not_run) + ); + } + if errors > 0 || not_run > 0 { + ERROR_EXIT + } else if failed > 0 { + FAILED_EXIT + } else { + 0 + } +} + +fn seconds(duration: Duration) -> String { + format!("{}s", duration.as_secs_f64()) +} + +fn plural(count: usize) -> &'static str { + if count == 1 { "" } else { "s" } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use super::{Outcome, TestRun, summarize}; + use crate::registry::Test; + use crate::registry::tests::test; + + const TEST: Test = test("suite::m", "t", false); + + fn runs(outcomes: Vec) -> Vec> { + outcomes + .into_iter() + .map(|outcome| TestRun { + test: &TEST, + outcome, + duration: None, + stdout: Vec::new(), + stderr: Vec::new(), + }) + .collect() + } + + #[test] + fn exit_code_separates_failures_from_runs_that_could_not_complete() { + let code = |outcomes| summarize("p", &runs(outcomes), 0, Duration::ZERO); + assert_eq!(code(vec![Outcome::Passed, Outcome::Skipped]), 0); + assert_eq!(code(vec![Outcome::Passed, Outcome::Failed("f".into())]), 1); + assert_eq!(code(vec![Outcome::TimedOut("t".into())]), 1); + assert_eq!( + code(vec![ + Outcome::Failed("f".into()), + Outcome::Error("e".into()) + ]), + 2 + ); + assert_eq!(code(vec![Outcome::Interrupted]), 2); + // Interrupted between tests: every test that ran passed. + assert_eq!( + summarize("p", &runs(vec![Outcome::Passed]), 1, Duration::ZERO), + 2 + ); + } + + #[test] + fn exit_statuses_map_to_outcomes() { + use std::os::unix::process::ExitStatusExt; + + use super::exited; + + let status = |code: i32| std::process::ExitStatus::from_raw(code << 8); + assert_eq!(exited(status(0)), Outcome::Passed); + assert_eq!(exited(status(101)), Outcome::Failed("test failed".into())); + assert!(matches!(exited(status(2)), Outcome::Error(_))); + assert_eq!( + exited(status(7)), + Outcome::Failed("test process exited with code 7".into()) + ); + let killed = exited(std::process::ExitStatus::from_raw(libc::SIGSEGV)); + assert!( + matches!(&killed, Outcome::Failed(message) if message.contains("signal")), + "{killed:?}" + ); + } +} diff --git a/tests/testkit/testkit/src/ready.rs b/tests/testkit/testkit/src/ready.rs new file mode 100644 index 0000000000..db36f67659 --- /dev/null +++ b/tests/testkit/testkit/src/ready.rs @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Waiting until the gateway is ready for tests. + +use std::time::Duration; + +use crate::cli::Cli; +use crate::gateway::Gateway; + +/// Wait until the gateway selected as for tests is healthy and the CLI +/// reports it connected, logging to stderr as `program`. Returns whether it +/// is ready; if not, the last error has been logged. +pub async fn check(program: &str, timeout: Duration) -> bool { + let result = async { + let gateway = Gateway::from_env()?; + eprintln!("{program}: {}", gateway.describe()); + let health = gateway.wait_ready(timeout).await?; + Cli::from_env(&gateway).check_connected().await?; + crate::Result::Ok(health) + } + .await; + match result { + Ok(health) => { + eprintln!( + "{program}: gateway {} is healthy and the CLI reaches it", + health.version + ); + true + } + Err(error) => { + eprintln!("{program}: {error}"); + false + } + } +} diff --git a/tests/testkit/testkit/src/registry.rs b/tests/testkit/testkit/src/registry.rs new file mode 100644 index 0000000000..aceaed673f --- /dev/null +++ b/tests/testkit/testkit/src/registry.rs @@ -0,0 +1,208 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! The tests linked into a program, registered by `#[testkit::test]`. + +use std::collections::BTreeSet; +use std::future::Future; +use std::pin::Pin; + +use crate::run::TestInfo; + +/// Every `#[testkit::test]` linked into the program. +#[linkme::distributed_slice] +pub static TESTS: [Test]; + +/// A registered test. +#[derive(Debug)] +pub struct Test { + pub info: TestInfo, + /// The directory under `tests/suites/` holding the test's crate, if any. + pub family: Option<&'static str>, + /// `module_path!()` where the test is defined. + pub module_path: &'static str, + pub ignored: bool, + /// The reason given in `#[ignore = "..."]`. + pub ignore_reason: Option<&'static str>, + /// Runs the test through [`crate::run`]; panics to fail it. + pub run: fn() -> Pin>>, +} + +impl Test { + /// The test's name, as libtest and nextest name it: the module path + /// without the crate, then the function name. + pub fn name(&self) -> String { + match self.module_path.split_once("::") { + Some((_crate, module)) => format!("{module}::{}", self.info.name), + None => self.info.name.to_string(), + } + } + + pub fn family(&self) -> &'static str { + self.family.unwrap_or("-") + } +} + +/// Names a test cannot have at the top level: the program would read them as +/// its own subcommands, so nextest could not run the test by name. +pub const RESERVED_NAMES: &[&str] = &["help", "list", "ready", "run"]; + +/// Check the tests can be told apart by name. +pub fn validate(tests: &[Test]) -> Result<(), String> { + let mut names = BTreeSet::new(); + for test in tests { + let name = test.name(); + if RESERVED_NAMES.contains(&name.as_str()) { + return Err(format!( + "test {name} has the name of a subcommand; rename it or move it into a module" + )); + } + if !names.insert(name.clone()) { + return Err(format!( + "two tests are named {name}; test names must be unique in a program" + )); + } + } + Ok(()) +} + +/// Which tests to run. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Selection { + /// Run tests whose name contains any of these; all tests if empty. + pub filters: Vec, + /// Run `#[ignore]`d tests too, instead of reporting them as skipped. + pub include_ignored: bool, +} + +/// What a selection does with one test. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Disposition { + Run, + /// Selected, but ignored and not asked for. + Skip, +} + +impl Selection { + /// The tests this selects, in name order, with what to do with each. + pub fn apply<'a>(&self, tests: &'a [Test]) -> Vec<(&'a Test, Disposition)> { + let mut selected: Vec<_> = tests + .iter() + .filter_map(|test| { + self.disposition(test) + .map(|disposition| (test, disposition)) + }) + .collect(); + selected.sort_by_cached_key(|(test, _)| test.name()); + selected + } + + fn disposition(&self, test: &Test) -> Option { + let name = test.name(); + if !self.filters.is_empty() + && !self + .filters + .iter() + .any(|filter| name.contains(filter.as_str())) + { + return None; + } + if test.ignored && !self.include_ignored { + Some(Disposition::Skip) + } else { + Some(Disposition::Run) + } + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::{Disposition, Selection, Test, validate}; + use crate::run::TestInfo; + + pub(crate) const fn test(module_path: &'static str, name: &'static str, ignored: bool) -> Test { + Test { + info: TestInfo { + name, + description: "A test.", + }, + family: Some("conformance"), + module_path, + ignored, + ignore_reason: None, + run: || Box::pin(async {}), + } + } + + const TESTS: &[Test] = &[ + test("sandbox::smoke", "create_exec_delete", false), + test("sandbox::lifecycle", "stop_start", false), + test("sandbox::lifecycle", "restart_later", true), + test("smoke", "top_level", false), + ]; + + fn names(selection: &Selection) -> Vec<(String, Disposition)> { + selection + .apply(TESTS) + .into_iter() + .map(|(test, disposition)| (test.name(), disposition)) + .collect() + } + + #[test] + fn names_follow_libtest() { + assert_eq!(TESTS[0].name(), "smoke::create_exec_delete"); + assert_eq!(TESTS[3].name(), "top_level"); + } + + #[test] + fn selection_follows_libtest() { + use Disposition::{Run, Skip}; + let all = Selection::default(); + assert_eq!( + names(&all), + [ + ("lifecycle::restart_later".into(), Skip), + ("lifecycle::stop_start".into(), Run), + ("smoke::create_exec_delete".into(), Run), + ("top_level".into(), Run), + ] + ); + + let filtered = Selection { + filters: vec!["stop".into(), "top".into()], + ..Selection::default() + }; + assert_eq!( + names(&filtered), + [ + ("lifecycle::stop_start".into(), Run), + ("top_level".into(), Run) + ] + ); + + let include = Selection { + filters: vec!["lifecycle".into()], + include_ignored: true, + }; + assert_eq!( + names(&include), + [ + ("lifecycle::restart_later".into(), Run), + ("lifecycle::stop_start".into(), Run) + ] + ); + } + + #[test] + fn validate_rejects_ambiguous_names() { + assert!(validate(TESTS).is_ok()); + let duplicate = [test("a::m", "t", false), test("b::m", "t", false)]; + let error = validate(&duplicate).unwrap_err(); + assert!(error.contains("two tests are named m::t"), "{error}"); + let reserved = [test("suite", "run", false)]; + let error = validate(&reserved).unwrap_err(); + assert!(error.contains("name of a subcommand"), "{error}"); + assert!(validate(&[test("suite::m", "run", false)]).is_ok()); + } +} diff --git a/tests/testkit/testkit/tests/attribute.rs b/tests/testkit/testkit/tests/attribute.rs new file mode 100644 index 0000000000..1f49adea40 --- /dev/null +++ b/tests/testkit/testkit/tests/attribute.rs @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! What `#[testkit::test]` expands to, checked by compiling tests with it in +//! an ordinary libtest target. The tests need a gateway, so they are ignored; +//! the check compares the registry with what libtest lists. + +use std::collections::BTreeSet; +use std::process::Command; + +/// Ignored with a reason. +#[testkit::test] +#[ignore = "needs a gateway"] +async fn ignored_with_reason(_cx: &mut testkit::Context) -> testkit::Result { + Ok(()) +} + +/// Lint attributes reach the body: `cx` is unused. +#[testkit::test] +#[ignore] +#[allow(unused_variables)] +async fn lint_allowed(cx: &mut testkit::Context) -> testkit::Result { + Ok(()) +} + +/// Compiled out, so neither a test nor registered. +#[testkit::test] +#[cfg(any())] +async fn compiled_out(_cx: &mut testkit::Context) -> testkit::Result { + Ok(()) +} + +mod nested { + /// Named with its module. + #[testkit::test] + #[ignore] + async fn in_a_module(_cx: &mut testkit::Context) -> testkit::Result { + Ok(()) + } +} + +/// The names libtest lists for this binary, with `extra` arguments. +fn libtest_list(extra: &[&str]) -> BTreeSet { + let output = Command::new(std::env::current_exe().expect("current exe")) + .args(["--list", "--format", "terse"]) + .args(extra) + .output() + .expect("list the tests"); + assert!(output.status.success(), "{output:?}"); + String::from_utf8(output.stdout) + .expect("utf-8") + .lines() + .filter_map(|line| line.strip_suffix(": test")) + .filter(|name| *name != "registry_matches_libtest") + .map(str::to_string) + .collect() +} + +#[test] +fn registry_matches_libtest() { + let registered: Vec<_> = testkit::__private::TESTS + .iter() + .map(|test| { + ( + test.name(), + test.module_path, + test.family, + test.ignored, + test.ignore_reason, + test.info.description, + ) + }) + .collect::>() + .into_iter() + .collect(); + assert_eq!( + registered, + [ + ( + "ignored_with_reason".to_string(), + "attribute", + None, + true, + Some("needs a gateway"), + "Ignored with a reason." + ), + ( + "lint_allowed".to_string(), + "attribute", + None, + true, + None, + "Lint attributes reach the body: `cx` is unused." + ), + ( + "nested::in_a_module".to_string(), + "attribute::nested", + None, + true, + None, + "Named with its module." + ), + ] + ); + + // libtest runs and ignores the same tests, under the same names. + let names = |ignored_only: bool| -> BTreeSet { + testkit::__private::TESTS + .iter() + .filter(|test| test.ignored || !ignored_only) + .map(testkit::Test::name) + .collect() + }; + assert_eq!(libtest_list(&[]), names(false)); + assert_eq!(libtest_list(&["--ignored"]), names(true)); +} diff --git a/tests/testkit/testkit/tests/program.rs b/tests/testkit/testkit/tests/program.rs new file mode 100644 index 0000000000..c414e8dd27 --- /dev/null +++ b/tests/testkit/testkit/tests/program.rs @@ -0,0 +1,527 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Checks of `testkit::main()`, as a test program. +//! +//! This binary is three programs, chosen by `TESTKIT_FIXTURE`: +//! +//! - unset: the checks below, run by `cargo test` and nextest through the +//! program's libtest interface; +//! - `fixtures`: tests that pass, fail, hang, ignore `SIGTERM`, or leave +//! processes behind, which the checks run through the program's +//! subcommands; +//! - `registered`: `testkit::main()` over the `#[testkit::test]`s below; +//! - `empty`: a program with no tests. + +#[cfg(unix)] +fn main() -> std::process::ExitCode { + match std::env::var("TESTKIT_FIXTURE").as_deref() { + Ok("fixtures") => testkit::program::main_with(&unix::FIXTURES), + Ok("registered") => testkit::main(), + Ok("empty") => testkit::program::main_with(&[]), + _ => testkit::program::main_with(&unix::CHECKS), + } +} + +#[cfg(not(unix))] +fn main() {} + +/// Needs a gateway. +#[testkit::test] +async fn registered_needs_a_gateway(cx: &mut testkit::Context) -> testkit::Result { + cx.cli(["status"]).await?.success()?; + Ok(()) +} + +/// Ignored until later. +#[testkit::test] +#[ignore = "https://github.com/NVIDIA/OpenShell/issues/1"] +async fn registered_ignored(_cx: &mut testkit::Context) -> testkit::Result { + Ok(()) +} + +#[cfg(unix)] +mod unix { + use std::future::Future; + use std::os::unix::process::CommandExt as _; + use std::path::{Path, PathBuf}; + use std::pin::Pin; + use std::process::{Child, Command, Output, Stdio}; + use std::time::{Duration, Instant}; + + use testkit::{Test, TestInfo}; + + const PID_FILE_ENV: &str = "FIXTURE_PID_FILE"; + + type Run = fn() -> Pin>>; + + const fn test(module_path: &'static str, name: &'static str, run: Run) -> Test { + Test { + info: TestInfo { + name, + description: "A fixture.", + }, + family: Some("fixtures"), + module_path, + ignored: false, + ignore_reason: None, + run, + } + } + + const fn fixture(name: &'static str, run: Run) -> Test { + test("program::fixture", name, run) + } + + const fn check(name: &'static str, run: Run) -> Test { + test("program::check", name, run) + } + + fn spawn_helper_and_record_its_pid() { + // A helper in the test's process group, like a CLI process. It is + // never waited on: the supervisor must kill it. + #[allow(clippy::zombie_processes)] + let helper = Command::new("sleep") + .arg("300") + .spawn() + .expect("spawn sleep"); + let pid_file = std::env::var(PID_FILE_ENV).expect("pid file"); + std::fs::write(pid_file, helper.id().to_string()).expect("write pid"); + } + + pub static FIXTURES: [Test; 7] = [ + fixture("passes", || { + Box::pin(async { + println!("passes: stdout"); + eprintln!("passes: stderr"); + }) + }), + fixture("fails", || Box::pin(async { panic!("fixture failure") })), + fixture("hangs", || { + Box::pin(async { + spawn_helper_and_record_its_pid(); + std::thread::sleep(Duration::from_secs(300)); + }) + }), + fixture("leaves_a_helper", || { + Box::pin(async { spawn_helper_and_record_its_pid() }) + }), + fixture("interrupts_after_exit", || { + Box::pin(async { + // Signal the supervisor once this test has exited, while it + // waits for the test's output to close. The helper leaves + // the test's process group so that it survives the test, and + // holds the output open until it has sent the signal. + let supervisor = std::os::unix::process::parent_id(); + #[allow(clippy::zombie_processes)] + let _signaller = Command::new("sh") + .args(["-c", &format!("sleep 1; kill -TERM {supervisor}")]) + .process_group(0) + .spawn() + .expect("spawn the signaller"); + }) + }), + fixture("ignores_sigterm", || { + Box::pin(async { + // SAFETY: plain syscall. + unsafe { + libc::signal(libc::SIGTERM, libc::SIG_IGN); + } + std::thread::sleep(Duration::from_secs(300)); + }) + }), + Test { + ignored: true, + ignore_reason: Some("not today"), + ..fixture("ignored", || Box::pin(async {})) + }, + ]; + + pub static CHECKS: [Test; 12] = [ + check("nextest_interface", || { + Box::pin(async { nextest_interface() }) + }), + check("list_shows_name_and_description", || { + Box::pin(async { list_shows_name_and_description() }) + }), + check("run_reports_every_outcome", || { + Box::pin(async { run_reports_every_outcome() }) + }), + check("run_exit_codes", || Box::pin(async { run_exit_codes() })), + check("a_program_without_tests_fails", || { + Box::pin(async { a_program_without_tests_fails() }) + }), + check("timeout_kills_the_process_group", || { + Box::pin(async { timeout_kills_the_process_group() }) + }), + check("sigterm_is_followed_by_sigkill", || { + Box::pin(async { sigterm_is_followed_by_sigkill() }) + }), + check("interrupt_stops_the_test_and_the_run", || { + Box::pin(async { interrupt_stops_the_test_and_the_run() }) + }), + check("interrupt_between_tests_ends_the_run", || { + Box::pin(async { interrupt_between_tests_ends_the_run() }) + }), + check("a_passing_test_leaves_nothing_behind", || { + Box::pin(async { a_passing_test_leaves_nothing_behind() }) + }), + check("registered_tests_run_through_testkit", || { + Box::pin(async { registered_tests_run_through_testkit() }) + }), + check("ready_fails_without_a_gateway", || { + Box::pin(async { ready_fails_without_a_gateway() }) + }), + ]; + + fn program(fixture: &str, args: &[&str]) -> Command { + let mut command = Command::new(std::env::current_exe().expect("current exe")); + command + .args(args) + .env("TESTKIT_FIXTURE", fixture) + .env_remove("TESTKIT_SUPERVISED") + .stdin(Stdio::null()); + command + } + + fn output(mut command: Command) -> Output { + let output = command.output().expect("run the program"); + eprintln!( + "{command:?}: {}\n--- stdout\n{}--- stderr\n{}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + output + } + + fn stdout(output: &Output) -> &str { + std::str::from_utf8(&output.stdout).expect("utf-8") + } + + fn stderr(output: &Output) -> &str { + std::str::from_utf8(&output.stderr).expect("utf-8") + } + + fn junit(dir: &Path) -> (PathBuf, String) { + let path = dir.join("results").join("junit.xml"); + (path.clone(), path.to_str().expect("utf-8 path").to_string()) + } + + /// nextest lists with `--list --format terse [--ignored]` and runs each + /// test with ` --exact --nocapture`. + fn nextest_interface() { + let list = output(program("fixtures", &["--list", "--format", "terse"])); + assert!(list.status.success()); + assert_eq!( + stdout(&list), + "fixture::fails: test\nfixture::hangs: test\nfixture::ignored: test\n\ + fixture::ignores_sigterm: test\nfixture::interrupts_after_exit: test\n\ + fixture::leaves_a_helper: test\nfixture::passes: test\n" + ); + let ignored = output(program( + "fixtures", + &["--list", "--format", "terse", "--ignored"], + )); + assert_eq!(stdout(&ignored), "fixture::ignored: test\n"); + + let pass = output(program( + "fixtures", + &["fixture::passes", "--exact", "--nocapture"], + )); + assert!(pass.status.success()); + assert!(stdout(&pass).contains("passes: stdout")); + assert!(stdout(&pass).contains("1 passed")); + + let fail = output(program( + "fixtures", + &["fixture::fails", "--exact", "--nocapture"], + )); + assert_eq!(fail.status.code(), Some(101)); + assert!(stderr(&fail).contains("fixture failure")); + + // An ignored test runs only when asked for. + let skip = output(program("fixtures", &["fixture::ignored", "--exact"])); + assert!(stdout(&skip).contains("1 ignored")); + let run = output(program( + "fixtures", + &["fixture::ignored", "--exact", "--ignored"], + )); + assert!(stdout(&run).contains("1 passed")); + } + + fn list_shows_name_and_description() { + let list = output(program("registered", &["list"])); + assert!(list.status.success()); + let lines: Vec<_> = stdout(&list).lines().collect(); + assert_eq!(lines.len(), 2, "{lines:?}"); + assert_eq!( + lines[0], + "registered_ignored Ignored until later. \ + [ignored: https://github.com/NVIDIA/OpenShell/issues/1]" + ); + assert_eq!(lines[1], "registered_needs_a_gateway Needs a gateway."); + + let filtered = output(program("fixtures", &["list", "pass"])); + assert_eq!(stdout(&filtered), "fixture::passes A fixture.\n"); + } + + fn run_reports_every_outcome() { + let dir = tempfile::tempdir().expect("temp dir"); + let (path, junit_arg) = junit(dir.path()); + let run = output(program( + "fixtures", + &["run", "passes", "fails", "ignored", "--junit", &junit_arg], + )); + assert_eq!(run.status.code(), Some(1)); + // Test output is passed through. + assert!(stdout(&run).contains("passes: stdout")); + assert!(stderr(&run).contains("passes: stderr")); + assert!(stderr(&run).contains("1 passed, 1 failed, 1 skipped")); + assert!(stderr(&run).contains("FAIL fixture::fails: test failed")); + + let xml = std::fs::read_to_string(path).expect("junit report"); + for expected in [ + r#""#, + r#""#, + r#""#, + "", + "passes: stdout", + "fixture failure", + ] { + assert!(xml.contains(expected), "missing {expected} in\n{xml}"); + } + } + + fn run_exit_codes() { + let pass = output(program("fixtures", &["run", "passes"])); + assert_eq!(pass.status.code(), Some(0)); + let ignored = output(program( + "fixtures", + &["run", "ignored", "--include-ignored"], + )); + assert_eq!(ignored.status.code(), Some(0)); + assert!(stderr(&ignored).contains("1 passed, 0 failed")); + // Selecting nothing to run is not a pass. + for args in [&["run", "nothing-matches"][..], &["run", "ignored"]] { + let none = output(program("fixtures", args)); + assert_eq!(none.status.code(), Some(2), "{args:?}"); + assert!(stderr(&none).contains("no tests to run")); + } + let usage = output(program("fixtures", &["run", "--timeout", "0"])); + assert_eq!(usage.status.code(), Some(2)); + } + + /// A program whose suites were not linked fails rather than passing with + /// no tests, under every interface. + fn a_program_without_tests_fails() { + for args in [ + &["list"][..], + &["run"], + &["--list", "--format", "terse"], + &[], + ] { + let empty = output(program("empty", args)); + assert_eq!(empty.status.code(), Some(2), "{args:?}"); + assert!( + stderr(&empty).contains("no tests are linked into this program"), + "{args:?}" + ); + } + } + + fn is_alive(pid: libc::pid_t) -> bool { + // SAFETY: signal 0 only checks the process exists. + unsafe { libc::kill(pid, 0) == 0 } + } + + /// Wait for a killed helper to go: once its parent is gone, init reaps it. + fn assert_gone(pid: libc::pid_t) { + let deadline = Instant::now() + Duration::from_secs(10); + while is_alive(pid) { + assert!(Instant::now() < deadline, "helper {pid} outlived its test"); + std::thread::sleep(Duration::from_millis(50)); + } + } + + fn read_pid(path: &Path) -> libc::pid_t { + let deadline = Instant::now() + Duration::from_secs(30); + loop { + if let Ok(pid) = std::fs::read_to_string(path) + && let Ok(pid) = pid.trim().parse() + { + return pid; + } + assert!(Instant::now() < deadline, "the hanging test never started"); + std::thread::sleep(Duration::from_millis(20)); + } + } + + fn timeout_kills_the_process_group() { + let dir = tempfile::tempdir().expect("temp dir"); + let pid_file = dir.path().join("pid"); + let (path, junit_arg) = junit(dir.path()); + let mut command = program( + "fixtures", + &[ + "run", + "hangs", + "--timeout", + "1", + "--grace", + "5", + "--junit", + &junit_arg, + ], + ); + command.env(PID_FILE_ENV, &pid_file); + let started = Instant::now(); + let run = output(command); + assert_eq!(run.status.code(), Some(1)); + // `SIGTERM` stops the test well inside the grace period. + assert!( + started.elapsed() < Duration::from_secs(5), + "{:?}", + started.elapsed() + ); + assert!(stderr(&run).contains("TIMEOUT fixture::hangs: timed out after 1s")); + assert_gone(read_pid(&pid_file)); + let xml = std::fs::read_to_string(path).expect("junit report"); + assert!(xml.contains(r#"type="timeout""#), "{xml}"); + } + + fn sigterm_is_followed_by_sigkill() { + let started = Instant::now(); + let run = output(program( + "fixtures", + &[ + "run", + "ignores_sigterm", + "--timeout", + "0.2", + "--grace", + "0.5", + ], + )); + assert_eq!(run.status.code(), Some(1)); + let elapsed = started.elapsed(); + assert!(elapsed >= Duration::from_millis(700), "{elapsed:?}"); + assert!(elapsed < Duration::from_secs(10), "{elapsed:?}"); + assert!(stderr(&run).contains("TIMEOUT fixture::ignores_sigterm")); + } + + fn interrupt_stops_the_test_and_the_run() { + let dir = tempfile::tempdir().expect("temp dir"); + let pid_file = dir.path().join("pid"); + let (path, junit_arg) = junit(dir.path()); + let mut command = program( + "fixtures", + &[ + "run", "hangs", "passes", "--grace", "5", "--junit", &junit_arg, + ], + ); + command + .env(PID_FILE_ENV, &pid_file) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + let child: Child = command.spawn().expect("start the run"); + let helper = read_pid(&pid_file); + // SAFETY: plain syscall to our own child, not yet reaped. + unsafe { + libc::kill( + libc::pid_t::try_from(child.id()).expect("pid"), + libc::SIGTERM, + ); + } + let run = child.wait_with_output().expect("wait for the run"); + eprintln!("{}", String::from_utf8_lossy(&run.stderr)); + assert_eq!(run.status.code(), Some(2)); + assert!(stderr(&run).contains("ERROR fixture::hangs: interrupted")); + assert_gone(helper); + let xml = std::fs::read_to_string(path).expect("junit report"); + assert!(xml.contains(r#"type="interrupted""#), "{xml}"); + assert!(!xml.contains("fixture::passes"), "{xml}"); + } + + fn interrupt_between_tests_ends_the_run() { + let dir = tempfile::tempdir().expect("temp dir"); + let (path, junit_arg) = junit(dir.path()); + let run = output(program( + "fixtures", + &[ + "run", + "interrupts_after_exit", + "passes", + "--junit", + &junit_arg, + ], + )); + assert_eq!(run.status.code(), Some(2)); + assert!(stderr(&run).contains("<== PASS fixture::interrupts_after_exit")); + assert!(stderr(&run).contains("interrupted; 1 later test did not run")); + assert!(!stdout(&run).contains("passes: stdout")); + let xml = std::fs::read_to_string(path).expect("junit report"); + assert!(!xml.contains("fixture::passes"), "{xml}"); + } + + fn a_passing_test_leaves_nothing_behind() { + let dir = tempfile::tempdir().expect("temp dir"); + let pid_file = dir.path().join("pid"); + let mut command = program("fixtures", &["run", "leaves_a_helper"]); + command.env(PID_FILE_ENV, &pid_file); + let run = output(command); + assert_eq!(run.status.code(), Some(0)); + assert_gone(read_pid(&pid_file)); + } + + /// Point the gateway lookup at a closed port, away from any registered + /// gateways. + fn without_gateway(mut command: Command, config: &Path) -> Command { + let closed = std::net::TcpListener::bind("127.0.0.1:0").expect("bind"); + let endpoint = format!("http://{}", closed.local_addr().expect("address")); + drop(closed); + command + .env("XDG_CONFIG_HOME", config) + .env("OPENSHELL_GATEWAY_ENDPOINT", endpoint) + .env_remove("OPENSHELL_GATEWAY") + .env("OPENSHELL_BIN", config.join("no-openshell")); + command + } + + fn registered_tests_run_through_testkit() { + let dir = tempfile::tempdir().expect("temp dir"); + let (path, junit_arg) = junit(dir.path()); + let run = output(without_gateway( + program("registered", &["run", "--junit", &junit_arg]), + dir.path(), + )); + assert_eq!(run.status.code(), Some(1)); + // `testkit::run` logs the description and the gateway. + assert!(stderr(&run).contains("[registered_needs_a_gateway] Needs a gateway.")); + assert!(stderr(&run).contains("1 failed, 1 skipped")); + let xml = std::fs::read_to_string(path).expect("junit report"); + assert!( + xml.contains(r#""#), + "{xml}" + ); + } + + fn ready_fails_without_a_gateway() { + let dir = tempfile::tempdir().expect("temp dir"); + let started = Instant::now(); + let ready = output(without_gateway( + program("registered", &["ready", "--timeout", "1"]), + dir.path(), + )); + assert_eq!(ready.status.code(), Some(2)); + assert!(stderr(&ready).contains("not ready after 1s")); + assert!(started.elapsed() < Duration::from_secs(10)); + } +} diff --git a/tests/tmachine/ansible/playbooks/openshell-k3s.yaml b/tests/tmachine/ansible/playbooks/openshell-k3s.yaml index 2f03f9d7a8..5b68bc0067 100644 --- a/tests/tmachine/ansible/playbooks/openshell-k3s.yaml +++ b/tests/tmachine/ansible/playbooks/openshell-k3s.yaml @@ -34,6 +34,9 @@ src: "{{ openshell_sandbox_image }}" - name: supervisor src: "{{ openshell_supervisor_image }}" + # The testkit's python_image() sandbox workload. + - name: e2e-python + src: "{{ openshell_e2e_python_image }}" - name: Import OpenShell images into K3s ansible.builtin.command: @@ -49,6 +52,7 @@ - gateway - sandbox - supervisor + - e2e-python - name: Copy OpenShell Helm chart ansible.builtin.copy: @@ -56,6 +60,41 @@ dest: /var/lib/openshell/artifacts/helm-chart.tgz mode: "0600" + # Tests run in the guest, so host.openshell.internal must resolve to the + # node itself. A tmachine environment is one VM, so the cluster has one + # node; the check below fails if that stops being true, because then the + # address must be the one of the node the tests run on. QEMU user + # networking gives every boot the same guest address (10.0.2.15), so the + # value stays valid when the disk is reused. + # The node can also list an IPv6 address after it, so take the IPv4 one. + - name: Discover the K3s node address + ansible.builtin.command: + argv: + - /usr/local/bin/k3s + - kubectl + - --request-timeout=10s + - get + - nodes + - --output=jsonpath={.items[0].status.addresses[?(@.type=="InternalIP")].address} + register: k3s_node_address + changed_when: false + until: >- + k3s_node_address.rc == 0 + and k3s_node_address.stdout.split() | select('match', '^[0-9.]+$') | list | length > 0 + retries: 30 + delay: 2 + + - name: Check the K3s cluster has a single node + ansible.builtin.command: + argv: [/usr/local/bin/k3s, kubectl, --request-timeout=10s, get, nodes, --output=name] + register: k3s_nodes + changed_when: false + failed_when: k3s_nodes.rc != 0 or k3s_nodes.stdout_lines | length != 1 + + - name: Select the K3s node IPv4 address + ansible.builtin.set_fact: + k3s_node_ipv4: "{{ k3s_node_address.stdout.split() | select('match', '^[0-9.]+$') | first }}" + - name: Write OpenShell Helm values ansible.builtin.copy: dest: /var/lib/openshell/artifacts/values.yaml @@ -85,6 +124,7 @@ auth: allowUnauthenticatedUsers: true disableTls: true + hostGatewayIP: "{{ k3s_node_ipv4 }}" telemetryEnabled: false - name: Write OpenShell Helm values overlay diff --git a/tests/tmachine/ansible/roles/openshell_gateway/tasks/docker.yaml b/tests/tmachine/ansible/roles/openshell_gateway/tasks/docker.yaml index 3585c1d33f..11b033972a 100644 --- a/tests/tmachine/ansible/roles/openshell_gateway/tasks/docker.yaml +++ b/tests/tmachine/ansible/roles/openshell_gateway/tasks/docker.yaml @@ -2,7 +2,9 @@ # SPDX-License-Identifier: Apache-2.0 --- -- name: Copy OpenShell runtime images +# Tests run sandboxes from the e2e-python workload image (the testkit's +# python_image()), so load it next to the runtime images. +- name: Copy OpenShell runtime and test workload images become: true ansible.builtin.copy: src: "{{ item.src }}" @@ -15,8 +17,10 @@ src: "{{ openshell_sandbox_image }}" - name: supervisor src: "{{ openshell_supervisor_image }}" + - name: e2e-python + src: "{{ openshell_e2e_python_image }}" -- name: Load OpenShell runtime images +- name: Load OpenShell runtime and test workload images become: true ansible.builtin.command: argv: @@ -27,3 +31,4 @@ loop: - sandbox - supervisor + - e2e-python diff --git a/tests/tmachine/ansible/roles/openshell_gateway/tasks/podman.yaml b/tests/tmachine/ansible/roles/openshell_gateway/tasks/podman.yaml index 784c56f6cb..b85361b769 100644 --- a/tests/tmachine/ansible/roles/openshell_gateway/tasks/podman.yaml +++ b/tests/tmachine/ansible/roles/openshell_gateway/tasks/podman.yaml @@ -2,7 +2,9 @@ # SPDX-License-Identifier: Apache-2.0 --- -- name: Copy OpenShell runtime images +# Tests run sandboxes from the e2e-python workload image (the testkit's +# python_image()), so load it next to the runtime images. +- name: Copy OpenShell runtime and test workload images become: true ansible.builtin.copy: src: "{{ item.src }}" @@ -15,8 +17,10 @@ src: "{{ openshell_sandbox_image }}" - name: supervisor src: "{{ openshell_supervisor_image }}" + - name: e2e-python + src: "{{ openshell_e2e_python_image }}" -- name: Load OpenShell runtime images +- name: Load OpenShell runtime and test workload images become: true become_user: "{{ openshell_gateway_user }}" ansible.builtin.command: @@ -28,6 +32,7 @@ loop: - sandbox - supervisor + - e2e-python environment: HOME: "{{ openshell_gateway_home }}" XDG_RUNTIME_DIR: "/run/user/{{ openshell_gateway_uid }}" diff --git a/tests/tmachine/config.nix b/tests/tmachine/config.nix index ac1aa28d6d..24206097f9 100644 --- a/tests/tmachine/config.nix +++ b/tests/tmachine/config.nix @@ -23,9 +23,12 @@ let OVMF = firmwarePkgs.OVMF; }; qemu = qemuPkgs.qemu.override { hostCpuOnly = true; }; + # Every installer loads the sandbox workload image tests run python3 in. + e2ePythonImage = "../../artifacts/images/openshell-e2e-python-dev.tar"; k3sInputs = { agent_sandbox_version = "0.5.0"; openshell_cli_binary = "../../artifacts/binaries/${muslTarget}/openshell"; + openshell_e2e_python_image = e2ePythonImage; openshell_gateway_image = "../../artifacts/images/openshell-gateway-tmachine.tar"; openshell_helm_chart = "../../artifacts/helm/helm-chart-0.0.0.tgz"; openshell_sandbox_image = "../../artifacts/images/openshell-sandbox-tmachine.tar"; @@ -204,6 +207,7 @@ let inputs = { openshell_cli_binary = "../../artifacts/binaries/${muslTarget}/openshell"; openshell_gateway_binary = "../../artifacts/binaries/${gnuTarget}/openshell-gateway"; + openshell_e2e_python_image = e2ePythonImage; openshell_supervisor_image = "../../artifacts/images/openshell-supervisor-tmachine.tar"; openshell_sandbox_image = "../../artifacts/images/openshell-sandbox-tmachine.tar"; }; @@ -216,6 +220,7 @@ let ]; inputs = { openshell_deb = "../../artifacts/packages/openshell.deb"; + openshell_e2e_python_image = e2ePythonImage; openshell_supervisor_image = "../../artifacts/images/openshell-supervisor-tmachine.tar"; openshell_sandbox_image = "../../artifacts/images/openshell-sandbox-tmachine.tar"; }; @@ -227,6 +232,7 @@ let inputs = { openshell_rpm = "../../artifacts/packages/rpm/openshell.rpm"; openshell_gateway_rpm = "../../artifacts/packages/rpm/openshell-gateway.rpm"; + openshell_e2e_python_image = e2ePythonImage; openshell_supervisor_image = "../../artifacts/images/openshell-supervisor-tmachine.tar"; openshell_sandbox_image = "../../artifacts/images/openshell-sandbox-tmachine.tar"; }; @@ -245,7 +251,7 @@ let playbooks = [ "ansible/playbooks/drivers/podman/e2e.yaml" ]; inputs = { openshell_podman_e2e_test_bundle = "../../artifacts/test-archives/${muslTarget}/openshell-podman-e2e-tests.tar"; - openshell_podman_e2e_workload_image = "../../artifacts/images/openshell-e2e-python-dev.tar"; + openshell_podman_e2e_workload_image = e2ePythonImage; }; } ]; diff --git a/tests/tmachine/src/ansible.rs b/tests/tmachine/src/ansible.rs index 3301934954..c9b37f91f1 100644 --- a/tests/tmachine/src/ansible.rs +++ b/tests/tmachine/src/ansible.rs @@ -28,6 +28,13 @@ pub async fn install_roles() -> Result<()> { pub async fn run(playbook: &Path, inputs: &BTreeMap) -> Result<()> { let mut command = Command::new("ansible-playbook"); + // Newer systemd guests (Fedora) print OSC 3008 shell-integration markers + // on every terminal session: from a profile script for login shells and + // from pam_systemd for `become`, which Ansible runs over `ssh -tt`. + // Ansible reports them as "Module invocation had junk after the JSON + // data". Both emitters skip a dumb terminal, so don't forward the caller's + // TERM. + command.env("TERM", "dumb"); // The VM's SSH forward may not be the inventory's default port. command .arg("--extra-vars")