├── .dockerignore ├── .env.sample ├── .formatter.exs ├── .github ├── CODEOWNERS └── workflows │ └── elixir.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── NOTICE ├── README.md ├── build_server.sh ├── config ├── config.exs ├── dev.exs ├── prod.exs ├── releases.exs ├── splunk.exs └── test.exs ├── docker-compose.yaml ├── docker-entrypoint-dev.sh ├── docker-source.sh ├── lib ├── schema.ex ├── schema │ ├── application.ex │ ├── cache.ex │ ├── generator.ex │ ├── graph.ex │ ├── helper.ex │ ├── json_reader.ex │ ├── json_schema.ex │ ├── profiles.ex │ ├── repo.ex │ ├── translator.ex │ ├── types.ex │ ├── utils.ex │ ├── validator.ex │ └── validator2.ex ├── schema_web.ex ├── schema_web │ ├── channels │ │ └── user_socket.ex │ ├── controllers │ │ ├── page_controller.ex │ │ └── schema_controller.ex │ ├── endpoint.ex │ ├── router.ex │ ├── templates │ │ ├── layout │ │ │ └── app.html.eex │ │ └── page │ │ │ ├── category.html.eex │ │ │ ├── class.html.eex │ │ │ ├── class_graph.html.eex │ │ │ ├── classes.html.eex │ │ │ ├── data_types.html.eex │ │ │ ├── dictionary.html.eex │ │ │ ├── index.html.eex │ │ │ ├── object.html.eex │ │ │ ├── object_graph.html.eex │ │ │ ├── objects.html.eex │ │ │ ├── profile.html.eex │ │ │ └── profiles.html.eex │ └── views │ │ ├── error_view.ex │ │ ├── layout_view.ex │ │ └── page_view.ex └── schemas.ex ├── mix.exs ├── mix.lock ├── priv ├── cert │ └── placeholder ├── data │ ├── country-and-continent-codes-list.json │ ├── enterprise-tactics.json │ ├── files.txt │ ├── names.txt │ ├── service_names.txt │ ├── techniques.json │ └── words.txt └── static │ ├── apple-touch-icon.png │ ├── css │ ├── app.css │ ├── base.css │ ├── bootstrap-4.6.2.min.css │ ├── bootstrap.select-1.13.18.min.css │ ├── components.css │ ├── fontawesome-6.4.0.min.css │ ├── layout.css │ ├── tables.css │ └── variables.css │ ├── favicon.ico │ ├── images │ ├── ocsf-logo.png │ └── ocsf.png │ ├── js │ ├── app.js │ ├── bootstrap-4.6.2.bundle.min.js │ ├── bootstrap.select-1.13.18.min.js │ ├── jquery-3.5.1.slim.min.js │ ├── popper.min.js │ ├── profiles.js │ ├── sorttable.js │ └── vis-network.min.js │ ├── robots.txt │ └── webfonts │ └── fa-solid-900.woff2 ├── rel ├── env.sh.eex ├── overlays │ └── bin │ │ ├── server │ │ └── server.bat └── vm.args.eex └── test ├── schema ├── check_enums.exs ├── compilation_test.exs └── update_types.exs ├── schema_web └── views │ └── page_view_test.exs ├── support ├── channel_case.ex └── conn_case.ex ├── test_helper.exs └── test_ocsf_schema ├── categories.json ├── dictionary.json ├── events ├── alpha.json ├── base_event.json ├── beta.json ├── eta.json ├── ghost_(hidden).json ├── network.json └── race_flagged.json ├── extensions └── rpg │ ├── categories.json │ ├── dictionary.json │ ├── events │ ├── alpha_(patch).json │ ├── damage.json │ └── eta_(patch).json │ ├── extension.json │ └── objects │ ├── device_(patch).json │ ├── hammer.json │ ├── war_hammer.json │ └── zeta_(patch).json ├── extensions2 └── rpg2 │ ├── extension.json │ └── objects │ ├── device_(patch).json │ ├── hammer_(patch).json │ └── sword.json ├── objects ├── _entity_(hidden).json ├── _zeta_base_(hidden).json ├── device.json ├── endpoint.json ├── metadata.json ├── network_endpoint.json ├── network_node.json ├── observable.json ├── service.json ├── specific_device.json └── zeta.json ├── profiles ├── datetime.json └── host.json └── version.json /.dockerignore: -------------------------------------------------------------------------------- 1 | # This file excludes paths from the Docker build context. 2 | # 3 | # By default, Docker's build context includes all files (and folders) in the 4 | # current directory. Even if a file isn't copied into the container it is still sent to 5 | # the Docker daemon. 6 | # 7 | # There are multiple reasons to exclude files from the build context: 8 | # 9 | # 1. Prevent nested folders from being copied into the container (ex: exclude 10 | # /assets/node_modules when copying /assets) 11 | # 2. Reduce the size of the build context and improve build time (ex. /build, /deps, /doc) 12 | # 3. Avoid sending files containing sensitive information 13 | # 14 | # More information on using .dockerignore is available here: 15 | # https://docs.docker.com/engine/reference/builder/#dockerignore-file 16 | 17 | .dockerignore 18 | 19 | # Ignore git, but keep git HEAD and refs to access current commit hash if needed: 20 | # 21 | # $ git show HEAD --pretty=full | head -n 1 22 | # commit d0b8727759e1e0e7aa3d41707d12376e373d5ecc 23 | .git 24 | !.git/HEAD 25 | !.git/refs 26 | 27 | # Common development/test artifacts 28 | /cover/ 29 | /doc/ 30 | /test/ 31 | /tmp/ 32 | .elixir_ls 33 | 34 | # Mix artifacts 35 | /_build/ 36 | /deps/ 37 | /.mix/ 38 | *.ez 39 | 40 | # Generated on crash by the VM 41 | erl_crash.dump 42 | 43 | # Static artifacts - These should be fetched and built inside the Docker image 44 | /assets/node_modules/ 45 | /priv/static/assets/ 46 | /priv/static/cache_manifest.json 47 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | SCHEMA_PATH=../ocsf-schema # Set the local schema path, eg. ../ocsf-schema, defaults to ./modules/schema 2 | OCSF_SERVER_PORT=8080 # Set the port for Docker to listen on for forwarding traffic to the Schema Server, defaults to 8080 3 | ELIXIR_VERSION=1.14-alpine # Set the Elixir container version for development, defaults to 1.14-alpine 4 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:phoenix], 3 | inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default Owners for everything in the repo 2 | * @rmouritzen-splunk @floydtree @mikeradka -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | 15 | name: Build and test 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Set up Elixir 21 | uses: erlef/setup-beam@v1 22 | with: 23 | elixir-version: '1.14.0' # Define the elixir version [required] 24 | otp-version: '25' # Define the OTP version [required] 25 | - name: Restore dependencies cache 26 | uses: actions/cache@v3 27 | with: 28 | path: deps 29 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 30 | restore-keys: ${{ runner.os }}-mix- 31 | - name: Install dependencies 32 | run: mix deps.get 33 | - name: Run tests 34 | run: mix compile 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # The directory Mix will write the release files to. 5 | /dist/ 6 | 7 | # If you run "mix test --cover", coverage assets end up here. 8 | /cover/ 9 | 10 | # The directory Mix downloads your dependencies sources to. 11 | /deps/ 12 | 13 | # Where 3rd-party dependencies like ExDoc output generated docs. 14 | /doc/ 15 | 16 | /.idea/ 17 | /.elixir_ls/ 18 | /.vscode/ 19 | 20 | # Ignore .fetch files in case you like to edit your project deps locally. 21 | /.fetch 22 | 23 | # If the VM crashes, it generates a dump, let's ignore it too. 24 | erl_crash.dump 25 | 26 | # Also ignore archive artifacts (built via "mix archive.build"). 27 | *.ez 28 | 29 | # Files matching config/*.secret.exs pattern contain sensitive 30 | # data and you should not commit them into version control. 31 | # 32 | # Alternatively, you may comment the line below and commit the 33 | # secrets files as long as you replace their contents by environment 34 | # variables. 35 | /config/*.secret.exs 36 | 37 | .version 38 | 39 | # Ignore /.env files used to define local environment variables 40 | /.env 41 | 42 | # Ignore /.mix/ folder 43 | /.mix/ 44 | 45 | # Ignore generated doc file 46 | priv/static/swagger.json 47 | 48 | /log -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG elixir_image=elixir:1.18.3-alpine 2 | 3 | FROM ${elixir_image} AS builder 4 | 5 | # prepare build dir 6 | WORKDIR /app 7 | 8 | RUN apk --update add openssl 9 | 10 | # install hex + rebar 11 | RUN mix local.hex --force && \ 12 | mix local.rebar --force 13 | 14 | # set build ENV 15 | ENV MIX_ENV="prod" 16 | 17 | # Set this magic ERL_FLAGS value to allow cross-compilation from Apple Silicon. 18 | # This (apparently) fixes a known QEMU issue. See: 19 | # * https://elixirforum.com/t/elixir-docker-image-wont-build-for-linux-arm64-v8-using-github-actions/56383/12 20 | # * https://elixirforum.com/t/unable-to-compile-default-elixir-project-from-the-getting-started-guide/57199/12 21 | ENV ERL_FLAGS="+JPperf true" 22 | 23 | # install mix dependencies 24 | COPY mix.exs mix.lock ./ 25 | RUN mix deps.get --only $MIX_ENV 26 | 27 | RUN mkdir config 28 | 29 | # copy compile-time config files before we compile dependencies 30 | # to ensure any relevant config change will trigger the dependencies 31 | # to be re-compiled. 32 | COPY config/config.exs config/${MIX_ENV}.exs config/ 33 | RUN mix deps.compile 34 | 35 | COPY priv priv 36 | COPY lib lib 37 | 38 | # Compile the release 39 | RUN mix compile 40 | 41 | # Generate ssl certificate 42 | RUN openssl req -new -newkey rsa:4096 -days 365 -nodes -sha256 -x509 -subj "/C=US/ST=CA/L=ocsf/O=ocsf.io/CN=ocsf-schema" -keyout priv/cert/selfsigned_key.pem -out priv/cert/selfsigned.pem 43 | 44 | # Changes to config/runtime.exs don't require recompiling the code 45 | COPY config/releases.exs config/runtime.exs 46 | 47 | COPY rel rel 48 | RUN mix release 49 | 50 | # start a new build stage so that the final image will only contain 51 | # the compiled release and other runtime necessities 52 | FROM ${elixir_image} 53 | 54 | ENV LANG=en_US.UTF-8 55 | ENV LANGUAGE=en_US:en 56 | ENV LC_ALL=en_US.UTF-8 57 | 58 | WORKDIR "/app" 59 | EXPOSE 8080 60 | EXPOSE 8443 61 | RUN chown nobody /app 62 | 63 | # set runner ENV 64 | ENV MIX_ENV="prod" 65 | ENV PORT=8080 66 | ENV SCHEMA_DIR="/app/schema" 67 | ENV SCHEMA_EXTENSION="extensions" 68 | 69 | # Only copy the final release from the build stage 70 | COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/schema_server ./ 71 | 72 | USER nobody 73 | 74 | CMD ["/app/bin/server"] 75 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Open Cybersecurity Schema Framework 2 | 3 | Copyright © OCSF a Series of LF Projects, LLC 4 | For web site terms of use, trademark policy and other project policies please see https://lfprojects.org. 5 | 6 | This content includes the ICD Schema developed by Symantec, a division of Broadcom. 7 | 8 | The schema and schema files are available under the Apache 2 license. 9 | Refer to the Apache 2 license in the file LICENSE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Open Cybersecurity Schema Framework Server 2 | This repository contains the Open Cybersecurity Schema Framework (OCSF) Schema Server source code. 3 | The schema server is an HTTP server that provides a convenient way to browse and use the OCSF schema. 4 | 5 | You can access the OCSF schema server, which is running the latest released schema, at [schema.ocsf.io](https://schema.ocsf.io). 6 | 7 | The schema server can be also used locally. To do that clone the `ocsf-server` and `ocsf-schema` repositories and follow the instruction below to build and run the schema server. 8 | 9 | ## Clone the OCFS schema (`ocsf-schema`) repository 10 | ```shell 11 | git clone https://github.com/ocsf/ocsf-schema.git 12 | ``` 13 | 14 | ## Clone the OCFS schema server (`ocsf-server`) repository 15 | ```shell 16 | git clone https://github.com/ocsf/ocsf-server.git 17 | ``` 18 | 19 | ## Build a server docker image 20 | ```shell 21 | cd ocsf-server 22 | docker build -t ocsf-server . 23 | ``` 24 | 25 | ## Run the server docker image 26 | Change the `/path/to` to your local OCSF schema directory (use an absolute path). Note, the `-p 8443:8443` parameter enables HTTPS with a self-signed SSL certificate. 27 | 28 | ```shell 29 | docker run -it --rm --volume /path/to/ocsf-schema:/app/schema -p 8080:8080 -p 8443:8443 ocsf-server 30 | ``` 31 | 32 | For example, if the `ocsf-schema` and `ocsf-server` repos were cloned to the local directory `~/github-projects`, this would be the proper replacement for `/path/to`: 33 | ```shell 34 | docker run -it --rm --volume ~/github-projects/ocsf/ocsf-schema:/app/schema -p 8080:8080 -p 8443:8443 ocsf-server 35 | ``` 36 | 37 | (Note that paths used for volume mounts with `docker run` cannot be relative.) 38 | 39 | To access the schema server, open [`localhost:8080`](http://localhost:8080) or [`localhost:8443`](https://localhost:8443) in your Web browser. 40 | 41 | ## Run the server docker image with a local schema extension: 42 | ```shell 43 | docker run -it --rm --volume /path/to/ocsf-schema:/app/schema --volume /path/to/ocsf-schema:/app/extension -e SCHEMA_EXTENSION="/app/extension" -p 8080:8080 -p 8443:8443 ocsf-server 44 | ``` 45 | 46 | ## Running against multiple versions of the schema 47 | The OCSF Server does not directly support hosting multiple versions. To present multiple version, each version is hosted by a different instance of the server, with support from a small amount of code to enable finding alternate versions and switching to other instances. The public OCSF Server does this by fronting the web site with Nginx and running each server instance in separate Docker containers. 48 | 49 | The following answer in this repo's discussions area covers the gory details: 50 | * [How can you run this server with more than one schema version? #131](https://github.com/ocsf/ocsf-server/discussions/131). 51 | 52 | ## Development with docker-compose 53 | **NOTE:** The `docker-compose` approach is not currently being actively maintained. It is left in the repo since it may work and may be helpful for some people. 54 | 55 | The `docker-compose` environment enables development without needing to install any dependencies (apart from Docker/Podman and docker-compose) on the development machine. 56 | 57 | When run, the standard `_build` and `deps` folders are created, along with a `.mix` folder. If the environment needs to be recreated for whatever reason, the `_build` folder can be removed and `docker-compose` brought down and up again and the environment will automatically rebuild. 58 | 59 | ### Run the ocsf-server and build the development container 60 | ```shell 61 | docker-compose up 62 | ``` 63 | 64 | Then browse to the schema server at http://localhost:8080 65 | 66 | ### Testing the schema with docker-compose 67 | **NOTE:** it is _not_ necessary to run the server with `docker-compose up` first in order to test the schema (or run any other commands in the development container). 68 | 69 | ``` 70 | # docker-compose run ocsf-elixir mix test 71 | Creating ocsf-server_ocsf-elixir_run ... done 72 | Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg. 73 | 74 | 75 | Finished in 0.00 seconds (0.00s async, 0.00s sync) 76 | 0 failures 77 | 78 | Randomized with seed 933777 79 | ``` 80 | 81 | ### Set aliases to avoid docker-compose inflicted RSI 82 | ```shell 83 | source docker-source.sh 84 | ``` 85 | 86 | ### Using aliases to run docker-compose commands 87 | ``` 88 | # testschema 89 | Creating ocsf-server_ocsf-elixir_run ... done 90 | Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg. 91 | 92 | 93 | Finished in 0.00 seconds (0.00s async, 0.00s sync) 94 | 0 failures 95 | 96 | Randomized with seed 636407 97 | ``` 98 | 99 | ### Using environment variables to change docker-compose defaults 100 | Optional environment variables can be placed in a `.env` file in the root of the repo to change the default behavior. 101 | 102 | An `.env.sample` is provided, and the following options are available: 103 | 104 | ``` 105 | SCHEMA_PATH=../ocsf-schema # Set the local schema path, eg. ../ocsf-schema, defaults to ../ocsf-schema 106 | OCSF_SERVER_PORT=8080 # Set the port for Docker to listen on for forwarding traffic to the Schema server, defaults to 8080 107 | ELIXIR_VERSION=otp-25-alpine # Set the Elixir container version for development, defaults to otp-25-alpine 108 | ``` 109 | 110 | ## Local Usage 111 | This section describes how to build and run the OCSF Schema server. 112 | 113 | ### Required build tools 114 | The Schema server is written in [Elixir](https://elixir-lang.org) using the [Phoenix](https://phoenixframework.org/) Web framework. 115 | 116 | The Elixir site maintains a great installation page, see https://elixir-lang.org/install.html for help. 117 | 118 | ### Building the schema server 119 | Elixir uses the [`mix`](https://hexdocs.pm/mix/Mix.html) build tool, which is included in the Elixir installation package. 120 | 121 | #### Install the build tools 122 | ```shell 123 | mix local.hex --force && mix local.rebar --force 124 | ``` 125 | 126 | #### Get the dependencies 127 | Change to the schema directory, fetch and compile the dependencies: 128 | 129 | ```shell 130 | cd ocsf-server 131 | mix do deps.get, deps.compile 132 | ``` 133 | 134 | #### Compile the source code 135 | ```shell 136 | mix compile 137 | ``` 138 | 139 | ### Testing local schema changes 140 | You can use `mix test` command to test the changes made to the schema. For example to ensure the JSON files are correct or the attributes are defined. 141 | 142 | Assuming the schema repo has been cloned in `../ocsf-schema` directory, then you can test the schema with this command: 143 | 144 | ```shell 145 | SCHEMA_DIR=../ocsf-schema SCHEMA_EXTENSION=extensions mix test 146 | ``` 147 | 148 | If everything is correct, then you should not see any errors or warnings. 149 | 150 | ### Running the schema server 151 | You can use the Elixir's interactive shell, [IEx](https://hexdocs.pm/iex/IEx.html), to start the schema server use: 152 | 153 | ```shell 154 | SCHEMA_DIR=../ocsf-schema SCHEMA_EXTENSION=extensions iex -S mix phx.server 155 | ``` 156 | 157 | Now you can access the Schema server at [`localhost:8080`](http://localhost:8080) or [`localhost:8443`](https://localhost:8443). 158 | 159 | ### Reloading the schema 160 | You can use the following command in the `iex` shell to force reloading the schema with extensions: 161 | 162 | ```elixir 163 | Schema.reload(["", "", ...]) 164 | ``` 165 | 166 | Reload the core schema without extensions: 167 | 168 | ```elixir 169 | Schema.reload() 170 | ``` 171 | 172 | Reload the schema only with the `linux` extension (note the folder is relative to the `SCHEMA_DIR` folder): 173 | 174 | ```elixir 175 | Schema.reload(["extensions/linux"]) 176 | ``` 177 | 178 | Reload the schema with all extensions defined in the `extensions` folder (note the folder is relative to the `SCHEMA_DIR` folder): 179 | 180 | ```elixir 181 | Schema.reload(["extensions"]) 182 | ``` 183 | 184 | Reload the schema with extensions defined outside the `SCHEMA_DIR` folder (use an absolute or relative path): 185 | 186 | ```elixir 187 | Schema.reload(["/home/schema/cloud", "../dev-ext"]) 188 | ``` 189 | 190 | ### Runtime configuration 191 | The schema server uses a number of environment variables. 192 | 193 | | Variable Name | Description | 194 | | ---------------- | ----------- | 195 | | HTTP_PORT | The server HTTP port number, default: `8080`| 196 | | HTTPS_PORT | The server HTTPS port number, default: `8443`| 197 | | SCHEMA_DIR | The directory containing the schema, default: `../ocsf-schema` | 198 | | SCHEMA_EXTENSION | The directory containing the schema extensions, relative to SCHEMA_DIR or absolute path | 199 | | RELEASE_NODE | The Erlang node name. Set it if you want to run more than one server on the same computer | 200 | 201 | ```shell 202 | SCHEMA_DIR=../ocsf-schema SCHEMA_EXTENSION=extensions iex -S mix phx.server 203 | ``` 204 | -------------------------------------------------------------------------------- /build_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # fetch and compile the dependencies 4 | MIX_ENV=prod mix do deps.get, deps.compile 5 | 6 | # compile the schema server 7 | MIX_ENV=prod mix compile 8 | 9 | # make a release package 10 | MIX_ENV=prod mix release --path dist 11 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | port = System.get_env("HTTP_PORT") || System.get_env("PORT") || 8080 4 | path = System.get_env("SCHEMA_PATH") || "/" 5 | 6 | # Configures the endpoint 7 | config :schema_server, SchemaWeb.Endpoint, 8 | http: [port: port], 9 | secret_key_base: "HUvG8AlzaUpVx5PShWbGv6JpifzM/d46Rj3mxAIddA7DJ9qKg6df8sG6PsKXScAh", 10 | render_errors: [view: SchemaWeb.ErrorView, accepts: ~w(html json)], 11 | pubsub_server: Schema.PubSub 12 | 13 | # Configures Elixir's Logger 14 | config :logger, :console, 15 | handle_otp_reports: true, 16 | handle_sasl_reports: true, 17 | format: "$date $time [$level] $metadata $message\n", 18 | metadata: [:request_id, :mfa, :line] 19 | 20 | # Use Jason for JSON parsing in Phoenix 21 | config :phoenix, :json_library, Jason 22 | 23 | config :schema_server, :phoenix_swagger, 24 | swagger_files: %{ 25 | "priv/static/swagger.json" => [ 26 | router: SchemaWeb.Router, 27 | endpoint: SchemaWeb.Endpoint 28 | ] 29 | } 30 | 31 | config :phoenix_swagger, json_library: Jason 32 | 33 | # Configures the location of the schema files 34 | config :schema_server, Schema.Application, home: System.get_env("SCHEMA_DIR") || "../ocsf-schema" 35 | config :schema_server, Schema.Application, extension: System.get_env("SCHEMA_EXTENSION") 36 | config :schema_server, Schema.Application, schema_home: System.get_env("SCHEMA_HOME") 37 | 38 | # Import environment specific config. This must remain at the bottom 39 | # of this file so it overrides the configuration defined above. 40 | import_config "#{Mix.env()}.exs" 41 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we use it 8 | # with webpack to recompile .js and .css sources. 9 | config :schema_server, SchemaWeb.Endpoint, 10 | debug_errors: true, 11 | code_reloader: true, 12 | check_origin: false, 13 | watchers: [] 14 | 15 | # Watch static and templates for browser reloading. 16 | config :schema_server, SchemaWeb.Endpoint, 17 | live_reload: [ 18 | patterns: [ 19 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$}, 20 | ~r{lib/schema_web/views/.*(ex)$}, 21 | ~r{lib/schema_web/templates/.*(eex|md)$} 22 | ] 23 | ] 24 | 25 | # If you are doing OTP releases, you need to instruct Phoenix 26 | # to start the server for all endpoints: 27 | # 28 | # config :phoenix, :serve_endpoints, true 29 | 30 | config :logger, :console, 31 | level: :debug, 32 | format: "[$level] $metadata $message\n", 33 | metadata: [:mfa, :line] 34 | 35 | # Set a higher stacktrace during development. Avoid configuring such 36 | # in production as building large stacktraces may be expensive. 37 | config :phoenix, :stacktrace_depth, 20 38 | 39 | # Initialize plugs at runtime for faster development compilation 40 | config :phoenix, :plug_init_mode, :runtime 41 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # Do not print debug messages in production 4 | config :logger, 5 | level: :warning 6 | 7 | config :phoenix, :serve_endpoints, true 8 | -------------------------------------------------------------------------------- /config/releases.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # For production, don't forget to configure the url host 4 | # to something meaningful, Phoenix uses this information 5 | # when generating URLs. 6 | # 7 | # Note we also include the path to a cache manifest 8 | # containing the digested version of static files. This 9 | # manifest is generated by the `mix phx.digest` task, 10 | # which you should run after static files are built and 11 | # before starting your production server. 12 | port_ssl = System.get_env("HTTPS_PORT") || 8443 13 | certfile = System.get_env("HTTPS_CERT_FILE") || "priv/cert/selfsigned.pem" 14 | keyfile = System.get_env("HTTPS_KEY_FILE") || "priv/cert/selfsigned_key.pem" 15 | 16 | config :schema_server, SchemaWeb.Endpoint, 17 | force_ssl: [rewrite_on: [:x_forwarded_proto], exclude: ["localhost"]], 18 | http: [port: System.get_env("PORT") || 8000], 19 | https: [ 20 | port: port_ssl, 21 | cipher_suite: :strong, 22 | certfile: certfile, 23 | keyfile: keyfile 24 | ], 25 | url: [ 26 | host: System.get_env("HOST") || "localhost", 27 | port: System.get_env("URL_PORT") || 8000, 28 | path: System.get_env("SCHEMA_PATH") || "/" 29 | ] 30 | 31 | # Configures the location of the schema files 32 | config :schema_server, Schema.Application, home: System.get_env("SCHEMA_DIR") 33 | config :schema_server, Schema.Application, extension: System.get_env("SCHEMA_EXTENSION") 34 | 35 | config :schema_server, Schema.Application, schema_home: System.get_env("SCHEMA_HOME") 36 | -------------------------------------------------------------------------------- /config/splunk.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # cache_static_manifest: "priv/static/cache_manifest.json" 4 | 5 | # Do not print debug messages in production 6 | config :logger, 7 | format: "$time $metadata[$level] $message\n", 8 | level: :info 9 | 10 | # ## SSL Support 11 | # 12 | # To get SSL working, you will need to add the `https` key 13 | # to the previous section and set your `:url` port to 443: 14 | # 15 | config :schema_server, SchemaWeb.Endpoint, 16 | https: [ 17 | port: 443, 18 | cipher_suite: :strong, 19 | keyfile: "/etc/ssl/private/splunk-selfsigned.key", 20 | certfile: "/etc/ssl/certs/splunk-selfsigned.crt" 21 | ] 22 | 23 | # 24 | # The `cipher_suite` is set to `:strong` to support only the 25 | # latest and more secure SSL ciphers. This means old browsers 26 | # and clients may not be supported. You can set it to 27 | # `:compatible` for wider support. 28 | # 29 | # `:keyfile` and `:certfile` expect an absolute path to the key 30 | # and cert in disk or a relative path inside priv, for example 31 | # "priv/ssl/server.key". For all supported SSL configuration 32 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 33 | # 34 | # We also recommend setting `force_ssl` in your endpoint, ensuring 35 | # no data is ever sent via http, always redirecting to https: 36 | # 37 | # config :schema, SchemaWeb.Endpoint, 38 | # force_ssl: [hsts: true] 39 | # 40 | # Check `Plug.SSL` for all available options in `force_ssl`. 41 | 42 | # ## Using releases (distillery) 43 | # 44 | # If you are doing OTP releases, you need to instruct Phoenix 45 | # to start the server for all endpoints: 46 | # 47 | config :phoenix, :serve_endpoints, true 48 | # 49 | # Alternatively, you can configure exactly which server to 50 | # start per endpoint: 51 | # 52 | # config :schema, SchemaWeb.Endpoint, server: true 53 | # 54 | # Note you can't rely on `System.get_env/1` when using releases. 55 | # See the releases documentation accordingly. 56 | 57 | # Finally import the config/prod.secret.exs which should be versioned 58 | # separately. 59 | # import_config "prod.secret.exs" 60 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :schema_server, SchemaWeb.Endpoint, 6 | http: [port: System.get_env("PORT") || 8000], 7 | server: false 8 | 9 | # Configure the logger to write to a file in test mode 10 | config :logger, 11 | level: :warning, 12 | backends: [{LoggerFileBackend, :test_log}] 13 | 14 | config :logger, :test_log, 15 | path: "log/test_#{System.system_time(:millisecond)}.log", 16 | level: :debug, 17 | format: "$time $metadata[$level] $message\n", 18 | metadata: [:request_id] 19 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | ocsf-server: 4 | build: . 5 | ports: 6 | - "${OCSF_SERVER_PORT:-8080}:8080" 7 | working_dir: /ocsf-server 8 | environment: 9 | - "SCHEMA_DIR=/app/schema" 10 | volumes: 11 | - "./:/ocsf-server:z" 12 | - "${SCHEMA_PATH:-../ocsf-schema}:/app/schema:z" 13 | 14 | ocsf-elixir: 15 | image: "elixir:${ELIXIR_VERSION:-1.14-alpine}" 16 | working_dir: /ocsf-server 17 | entrypoint: /docker-entrypoint-dev.sh 18 | command: echo # dummy command 19 | environment: 20 | - "SCHEMA_DIR=/app/schema" 21 | - "MIX_HOME=/ocsf-server/.mix" 22 | volumes: 23 | - "./:/ocsf-server:z" 24 | - "${SCHEMA_PATH:-../ocsf-schema}:/app/schema:z" 25 | - "./docker-entrypoint-dev.sh:/docker-entrypoint-dev.sh" 26 | -------------------------------------------------------------------------------- /docker-entrypoint-dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd /ocsf-server 4 | if [ ! -d ./_build ]; then 5 | echo "_build folder not found, removing .mix and deps/ and running a build." 6 | rm -Rf .mix/ 7 | rm -Rf deps/ 8 | mix local.hex --force 9 | mix local.rebar --force 10 | mix do deps.get, deps.compile 11 | mix compile 12 | fi 13 | 14 | exec "$@" 15 | -------------------------------------------------------------------------------- /docker-source.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | alias testschema="docker-compose run ocsf-elixir mix test" 3 | -------------------------------------------------------------------------------- /lib/schema/application.ex: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # http://www.apache.org/licenses/LICENSE-2.0 5 | # Unless required by applicable law or agreed to in writing, software 6 | # distributed under the License is distributed on an "AS IS" BASIS, 7 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | # See the License for the specific language governing permissions and 9 | # limitations under the License. 10 | defmodule Schema.Application do 11 | # See https://hexdocs.pm/elixir/Application.html 12 | # for more information on OTP Applications 13 | @moduledoc false 14 | 15 | use Application 16 | 17 | @spec start(any, any) :: {:error, any} | {:ok, pid} 18 | def start(_type, _args) do 19 | env = Application.get_env(:schema_server, __MODULE__) 20 | 21 | schema_dir = Keyword.get(env, :home) 22 | schemas_dir = Keyword.get(env, :schema_home) 23 | 24 | extensions = 25 | case Keyword.get(env, :extension) do 26 | nil -> [] 27 | ext -> String.split(ext, ",") 28 | end 29 | 30 | # List all child processes to be supervised 31 | children = [ 32 | {Schemas, schemas_dir}, 33 | {Schema.JsonReader, [schema_dir, extensions]}, 34 | Schema.Repo, 35 | Schema.Generator, 36 | 37 | # Start the endpoint when the application starts 38 | SchemaWeb.Endpoint, 39 | {Phoenix.PubSub, [name: Schema.PubSub, adapter: Phoenix.PubSub.PG2]} 40 | ] 41 | 42 | # See https://hexdocs.pm/elixir/Supervisor.html 43 | # for other strategies and supported options 44 | opts = [strategy: :one_for_one, name: Schema.Supervisor] 45 | Supervisor.start_link(children, opts) 46 | end 47 | 48 | # Tell Phoenix to update the endpoint configuration 49 | # whenever the application is updated. 50 | def config_change(changed, _new, removed) do 51 | SchemaWeb.Endpoint.config_change(changed, removed) 52 | :ok 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/schema/graph.ex: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # http://www.apache.org/licenses/LICENSE-2.0 5 | # Unless required by applicable law or agreed to in writing, software 6 | # distributed under the License is distributed on an "AS IS" BASIS, 7 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | # See the License for the specific language governing permissions and 9 | # limitations under the License. 10 | defmodule Schema.Graph do 11 | @moduledoc """ 12 | This module generates graph data to display event class diagram. 13 | """ 14 | 15 | @doc """ 16 | Builds graph data for the given class. 17 | """ 18 | @spec build(map()) :: map() 19 | def build(class) do 20 | %{ 21 | nodes: build_nodes(class), 22 | edges: build_edges(class) |> Enum.uniq(), 23 | class: Map.delete(class, :attributes) |> Map.delete(:objects) |> Map.delete(:_links) 24 | } 25 | end 26 | 27 | defp build_nodes(class) do 28 | node = 29 | Map.new() 30 | |> Map.put(:color, "#F5F5C8") 31 | |> Map.put(:id, make_id(class.name, class[:extension])) 32 | |> Map.put(:label, class.caption) 33 | 34 | build_nodes([node], class) 35 | end 36 | 37 | defp build_nodes(nodes, class) do 38 | Map.get(class, :objects) 39 | |> Enum.reduce(nodes, fn {_name, obj}, acc -> 40 | node = %{ 41 | id: make_id(obj.name, obj[:extension]), 42 | label: obj.caption 43 | } 44 | 45 | # Don't add class/object that is already added (present infinite loop) 46 | if not nodes_member?(nodes, node) do 47 | [node | acc] 48 | else 49 | acc 50 | end 51 | end) 52 | end 53 | 54 | defp make_id(name, nil) do 55 | name 56 | end 57 | 58 | defp make_id(name, ext) do 59 | Path.join(ext, name) 60 | end 61 | 62 | defp nodes_member?(nodes, node) do 63 | Enum.any?(nodes, fn n -> n.id == node.id end) 64 | end 65 | 66 | defp build_edges(class) do 67 | objects = Map.new(class.objects) 68 | build_edges([], class, objects) 69 | end 70 | 71 | defp build_edges(edges, class, objects) do 72 | Map.get(class, :attributes) 73 | |> Enum.reduce(edges, fn {name, obj}, acc -> 74 | case obj.type do 75 | "object_t" -> 76 | # For a recursive definition, we need to add the edge, creating the looping edge 77 | # and then we don't want to continue searching this path. 78 | recursive? = edges_member?(acc, obj) 79 | 80 | edge = 81 | %{ 82 | source: Atom.to_string(obj[:_source]), 83 | group: obj[:group], 84 | requirement: obj[:requirement] || "optional", 85 | from: make_id(class.name, class[:extension]), 86 | to: obj.object_type, 87 | label: Atom.to_string(name) 88 | } 89 | |> add_profile(obj[:profile]) 90 | 91 | acc = [edge | acc] 92 | 93 | # For recursive definitions, we've already added the edge creating the loop in the graph. 94 | # There's no need to recurse further (avoid infinite loops). 95 | if not recursive? do 96 | o = objects[String.to_atom(obj.object_type)] 97 | build_edges(acc, o, objects) 98 | else 99 | acc 100 | end 101 | 102 | _ -> 103 | acc 104 | end 105 | end) 106 | end 107 | 108 | defp edges_member?(edges, obj) do 109 | Enum.any?(edges, fn edge -> obj.object_type == edge.to end) 110 | end 111 | 112 | defp add_profile(edge, nil) do 113 | edge 114 | end 115 | 116 | defp add_profile(edge, profile) do 117 | Map.put(edge, :profile, profile) 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/schema/helper.ex: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # http://www.apache.org/licenses/LICENSE-2.0 5 | # Unless required by applicable law or agreed to in writing, software 6 | # distributed under the License is distributed on an "AS IS" BASIS, 7 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | # See the License for the specific language governing permissions and 9 | # limitations under the License. 10 | defmodule Schema.Helper do 11 | @moduledoc """ 12 | Provides helper functions to enrich the event data. 13 | """ 14 | require Logger 15 | 16 | def enrich(data, enum_text, observables) when is_map(data) do 17 | Logger.debug(fn -> 18 | "enrich event: #{inspect(data)}, enum_text: #{enum_text}, observables: #{observables}" 19 | end) 20 | 21 | enrich_class(data["class_uid"], data, enum_text, observables) 22 | end 23 | 24 | # this is not an event 25 | def enrich(data, _enum_text, _observables), do: %{:error => "Not a JSON object", :data => data} 26 | 27 | # missing class_uid 28 | defp enrich_class(nil, data, _enum_text, _observables), 29 | do: %{:error => "Missing class_uid", :data => data} 30 | 31 | defp enrich_class(class_uid, data, enum_text, _observables) do 32 | Logger.debug("enrich class: #{class_uid}") 33 | 34 | # if observables == "true", do: 35 | 36 | case Schema.find_class(class_uid) do 37 | # invalid event class ID 38 | nil -> 39 | %{:error => "Invalid class_uid: #{class_uid}", :data => data} 40 | 41 | class -> 42 | data = type_uid(class_uid, data) 43 | 44 | if enum_text == "true" do 45 | enrich_type(class, data) 46 | else 47 | data 48 | end 49 | end 50 | end 51 | 52 | defp enrich_type(type, data) do 53 | attributes = type[:attributes] 54 | 55 | Enum.reduce(data, %{}, fn {name, value}, acc -> 56 | key = to_atom(name) 57 | 58 | case attributes[key] do 59 | # Attribute name is not defined in the schema 60 | nil -> 61 | Map.put(acc, name, value) 62 | 63 | attribute -> 64 | {name, text} = enrich_attribute(attribute[:type], name, attribute, value) 65 | 66 | if Map.has_key?(attribute, :enum) do 67 | Logger.debug("enrich enum: #{name} = #{text}") 68 | 69 | case attribute[:sibling] do 70 | nil -> 71 | Map.put_new(acc, name, value) 72 | 73 | sibling -> 74 | Map.put_new(acc, name, value) |> Map.put_new(sibling, text) 75 | end 76 | else 77 | Map.put(acc, name, text) 78 | end 79 | end 80 | end) 81 | end 82 | 83 | defp type_uid(class_uid, data) do 84 | case data["activity_id"] do 85 | nil -> 86 | data 87 | 88 | activity_id -> 89 | uid = 90 | if activity_id >= 0 do 91 | Schema.Types.type_uid(class_uid, activity_id) 92 | else 93 | 0 94 | end 95 | 96 | Map.put(data, "type_uid", uid) 97 | end 98 | end 99 | 100 | defp to_atom(key) when is_atom(key), do: key 101 | defp to_atom(key), do: String.to_atom(key) 102 | 103 | defp enrich_attribute("integer_t", name, attribute, value) do 104 | enrich_integer(attribute[:enum], name, value) 105 | end 106 | 107 | defp enrich_attribute("object_t", name, attribute, value) when is_map(value) do 108 | {name, enrich_type(Schema.object(attribute[:object_type]), value)} 109 | end 110 | 111 | defp enrich_attribute("object_t", name, attribute, value) when is_list(value) do 112 | data = 113 | if attribute[:is_array] and is_map(List.first(value)) do 114 | obj_type = Schema.object(attribute[:object_type]) 115 | 116 | Enum.map(value, fn data -> 117 | enrich_type(obj_type, data) 118 | end) 119 | else 120 | value 121 | end 122 | 123 | {name, data} 124 | end 125 | 126 | defp enrich_attribute(_, name, _attribute, value) do 127 | {name, value} 128 | end 129 | 130 | # Integer value 131 | defp enrich_integer(nil, name, value) do 132 | {name, value} 133 | end 134 | 135 | # Single enum value 136 | defp enrich_integer(enum, name, value) when is_integer(value) do 137 | key = Integer.to_string(value) |> String.to_atom() 138 | 139 | {name, caption(enum[key], value)} 140 | end 141 | 142 | # Array of enum values 143 | defp enrich_integer(enum, name, values) when is_list(values) do 144 | list = 145 | Enum.map(values, fn n -> 146 | key = Integer.to_string(n) |> String.to_atom() 147 | caption(enum[key], key) 148 | end) 149 | 150 | {name, list} 151 | end 152 | 153 | # Non-integer value 154 | defp enrich_integer(_, name, value), 155 | do: {name, value} 156 | 157 | defp caption(nil, value), do: value 158 | defp caption(map, _value), do: map[:caption] 159 | end 160 | -------------------------------------------------------------------------------- /lib/schema/json_schema.ex: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # http://www.apache.org/licenses/LICENSE-2.0 5 | # Unless required by applicable law or agreed to in writing, software 6 | # distributed under the License is distributed on an "AS IS" BASIS, 7 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | # See the License for the specific language governing permissions and 9 | # limitations under the License. 10 | defmodule Schema.JsonSchema do 11 | @moduledoc """ 12 | Json schema generator. This module defines functions that generate JSON schema (see http://json-schema.org) schemas for OCSF schema. 13 | """ 14 | @schema_base_uri "https://schema.ocsf.io/schema/classes" 15 | @schema_version "http://json-schema.org/draft-07/schema#" 16 | 17 | @doc """ 18 | Generates a JSON schema corresponding to the `type` parameter. 19 | The `type` can be either a class or an object defintion. 20 | 21 | Options: :package_name | :schema_version 22 | """ 23 | @spec encode(map(), nil | Keyword.t()) :: map() 24 | def encode(type, options) when is_map(type) do 25 | Process.put(:options, options || []) 26 | 27 | try do 28 | encode(type) 29 | after 30 | Process.delete(:options) 31 | end 32 | end 33 | 34 | def encode(type) do 35 | name = type[:name] 36 | 37 | {properties, required, just_one, at_least_one} = map_reduce(name, type) 38 | 39 | ext = type[:extension] 40 | 41 | if Map.has_key?(type, :_links) do 42 | Map.new() 43 | |> add_java_class(name) 44 | else 45 | class_schema(make_class_ref(name, ext)) 46 | end 47 | |> Map.put("title", type[:caption]) 48 | |> Map.put("type", "object") 49 | |> Map.put("properties", properties) 50 | |> Map.put("additionalProperties", false) 51 | |> put_required(required) 52 | |> put_just_one(just_one) 53 | |> put_at_least_one(at_least_one) 54 | |> encode_objects(type[:objects]) 55 | |> empty_object(properties) 56 | end 57 | 58 | defp add_java_class(obj, name) do 59 | case Process.get(:options) do 60 | nil -> 61 | obj 62 | 63 | options -> 64 | add_java_class(obj, name, Keyword.get(options, :package_name)) 65 | end 66 | end 67 | 68 | defp add_java_class(obj, _name, nil) do 69 | obj 70 | end 71 | 72 | defp add_java_class(obj, name, package) do 73 | Map.put(obj, "javaType", make_java_name(package, name)) 74 | end 75 | 76 | defp make_java_name(package, name) do 77 | name = String.split(name, "_") |> Enum.map_join(fn name -> String.capitalize(name) end) 78 | "#{package}.#{name}" 79 | end 80 | 81 | defp class_schema(id) do 82 | %{ 83 | "$schema" => @schema_version, 84 | "$id" => id, 85 | "additionalProperties" => false 86 | } 87 | end 88 | 89 | defp make_object_ref(name) do 90 | Path.join([ref_object(), String.replace(name, "/", "_")]) 91 | end 92 | 93 | defp ref_object() do 94 | "#/$defs" 95 | end 96 | 97 | defp make_class_ref(name, nil) do 98 | Path.join([@schema_base_uri, name]) 99 | end 100 | 101 | defp make_class_ref(name, ext) do 102 | Path.join([@schema_base_uri, ext, name]) 103 | end 104 | 105 | defp empty_object(map, properties) do 106 | if map_size(properties) == 0 do 107 | Map.put(map, "additionalProperties", true) 108 | else 109 | map 110 | end 111 | end 112 | 113 | defp put_required(map, []) do 114 | map 115 | end 116 | 117 | defp put_required(map, required) do 118 | Map.put(map, "required", Enum.sort(required)) 119 | end 120 | 121 | defp put_just_one(map, []) do 122 | map 123 | end 124 | 125 | defp put_just_one(map, just_one) do 126 | one_of = 127 | Enum.map(just_one, fn item -> 128 | others = Enum.reject(just_one, &(&1 == item)) 129 | 130 | %{ 131 | "required" => [item], 132 | "not" => %{"required" => others} 133 | } 134 | end) 135 | 136 | Map.put(map, "oneOf", one_of) 137 | end 138 | 139 | defp put_at_least_one(map, []) do 140 | map 141 | end 142 | 143 | defp put_at_least_one(map, at_least_one) do 144 | any_of = 145 | Enum.map(at_least_one, fn item -> 146 | %{"required" => [item]} 147 | end) 148 | 149 | Map.put(map, "anyOf", any_of) 150 | end 151 | 152 | defp encode_objects(schema, nil) do 153 | schema 154 | end 155 | 156 | defp encode_objects(schema, []) do 157 | schema 158 | end 159 | 160 | defp encode_objects(schema, objects) do 161 | defs = 162 | Enum.into(objects, %{}, fn {name, object} -> 163 | key = Atom.to_string(name) |> String.replace("/", "_") 164 | {key, encode(object)} 165 | end) 166 | 167 | Map.put(schema, "$defs", defs) 168 | end 169 | 170 | defp map_reduce(type_name, type) do 171 | {properties, {required, just_one, at_least_one}} = 172 | Enum.map_reduce(type[:attributes], {[], [], []}, fn {key, attribute}, 173 | {required, just_one, at_least_one} -> 174 | name = Atom.to_string(key) 175 | just_one_list = List.wrap(type[:constraints][:just_one]) 176 | at_least_one_list = List.wrap(type[:constraints][:at_least_one]) 177 | 178 | cond do 179 | name in just_one_list -> 180 | {required, [name | just_one], at_least_one} 181 | 182 | name in at_least_one_list -> 183 | {required, just_one, [name | at_least_one]} 184 | 185 | attribute[:requirement] == "required" -> 186 | {[name | required], just_one, at_least_one} 187 | 188 | true -> 189 | {required, just_one, at_least_one} 190 | end 191 | |> (fn {required, just_one, at_least_one} -> 192 | schema = 193 | encode_attribute(type_name, attribute[:type], attribute) 194 | |> encode_array(attribute[:is_array]) 195 | 196 | {{name, schema}, {required, just_one, at_least_one}} 197 | end).() 198 | end) 199 | 200 | {Map.new(properties), required, just_one, at_least_one} 201 | end 202 | 203 | defp encode_attribute(_name, "integer_t", attr) do 204 | new_schema(attr) |> encode_integer(attr) 205 | end 206 | 207 | defp encode_attribute(_name, "string_t", attr) do 208 | new_schema(attr) |> encode_string(attr) 209 | end 210 | 211 | defp encode_attribute(name, "object_t", attr) do 212 | new_schema(attr) |> encode_object(name, attr) 213 | end 214 | 215 | defp encode_attribute(_name, "json_t", attr) do 216 | new_schema(attr) 217 | end 218 | 219 | defp encode_attribute(_name, type, attr) do 220 | new_schema(attr) |> Map.put("type", encode_type(type)) 221 | end 222 | 223 | defp new_schema(attr), do: %{"title" => attr[:caption]} 224 | 225 | defp encode_type(type) do 226 | cond do 227 | Schema.data_type?(type, "string_t") -> "string" 228 | Schema.data_type?(type, "integer_t") -> "integer" 229 | Schema.data_type?(type, "long_t") -> "integer" 230 | Schema.data_type?(type, "float_t") -> "number" 231 | Schema.data_type?(type, "boolean_t") -> "boolean" 232 | true -> type 233 | end 234 | end 235 | 236 | defp encode_object(schema, _name, attr) do 237 | type = attr[:object_type] 238 | Map.put(schema, "$ref", make_object_ref(type)) 239 | end 240 | 241 | defp encode_integer(schema, attr) do 242 | encode_enum(schema, attr, "integer", fn name -> 243 | Atom.to_string(name) |> String.to_integer() 244 | end) 245 | end 246 | 247 | defp encode_string(schema, attr) do 248 | encode_enum(schema, attr, "string", &Atom.to_string/1) 249 | end 250 | 251 | defp encode_enum(schema, attr, type, encoder) do 252 | case attr[:enum] do 253 | nil -> 254 | schema 255 | 256 | enum -> 257 | case encode_enum_values(enum, encoder) do 258 | [uid] -> 259 | Map.put(schema, "const", uid) 260 | 261 | values -> 262 | Map.put(schema, "enum", values) 263 | end 264 | end 265 | |> Map.put("type", type) 266 | end 267 | 268 | defp encode_enum_values(enum, encoder) do 269 | Enum.map(enum, fn {name, _} -> 270 | encoder.(name) 271 | end) 272 | end 273 | 274 | defp encode_array(schema, true) do 275 | {type, schema} = items_type(schema) 276 | 277 | Map.put(schema, "type", "array") |> Map.put("items", type) 278 | end 279 | 280 | defp encode_array(schema, _is_array) do 281 | schema 282 | end 283 | 284 | defp items_type(schema) do 285 | case Map.get(schema, "type") do 286 | nil -> 287 | {ref, updated} = Map.pop(schema, "$ref") 288 | {%{"$ref" => ref}, updated} 289 | 290 | type -> 291 | case Map.pop(schema, "enum") do 292 | {nil, updated} -> 293 | {%{"type" => type}, updated} 294 | 295 | {enum, updated} -> 296 | {%{"type" => type, "enum" => enum}, updated} 297 | end 298 | end 299 | end 300 | end 301 | -------------------------------------------------------------------------------- /lib/schema/profiles.ex: -------------------------------------------------------------------------------- 1 | defmodule Schema.Profiles do 2 | @moduledoc """ 3 | Profiles helper functions 4 | """ 5 | 6 | require Logger 7 | 8 | @doc """ 9 | Filter attributes based on a given set of profiles. 10 | """ 11 | def apply_profiles(class, profiles) when is_list(profiles) do 12 | apply_profiles(class, MapSet.new(profiles)) 13 | end 14 | 15 | def apply_profiles(class, %MapSet{} = profiles) do 16 | size = MapSet.size(profiles) 17 | 18 | Map.update!(class, :attributes, fn attributes -> 19 | apply_profiles(attributes, profiles, size) 20 | end) 21 | |> Map.update!(:objects, fn objects -> 22 | Enum.map(objects, fn {name, object} -> 23 | {name, 24 | Map.update!(object, :attributes, fn attributes -> 25 | apply_profiles(attributes, profiles, size) 26 | end)} 27 | end) 28 | end) 29 | end 30 | 31 | defp apply_profiles(attributes, _profiles, 0) do 32 | remove_profiles(attributes) 33 | end 34 | 35 | defp apply_profiles(attributes, profiles, _size) do 36 | Enum.filter(attributes, fn {_k, v} -> 37 | case v[:profile] do 38 | nil -> true 39 | profile -> MapSet.member?(profiles, profile) 40 | end 41 | end) 42 | end 43 | 44 | defp remove_profiles(attributes) do 45 | Enum.filter(attributes, fn {_k, v} -> Map.has_key?(v, :profile) == false end) 46 | end 47 | 48 | @doc """ 49 | Checks items (classes or objects), ensuring that each all profiles defined in each item are 50 | defined in profiles. Adds each properly define profile to profile's _links. 51 | """ 52 | def sanity_check(group, items, profiles) do 53 | Enum.reduce(items, profiles, fn {item_name, item}, acc -> 54 | check_profiles(group, item_name, item, item[:profiles], acc) 55 | end) 56 | end 57 | 58 | # Checks if all profile attributes are defined in the given attribute set. 59 | defp check_profiles(_group, _name, _map, nil, all_profiles) do 60 | all_profiles 61 | end 62 | 63 | defp check_profiles(group, item_name, item, item_profiles, all_profiles) do 64 | Enum.reduce(item_profiles, all_profiles, fn p, acc -> 65 | case acc[p] do 66 | nil -> 67 | Logger.warning("#{item_name} uses undefined profile: #{p}") 68 | acc 69 | 70 | profile -> 71 | link = Schema.Utils.make_link(group, item_name, item) 72 | profile = Map.update(profile, :_links, [link], fn links -> [link | links] end) 73 | Map.put(acc, p, profile) 74 | end 75 | end) 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/schema/translator.ex: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # http://www.apache.org/licenses/LICENSE-2.0 5 | # Unless required by applicable law or agreed to in writing, software 6 | # distributed under the License is distributed on an "AS IS" BASIS, 7 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | # See the License for the specific language governing permissions and 9 | # limitations under the License. 10 | defmodule Schema.Translator do 11 | @moduledoc """ 12 | Translates events to more user friendly form. 13 | """ 14 | require Logger 15 | 16 | def translate(data, options) when is_map(data) do 17 | Logger.debug("translate event: #{inspect(data)}, options: #{inspect(options)}") 18 | 19 | translate_class(data["class_uid"], data, options) 20 | end 21 | 22 | # this is not an event 23 | def translate(data, _options), do: data 24 | 25 | # missing class_uid, thus cannot translate the event 26 | defp translate_class(nil, data, _options), do: data 27 | 28 | defp translate_class(class_uid, data, options) do 29 | Logger.debug("translate class: #{class_uid}") 30 | translate_event(Schema.find_class(class_uid), data, options) 31 | end 32 | 33 | # unknown event class, thus cannot translate the event 34 | defp translate_event(nil, data, _options), do: data 35 | 36 | defp translate_event(type, data, options) do 37 | attributes = type[:attributes] 38 | 39 | Enum.reduce(data, %{}, fn {name, value}, acc -> 40 | Logger.debug("translate attribute: #{name} = #{inspect(value)}") 41 | 42 | key = to_atom(name) 43 | 44 | case attributes[key] do 45 | nil -> 46 | # Attribute name is not defined in the schema 47 | Map.put(acc, name, value) 48 | 49 | attribute -> 50 | {name, text} = translate_attribute(attribute[:type], name, attribute, value, options) 51 | 52 | verbose = Keyword.get(options, :verbose) 53 | 54 | if Map.has_key?(attribute, :enum) and (verbose == 1 or verbose == 2) do 55 | Logger.debug("translated enum: #{name} = #{text}") 56 | 57 | case sibling(attribute[:sibling], attributes, options, verbose) do 58 | nil -> 59 | Map.put_new(acc, name, value) 60 | 61 | sibling -> 62 | Logger.debug("translated name: #{sibling}") 63 | 64 | Map.put_new(acc, name, value) |> Map.put_new(sibling, text) 65 | end 66 | else 67 | Map.put(acc, name, text) 68 | end 69 | end 70 | end) 71 | end 72 | 73 | defp sibling(nil, _attributes, _options, _verbose) do 74 | nil 75 | end 76 | 77 | defp sibling(name, _attributes, _options, 1) do 78 | name 79 | end 80 | 81 | defp sibling(name, attributes, options, _verbose) do 82 | case attributes[String.to_atom(name)] do 83 | nil -> nil 84 | attr -> attr[:caption] |> to_text(options) 85 | end 86 | end 87 | 88 | defp to_atom(key) when is_atom(key), do: key 89 | defp to_atom(key), do: String.to_atom(key) 90 | 91 | defp translate_attribute("integer_t", name, attribute, value, options) do 92 | translate_integer(attribute[:enum], name, attribute, value, options) 93 | end 94 | 95 | defp translate_attribute("object_t", name, attribute, value, options) when is_map(value) do 96 | translated = translate_event(Schema.object(attribute[:object_type]), value, options) 97 | translate_attribute(name, attribute, translated, options) 98 | end 99 | 100 | defp translate_attribute("object_t", name, attribute, value, options) when is_list(value) do 101 | translated = 102 | if attribute[:is_array] and is_map(List.first(value)) do 103 | obj_type = Schema.object(attribute[:object_type]) 104 | 105 | Enum.map(value, fn data -> 106 | translate_event(obj_type, data, options) 107 | end) 108 | else 109 | value 110 | end 111 | 112 | translate_attribute(name, attribute, translated, options) 113 | end 114 | 115 | defp translate_attribute(_, name, attribute, value, options), 116 | do: translate_attribute(name, attribute, value, options) 117 | 118 | # Translate an integer value 119 | defp translate_integer(nil, name, attribute, value, options), 120 | do: translate_attribute(name, attribute, value, options) 121 | 122 | # Translate a single enum value 123 | defp translate_integer(enum, name, attribute, value, options) when is_integer(value) do 124 | item = Integer.to_string(value) |> String.to_atom() 125 | 126 | translated = 127 | case enum[item] do 128 | nil -> value 129 | map -> map[:caption] 130 | end 131 | 132 | translate_enum(name, attribute, value, translated, options) 133 | end 134 | 135 | # Translate an array of enum values 136 | defp translate_integer(enum, name, attribute, value, options) when is_list(value) do 137 | Logger.debug("translate_integer: #{name}") 138 | 139 | translated = 140 | Enum.map(value, fn n -> 141 | item = Integer.to_string(n) |> String.to_atom() 142 | 143 | case enum[item] do 144 | nil -> n 145 | map -> map[:caption] 146 | end 147 | end) 148 | 149 | translate_enum(name, attribute, value, translated, options) 150 | end 151 | 152 | # Translate a non-integer value 153 | defp translate_integer(_, name, attribute, value, options), 154 | do: translate_attribute(name, attribute, value, options) 155 | 156 | defp translate_attribute(name, attribute, value, options) do 157 | case Keyword.get(options, :verbose) do 158 | 2 -> 159 | {to_text(attribute[:caption], options), value} 160 | 161 | 3 -> 162 | {name, 163 | %{ 164 | "name" => to_text(attribute[:caption], options), 165 | "type" => attribute[:object_type] || attribute[:type], 166 | "value" => value 167 | }} 168 | 169 | _ -> 170 | {name, value} 171 | end 172 | end 173 | 174 | defp translate_enum(name, attribute, value, translated, options) do 175 | Logger.debug("translate enum: #{name} = #{value}") 176 | 177 | case Keyword.get(options, :verbose) do 178 | 1 -> 179 | {name, translated} 180 | 181 | 2 -> 182 | {to_text(attribute[:caption], options), translated} 183 | 184 | 3 -> 185 | {name, 186 | %{ 187 | "name" => to_text(attribute[:caption], options), 188 | "type" => attribute[:object_type] || attribute[:type], 189 | "value" => value, 190 | "caption" => translated 191 | }} 192 | 193 | _ -> 194 | {name, value} 195 | end 196 | end 197 | 198 | defp to_text(name, options) do 199 | case Keyword.get(options, :spaces) do 200 | nil -> name 201 | ch -> String.replace(name, " ", ch) 202 | end 203 | end 204 | end 205 | -------------------------------------------------------------------------------- /lib/schema/types.ex: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # http://www.apache.org/licenses/LICENSE-2.0 5 | # Unless required by applicable law or agreed to in writing, software 6 | # distributed under the License is distributed on an "AS IS" BASIS, 7 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | # See the License for the specific language governing permissions and 9 | # limitations under the License. 10 | 11 | defmodule Schema.Types do 12 | @moduledoc """ 13 | Schema types and helpers functions to make unique identifiers. 14 | """ 15 | 16 | @doc """ 17 | Makes a category uid for the given category and extension identifiers. 18 | """ 19 | @spec category_uid(number, number) :: number 20 | def category_uid(extension_uid, category_id), do: extension_uid * 100 + category_id 21 | 22 | @doc """ 23 | Makes a category uid for the given category and extension identifiers. Checks if the 24 | category uid already has the extension. 25 | """ 26 | @spec category_uid_ex(number, number) :: number 27 | def category_uid_ex(extension_uid, category_id) when category_id < 100, 28 | do: category_uid(extension_uid, category_id) 29 | 30 | def category_uid_ex(_extension_uid, category_id), do: category_id 31 | 32 | @doc """ 33 | Makes a class uid for the given class and category identifiers. 34 | """ 35 | @spec class_uid(number, number) :: number 36 | def class_uid(category_uid, class_id), do: category_uid * 1000 + class_id 37 | 38 | @doc """ 39 | Makes a type uid for the given class and activity identifiers. 40 | """ 41 | @spec type_uid(number, number) :: number 42 | def type_uid(class_uid, activity_id), do: class_uid * 100 + activity_id 43 | 44 | @doc """ 45 | Makes type name from class name and type uid enum name. 46 | """ 47 | @spec type_name(binary, binary) :: binary 48 | def type_name(class, name) do 49 | class <> ": " <> name 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /lib/schema/validator.ex: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # http://www.apache.org/licenses/LICENSE-2.0 5 | # Unless required by applicable law or agreed to in writing, software 6 | # distributed under the License is distributed on an "AS IS" BASIS, 7 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | # See the License for the specific language governing permissions and 9 | # limitations under the License. 10 | defmodule Schema.Validator do 11 | @moduledoc """ 12 | OCSF Event validator. 13 | """ 14 | 15 | require Logger 16 | 17 | alias Schema.Utils 18 | 19 | @class_uid "class_uid" 20 | 21 | @doc """ 22 | Validates the given event using `class_uid` value and the schema. 23 | """ 24 | @spec validate(map()) :: map() 25 | def validate(data) when is_map(data), do: data[@class_uid] |> validate_class(data) 26 | def validate(_data), do: %{:error => "Not a JSON object"} 27 | 28 | defp validate_class(nil, _data), do: %{:error => "Missing class_uid"} 29 | 30 | defp validate_class(class_uid, data) do 31 | profiles = get_in(data, ["metadata", "profiles"]) || [] 32 | 33 | Logger.info("validate class: #{class_uid} using profiles: #{inspect(profiles)}") 34 | 35 | case Schema.find_class(class_uid) do 36 | nil -> 37 | class_uid = data[@class_uid] 38 | %{:error => "Invalid class_uid value", :value => class_uid} 39 | 40 | class -> 41 | if is_list(profiles) do 42 | validate_type(class, data, profiles) 43 | else 44 | %{:error => "Invalid profiles value", :value => profiles} 45 | end 46 | end 47 | end 48 | 49 | defp validate_type(type, data, profiles) when is_map(data) do 50 | attributes = type[:attributes] |> Utils.apply_profiles(profiles) 51 | 52 | Enum.reduce(attributes, %{}, fn {name, attribute}, acc -> 53 | value = data[Atom.to_string(name)] 54 | 55 | if attribute[:is_array] == true and value != nil and not is_list(value) do 56 | Map.put(acc, name, invalid_data_type(attribute, value, "array of " <> attribute[:type])) 57 | else 58 | validate_data(acc, name, attribute, value, profiles) 59 | end 60 | end) 61 | |> undefined_attributes(attributes, data) 62 | end 63 | 64 | defp validate_type(type, data, _profiles) do 65 | invalid_data_type(type, data, type.name) 66 | end 67 | 68 | defp undefined_attributes(acc, attributes, data) do 69 | Enum.reduce(data, acc, fn {key, value}, map -> 70 | case attributes[String.to_atom(key)] do 71 | nil -> 72 | Map.put(map, key, %{:error => "Undefined attribute name", :value => value}) 73 | 74 | _attribute -> 75 | map 76 | end 77 | end) 78 | end 79 | 80 | defp validate_data(acc, name, attribute, value, _profiles) when is_binary(value) do 81 | case attribute[:type] do 82 | "string_t" -> 83 | case validate_enum_value(attribute[:enum], value) do 84 | :ok -> acc 85 | error -> Map.put(acc, name, error) 86 | end 87 | 88 | "json_t" -> 89 | acc 90 | 91 | _ -> 92 | validate_data_type(acc, name, attribute, value, "string_t") 93 | end 94 | end 95 | 96 | defp validate_data(acc, name, attribute, value, _profiles) when is_integer(value) do 97 | case attribute[:type] do 98 | "integer_t" -> 99 | case validate_enum(attribute[:enum], attribute, value) do 100 | :ok -> acc 101 | error -> Map.put(acc, name, error) 102 | end 103 | 104 | "json_t" -> 105 | acc 106 | 107 | _ -> 108 | validate_data_type(acc, name, attribute, value, ["integer_t", "long_t"]) 109 | end 110 | end 111 | 112 | defp validate_data(acc, name, attribute, value, _profiles) when is_float(value) do 113 | validate_data_type(acc, name, attribute, value, "float_t") 114 | end 115 | 116 | defp validate_data(acc, name, attribute, value, _profiles) when is_boolean(value) do 117 | validate_data_type(acc, name, attribute, value, "boolean_t") 118 | end 119 | 120 | defp validate_data(acc, name, attribute, value, profiles) when is_map(value) do 121 | case attribute[:type] do 122 | "object_t" -> validate_object(acc, name, attribute, value, profiles) 123 | "json_t" -> acc 124 | type -> Map.put(acc, name, invalid_data_type(attribute, value, type)) 125 | end 126 | end 127 | 128 | defp validate_data(acc, name, attribute, value, profiles) when is_list(value) do 129 | case attribute[:type] do 130 | "json_t" -> 131 | acc 132 | 133 | type -> 134 | if attribute[:is_array] do 135 | validate_array(acc, name, attribute, value, profiles) 136 | else 137 | Map.put(acc, name, invalid_data_type(attribute, value, type)) 138 | end 139 | end 140 | end 141 | 142 | # checks for missing required attributes 143 | defp validate_data(acc, name, attribute, nil, _profiles) do 144 | case attribute[:requirement] do 145 | "required" -> 146 | Map.put(acc, name, %{ 147 | :error => "Missing required attribute" 148 | }) 149 | 150 | _ -> 151 | acc 152 | end 153 | end 154 | 155 | defp validate_data(acc, name, _attribute, value, _profiles) do 156 | Map.put(acc, name, %{ 157 | :error => "Unhanded attribute", 158 | :value => value 159 | }) 160 | end 161 | 162 | defp validate_array(acc, _name, _attribute, [], _profiles) do 163 | acc 164 | end 165 | 166 | defp validate_array(acc, name, attribute, value, profiles) do 167 | Logger.debug("validate array: #{name}") 168 | 169 | case attribute[:type] do 170 | "json_t" -> acc 171 | "object_t" -> validate_object_array(acc, name, attribute, value, profiles) 172 | _simple_type -> validate_simple_array(acc, name, attribute, value, profiles) 173 | end 174 | end 175 | 176 | defp validate_simple_array(acc, name, attribute, value, profiles) do 177 | {map, _count} = 178 | Enum.reduce(value, {Map.new(), 0}, fn data, {map, count} -> 179 | {validate_data(map, Integer.to_string(count), attribute, data, profiles), count + 1} 180 | end) 181 | 182 | if map_size(map) > 0 do 183 | error = 184 | if attribute[:enum] == nil do 185 | "The array contains invalid data: expected #{attribute[:type]} type" 186 | else 187 | "The array contains invalid enum values" 188 | end 189 | 190 | values = Enum.into(map, %{}, fn {key, data} -> {key, data.value} end) 191 | 192 | Map.put(acc, name, %{ 193 | :error => error, 194 | :values => values 195 | }) 196 | else 197 | acc 198 | end 199 | end 200 | 201 | defp validate_object_array(acc, name, attribute, value, profiles) do 202 | case attribute[:object_type] do 203 | "object" -> 204 | acc 205 | 206 | object_type -> 207 | object = Schema.object(object_type) 208 | 209 | {map, _count} = 210 | Enum.reduce(value, {Map.new(), 0}, fn data, {map, count} -> 211 | map = validate_type(object, data, profiles) |> add_count(map, count) 212 | 213 | {map, count + 1} 214 | end) 215 | 216 | if map_size(map) > 0 do 217 | Map.put(acc, name, %{ 218 | :error => "The array contains invalid data", 219 | :values => map 220 | }) 221 | else 222 | acc 223 | end 224 | end 225 | end 226 | 227 | defp add_count(result, map, count) do 228 | if map_size(result) > 0 do 229 | Map.put(map, "#{count}", result) 230 | else 231 | map 232 | end 233 | end 234 | 235 | defp validate_object(acc, name, attribute, value, profiles) do 236 | case attribute[:object_type] do 237 | "object" -> 238 | acc 239 | 240 | object_type -> 241 | Schema.object(object_type) 242 | |> validate_type(value, profiles) 243 | |> valid?(acc, name) 244 | end 245 | end 246 | 247 | defp valid?(map, acc, name) do 248 | if map_size(map) > 0 do 249 | Map.put(acc, name, map) 250 | else 251 | acc 252 | end 253 | end 254 | 255 | # check the attribute value type 256 | defp validate_data_type(acc, name, attribute, value, value_type) do 257 | case attribute[:type] do 258 | "json_t" -> 259 | acc 260 | 261 | ^value_type -> 262 | acc 263 | 264 | type -> 265 | if Schema.data_type?(type, value_type) do 266 | acc 267 | else 268 | Map.put(acc, name, invalid_data_type(attribute, value, type)) 269 | end 270 | end 271 | end 272 | 273 | defp invalid_data_type(_attribute, value, type) do 274 | %{ 275 | :error => "Invalid data type: expected '#{type}' type", 276 | :value => value 277 | } 278 | end 279 | 280 | # Validate an integer enum value 281 | defp validate_enum(nil, _attribute, _value), do: :ok 282 | 283 | defp validate_enum(enum, _attribute, value) do 284 | validate_enum_value(enum, Integer.to_string(value)) 285 | end 286 | 287 | defp validate_enum_value(nil, _value), do: :ok 288 | 289 | defp validate_enum_value(enum, value) do 290 | if Map.has_key?(enum, String.to_atom(value)) do 291 | :ok 292 | else 293 | %{ 294 | :error => "Invalid enum value", 295 | :value => value 296 | } 297 | end 298 | end 299 | end 300 | -------------------------------------------------------------------------------- /lib/schema_web.ex: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # http://www.apache.org/licenses/LICENSE-2.0 5 | # Unless required by applicable law or agreed to in writing, software 6 | # distributed under the License is distributed on an "AS IS" BASIS, 7 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | # See the License for the specific language governing permissions and 9 | # limitations under the License. 10 | defmodule SchemaWeb do 11 | @moduledoc """ 12 | The entrypoint for defining your web interface, such 13 | as controllers, views, channels and so on. 14 | 15 | This can be used in your application as: 16 | 17 | use SchemaWeb, :controller 18 | use SchemaWeb, :view 19 | 20 | The definitions below will be executed for every view, 21 | controller, etc, so keep them short and clean, focused 22 | on imports, uses and aliases. 23 | 24 | Do NOT define functions inside the quoted expressions 25 | below. Instead, define any helper function in modules 26 | and import those modules here. 27 | """ 28 | 29 | def controller do 30 | quote do 31 | use Phoenix.Controller, namespace: SchemaWeb 32 | 33 | import Plug.Conn 34 | alias SchemaWeb.Router.Helpers, as: Routes 35 | end 36 | end 37 | 38 | def view do 39 | quote do 40 | use Phoenix.View, 41 | root: "lib/schema_web/templates", 42 | namespace: SchemaWeb 43 | 44 | # Import convenience functions from controllers 45 | import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1] 46 | 47 | # Use all HTML functionality (forms, tags, etc) 48 | import Phoenix.HTML 49 | 50 | alias SchemaWeb.Router.Helpers, as: Routes 51 | end 52 | end 53 | 54 | def router do 55 | quote do 56 | use Phoenix.Router 57 | import Plug.Conn 58 | import Phoenix.Controller 59 | end 60 | end 61 | 62 | def channel do 63 | quote do 64 | use Phoenix.Channel 65 | end 66 | end 67 | 68 | @doc """ 69 | When used, dispatch to the appropriate controller/view/etc. 70 | """ 71 | defmacro __using__(which) when is_atom(which) do 72 | apply(__MODULE__, which, []) 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/schema_web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule SchemaWeb.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "room:*", SchemaWeb.RoomChannel 6 | 7 | # Socket params are passed from the client and can 8 | # be used to verify and authenticate a user. After 9 | # verification, you can put default assigns into 10 | # the socket that will be set for all channels, ie 11 | # 12 | # {:ok, assign(socket, :user_id, verified_user_id)} 13 | # 14 | # To deny connection, return `:error`. 15 | # 16 | # See `Phoenix.Token` documentation for examples in 17 | # performing token verification on connect. 18 | def connect(_params, socket, _connect_info) do 19 | {:ok, socket} 20 | end 21 | 22 | # Socket id's are topics that allow you to identify all sockets for a given user: 23 | # 24 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}" 25 | # 26 | # Would allow you to broadcast a "disconnect" event and terminate 27 | # all active sockets and channels for a given user: 28 | # 29 | # SchemaWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) 30 | # 31 | # Returning `nil` makes this socket anonymous. 32 | def id(_socket), do: nil 33 | end 34 | -------------------------------------------------------------------------------- /lib/schema_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # http://www.apache.org/licenses/LICENSE-2.0 5 | # Unless required by applicable law or agreed to in writing, software 6 | # distributed under the License is distributed on an "AS IS" BASIS, 7 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | # See the License for the specific language governing permissions and 9 | # limitations under the License. 10 | defmodule SchemaWeb.PageController do 11 | @moduledoc """ 12 | The schema server web pages 13 | """ 14 | use SchemaWeb, :controller 15 | 16 | alias SchemaWeb.SchemaController 17 | 18 | @spec class_graph(Plug.Conn.t(), any) :: Plug.Conn.t() 19 | def class_graph(conn, %{"id" => id} = params) do 20 | case SchemaWeb.SchemaController.class_ex(id, params) do 21 | nil -> 22 | send_resp(conn, 404, "Not Found: #{id}") 23 | 24 | class -> 25 | data = Schema.Graph.build(class) 26 | 27 | render(conn, "class_graph.html", 28 | extensions: Schema.extensions(), 29 | profiles: SchemaController.get_profiles(params), 30 | data: data 31 | ) 32 | end 33 | end 34 | 35 | @spec object_graph(Plug.Conn.t(), any) :: Plug.Conn.t() 36 | def object_graph(conn, %{"id" => id} = params) do 37 | case SchemaWeb.SchemaController.object_ex(id, params) do 38 | nil -> 39 | send_resp(conn, 404, "Not Found: #{id}") 40 | 41 | obj -> 42 | data = Schema.Graph.build(obj) 43 | 44 | render(conn, "object_graph.html", 45 | extensions: Schema.extensions(), 46 | profiles: SchemaController.get_profiles(params), 47 | data: data 48 | ) 49 | end 50 | end 51 | 52 | @doc """ 53 | Renders the data types. 54 | """ 55 | @spec data_types(Plug.Conn.t(), any) :: Plug.Conn.t() 56 | def data_types(conn, params) do 57 | data = Schema.data_types() |> sort_attributes_by_key() 58 | 59 | render(conn, "data_types.html", 60 | extensions: Schema.extensions(), 61 | profiles: SchemaController.get_profiles(params), 62 | data: data 63 | ) 64 | end 65 | 66 | @doc """ 67 | Renders schema profiles. 68 | """ 69 | @spec profiles(Plug.Conn.t(), map) :: Plug.Conn.t() 70 | def profiles(conn, %{"id" => id} = params) do 71 | name = 72 | case params["extension"] do 73 | nil -> id 74 | extension -> "#{extension}/#{id}" 75 | end 76 | 77 | profiles = SchemaController.get_profiles(params) 78 | 79 | case Schema.profile(profiles, name) do 80 | nil -> 81 | send_resp(conn, 404, "Not Found: #{name}") 82 | 83 | profile -> 84 | render(conn, "profile.html", 85 | extensions: Schema.extensions(), 86 | profiles: profiles, 87 | data: sort_attributes_by_key(profile) 88 | ) 89 | end 90 | end 91 | 92 | def profiles(conn, params) do 93 | profiles = SchemaController.get_profiles(params) 94 | sorted_profiles = sort_by_descoped_key(profiles) 95 | 96 | render(conn, "profiles.html", 97 | extensions: Schema.extensions(), 98 | profiles: profiles, 99 | data: sorted_profiles 100 | ) 101 | end 102 | 103 | @doc """ 104 | Renders categories or the classes in a given category. 105 | """ 106 | @spec categories(Plug.Conn.t(), map) :: Plug.Conn.t() 107 | def categories(conn, %{"id" => id} = params) do 108 | case SchemaController.category_classes(params) do 109 | nil -> 110 | send_resp(conn, 404, "Not Found: #{id}") 111 | 112 | data -> 113 | classes = sort_by(data[:classes], :uid) 114 | 115 | render(conn, "category.html", 116 | extensions: Schema.extensions(), 117 | profiles: SchemaController.get_profiles(params), 118 | data: Map.put(data, :classes, classes) 119 | ) 120 | end 121 | end 122 | 123 | def categories(conn, params) do 124 | data = 125 | Map.put_new(params, "extensions", "") 126 | |> SchemaController.categories() 127 | |> sort_attributes(:uid) 128 | |> sort_classes() 129 | 130 | render(conn, "index.html", 131 | extensions: Schema.extensions(), 132 | profiles: SchemaController.get_profiles(params), 133 | data: data 134 | ) 135 | end 136 | 137 | @doc """ 138 | Renders the attribute dictionary. 139 | """ 140 | @spec dictionary(Plug.Conn.t(), any) :: Plug.Conn.t() 141 | def dictionary(conn, params) do 142 | data = SchemaController.dictionary(params) |> sort_attributes_by_key() 143 | 144 | render(conn, "dictionary.html", 145 | extensions: Schema.extensions(), 146 | profiles: SchemaController.get_profiles(params), 147 | data: data 148 | ) 149 | end 150 | 151 | @doc """ 152 | Redirects from the older /base_event URL to /classes/base_event. 153 | """ 154 | @spec base_event(Plug.Conn.t(), any) :: Plug.Conn.t() 155 | def base_event(conn, _params) do 156 | redirect(conn, to: "/classes/base_event") 157 | end 158 | 159 | @doc """ 160 | Renders event classes. 161 | """ 162 | @spec classes(Plug.Conn.t(), any) :: Plug.Conn.t() 163 | def classes(conn, %{"id" => id} = params) do 164 | extension = params["extension"] 165 | profiles = parse_profiles_from_params(params) 166 | 167 | case Schema.class(extension, id, profiles) do 168 | nil -> 169 | send_resp(conn, 404, "Not Found: #{id}") 170 | 171 | data -> 172 | data = 173 | data 174 | |> sort_attributes_by_key() 175 | |> Map.put(:key, Schema.Utils.to_uid(extension, id)) 176 | 177 | render(conn, "class.html", 178 | extensions: Schema.extensions(), 179 | profiles: SchemaController.get_profiles(params), 180 | data: data 181 | ) 182 | end 183 | end 184 | 185 | def classes(conn, params) do 186 | data = SchemaController.classes(params) |> sort_by(:uid) 187 | 188 | render(conn, "classes.html", 189 | extensions: Schema.extensions(), 190 | profiles: SchemaController.get_profiles(params), 191 | data: data 192 | ) 193 | end 194 | 195 | @doc """ 196 | Renders objects. 197 | """ 198 | @spec objects(Plug.Conn.t(), map) :: Plug.Conn.t() 199 | def objects(conn, %{"id" => id} = params) do 200 | case SchemaController.object(params) do 201 | nil -> 202 | send_resp(conn, 404, "Not Found: #{id}") 203 | 204 | data -> 205 | data = 206 | data 207 | |> sort_attributes_by_key() 208 | |> Map.put(:key, Schema.Utils.to_uid(params["extension"], id)) 209 | 210 | render(conn, "object.html", 211 | extensions: Schema.extensions(), 212 | profiles: SchemaController.get_profiles(params), 213 | data: data 214 | ) 215 | end 216 | end 217 | 218 | def objects(conn, params) do 219 | data = SchemaController.objects(params) |> sort_by_descoped_key() 220 | 221 | render(conn, "objects.html", 222 | extensions: Schema.extensions(), 223 | profiles: SchemaController.get_profiles(params), 224 | data: data 225 | ) 226 | end 227 | 228 | defp sort_classes(categories) do 229 | Map.update!(categories, :attributes, fn list -> 230 | Enum.map(list, fn {name, category} -> 231 | {name, Map.update!(category, :classes, &sort_by(&1, :uid))} 232 | end) 233 | end) 234 | end 235 | 236 | defp sort_attributes(map, key) do 237 | Map.update!(map, :attributes, &sort_by(&1, key)) 238 | end 239 | 240 | defp sort_by(map, key) do 241 | Enum.sort(map, fn {_, v1}, {_, v2} -> v1[key] <= v2[key] end) 242 | end 243 | 244 | defp sort_attributes_by_key(map) do 245 | Map.update!(map, :attributes, &sort_by_descoped_key/1) 246 | end 247 | 248 | defp sort_by_descoped_key(map) do 249 | Enum.sort(map, fn {k1, _}, {k2, _} -> 250 | Schema.Utils.descope(k1) <= Schema.Utils.descope(k2) 251 | end) 252 | end 253 | 254 | defp parse_profiles_from_params(params) do 255 | case params["profiles"] do 256 | nil -> nil 257 | "" -> MapSet.new() 258 | profiles_string -> 259 | profiles_string 260 | |> String.split(",") 261 | |> Enum.map(&String.trim/1) 262 | |> MapSet.new() 263 | end 264 | end 265 | end 266 | -------------------------------------------------------------------------------- /lib/schema_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # http://www.apache.org/licenses/LICENSE-2.0 5 | # Unless required by applicable law or agreed to in writing, software 6 | # distributed under the License is distributed on an "AS IS" BASIS, 7 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | # See the License for the specific language governing permissions and 9 | # limitations under the License. 10 | defmodule SchemaWeb.Endpoint do 11 | use Phoenix.Endpoint, otp_app: :schema_server 12 | 13 | socket "/socket", SchemaWeb.UserSocket, 14 | websocket: false, 15 | longpoll: false 16 | 17 | # Serve at "/" the static files from "priv/static" directory. 18 | # 19 | # You should set gzip to true if you are running phx.digest 20 | # when deploying your static files in production. 21 | plug Plug.Static, 22 | at: "/", 23 | from: :schema_server, 24 | gzip: false, 25 | only: ~w(css webfonts images js apidoc favicon.ico robots.txt) 26 | 27 | # Code reloading can be explicitly enabled under the 28 | # :code_reloader configuration of your endpoint. 29 | if code_reloading? do 30 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 31 | plug Phoenix.LiveReloader 32 | plug Phoenix.CodeReloader 33 | end 34 | 35 | plug Plug.RequestId 36 | plug Plug.Logger 37 | 38 | plug Plug.Parsers, 39 | parsers: [:urlencoded, :multipart, :json], 40 | pass: ["*/*"], 41 | json_decoder: Phoenix.json_library(), 42 | nest_all_json: true 43 | 44 | plug Plug.MethodOverride 45 | plug Plug.Head 46 | 47 | # The session will be stored in the cookie and signed, 48 | # this means its contents can be read but not tampered with. 49 | # Set :encryption_salt if you would also like to encrypt it. 50 | plug Plug.Session, 51 | store: :cookie, 52 | key: "_schema_key", 53 | signing_salt: "5V6q/2La" 54 | 55 | plug SchemaWeb.Router 56 | end 57 | -------------------------------------------------------------------------------- /lib/schema_web/router.ex: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # http://www.apache.org/licenses/LICENSE-2.0 5 | # Unless required by applicable law or agreed to in writing, software 6 | # distributed under the License is distributed on an "AS IS" BASIS, 7 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | # See the License for the specific language governing permissions and 9 | # limitations under the License. 10 | defmodule SchemaWeb.Router do 11 | use SchemaWeb, :router 12 | 13 | pipeline :browser do 14 | plug :accepts, ["html"] 15 | plug :fetch_session 16 | plug :fetch_flash 17 | plug :protect_from_forgery 18 | plug :put_secure_browser_headers 19 | end 20 | 21 | pipeline :api do 22 | plug :accepts, ["json"] 23 | end 24 | 25 | scope "/", SchemaWeb do 26 | pipe_through :browser 27 | 28 | get "/", PageController, :categories 29 | get "/categories", PageController, :categories 30 | 31 | get "/categories/:id", PageController, :categories 32 | get "/categories/:extension/:id", PageController, :categories 33 | 34 | get "/profiles", PageController, :profiles 35 | get "/profiles/:id", PageController, :profiles 36 | get "/profiles/:extension/:id", PageController, :profiles 37 | 38 | get "/classes", PageController, :classes 39 | get "/classes/:id", PageController, :classes 40 | get "/classes/:extension/:id", PageController, :classes 41 | 42 | get "/class/graph/:id", PageController, :class_graph 43 | get "/class/graph/:extension/:id", PageController, :class_graph 44 | 45 | get "/base_event", PageController, :base_event 46 | get "/dictionary", PageController, :dictionary 47 | 48 | get "/objects", PageController, :objects 49 | get "/objects/:id", PageController, :objects 50 | get "/objects/:extension/:id", PageController, :objects 51 | 52 | get "/object/graph/:id", PageController, :object_graph 53 | get "/object/graph/:extension/:id", PageController, :object_graph 54 | 55 | get "/data_types", PageController, :data_types 56 | end 57 | 58 | # Other scopes may use custom stacks. 59 | scope "/api", SchemaWeb do 60 | pipe_through :api 61 | 62 | get "/version", SchemaController, :version 63 | get "/versions", SchemaController, :versions 64 | 65 | get "/profiles", SchemaController, :profiles 66 | get "/extensions", SchemaController, :extensions 67 | 68 | get "/categories", SchemaController, :categories 69 | get "/categories/:id", SchemaController, :category 70 | get "/categories/:extension/:id", SchemaController, :category 71 | 72 | get "/profiles/:id", SchemaController, :profile 73 | get "/profiles/:extension/:id", SchemaController, :profile 74 | 75 | get "/classes", SchemaController, :classes 76 | get "/classes/:id", SchemaController, :class 77 | get "/classes/:extension/:id", SchemaController, :class 78 | 79 | get "/base_event", SchemaController, :base_event 80 | get "/dictionary", SchemaController, :dictionary 81 | 82 | get "/objects", SchemaController, :objects 83 | get "/objects/:id", SchemaController, :object 84 | get "/objects/:extension/:id", SchemaController, :object 85 | 86 | get "/data_types", SchemaController, :data_types 87 | 88 | post "/enrich", SchemaController, :enrich 89 | post "/translate", SchemaController, :translate 90 | post "/validate", SchemaController, :validate 91 | post "/v2/validate", SchemaController, :validate2 92 | post "/v2/validate_bundle", SchemaController, :validate2_bundle 93 | end 94 | 95 | scope "/schema", SchemaWeb do 96 | pipe_through :api 97 | 98 | get "/classes/:id", SchemaController, :json_class 99 | get "/classes/:extension/:id", SchemaController, :json_class 100 | 101 | get "/objects/:id", SchemaController, :json_object 102 | get "/objects/:extension/:id", SchemaController, :json_object 103 | end 104 | 105 | scope "/export", SchemaWeb do 106 | pipe_through :api 107 | 108 | get "/base_event", SchemaController, :export_base_event 109 | get "/classes", SchemaController, :export_classes 110 | get "/objects", SchemaController, :export_objects 111 | get "/schema", SchemaController, :export_schema 112 | end 113 | 114 | scope "/sample", SchemaWeb do 115 | pipe_through :api 116 | 117 | get "/base_event", SchemaController, :sample_event 118 | 119 | get "/objects/:id", SchemaController, :sample_object 120 | get "/objects/:extension/:id", SchemaController, :sample_object 121 | 122 | get "/classes/:id", SchemaController, :sample_class 123 | get "/classes/:extension/:id", SchemaController, :sample_class 124 | end 125 | 126 | scope "/doc" do 127 | forward "/", PhoenixSwagger.Plug.SwaggerUI, 128 | otp_app: :schema_server, 129 | swagger_file: "swagger.json" 130 | end 131 | 132 | def swagger_info do 133 | %{ 134 | info: %{ 135 | title: "The OCSF Schema API", 136 | description: 137 | "The Open Cybersecurity Schema Framework (OCSF) server API allows to access the JSON" <> 138 | " schema definitions and to validate and translate events.", 139 | license: %{ 140 | name: "Apache 2.0", 141 | url: "http://www.apache.org/licenses/LICENSE-2.0.html" 142 | }, 143 | version: "1.0.0" 144 | } 145 | } 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /lib/schema_web/templates/page/category.html.eex: -------------------------------------------------------------------------------- 1 | <%!-- 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software 7 | distributed under the License is distributed on an "AS IS" BASIS, 8 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | See the License for the specific language governing permissions and 10 | limitations under the License. 11 | --%> 12 | 13 |
14 |
15 |

<%= @data[:caption] %> 16 | 17 | [<%= @data[:uid] %>]<%= if @data[:extension] && @data[:extension] != "" do %><%= @data[:extension] %><% end %> 18 | Category 19 | 20 |

21 |
<%= raw description(@data) %>
22 |
23 |
24 |
25 | 30 |
31 |
32 |
33 | 34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | <%= for {id, class} <- @data[:classes] do %> 46 | <% name = Atom.to_string(id) %> 47 | <% path = Routes.static_path(@conn, "/classes/" <> name) %> 48 | > 49 | 50 | 53 | <% uid = class[:uid] %> 54 | <%= if uid != nil do %> 55 | 56 | <% else %> 57 | 58 | <% end %> 59 | 60 | 61 | <% end %> 62 | 63 |
CaptionNameIDDescription
<%= raw format_caption(name, class) %> 51 | <%= name %> 52 | <%= uid %><%= raw description(class) %>
64 |
65 | 66 | 69 | -------------------------------------------------------------------------------- /lib/schema_web/templates/page/class.html.eex: -------------------------------------------------------------------------------- 1 | <%!-- 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software 7 | distributed under the License is distributed on an "AS IS" BASIS, 8 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | See the License for the specific language governing permissions and 10 | limitations under the License. 11 | --%> 12 | 13 | <% category = @data[:category] %> 14 | <% category_name = @data[:category_name] %> 15 | <% extension = @data[:extension] %> 16 | <% observables = @data[:observables] %> 17 | <% references = @data[:references] %> 18 | <% constraints = @data[:constraints] %> 19 | 20 |
21 |
22 | <% path = Routes.static_path(@conn, "/categories/" <> category) %> 23 |

24 | <%= @data[:caption] %> 25 | [<%= @data[:uid] %>]<%= if extension && extension != "" do %><%= extension %><% end %> 26 | Class 27 | 28 | <%= raw profile_badges(@conn, @data, @profiles) %> 29 |

30 | 31 | <%= if category_name != nil do %> 32 |

33 | <%= category_name %> 34 | Category 35 |

36 | <% end %> 37 | 38 |
39 | <%= raw description(@data) %> 40 |
41 | <%= if observables != nil and !Enum.empty?(observables) do %> 42 |
43 | Class-specific attribute path observables are at the bottom of this page. 44 |
45 | <% end %> 46 | <%= raw object_references_section(references) %> 47 |
48 | 51 |
52 | 53 |
54 |
55 |
56 | 57 | 58 | 59 |
60 |
61 |
62 | 75 |
76 |
77 | 78 |
79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | <%= for {attribute_key, attribute} <- @data[:attributes] do %> 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | <% end %> 101 | 102 |
NameCaptionGroupRequirementTypeDescription
<%= format_attribute_name(attribute_key) %><%= raw format_attribute_caption(@conn, attribute_key, attribute) %><%= attribute[:group] %><%= raw format_requirement(constraints, attribute_key, attribute) %><%= raw format_type(@conn, attribute) %><%= raw format_attribute_desc(attribute_key, attribute) %>
103 |
104 | 105 | <%= if constraints != nil and map_size(constraints) > 0 do %> 106 |
Constraints
107 |
108 | <%= raw constraints(constraints) %> 109 |
110 | <% end %> 111 | 112 | <% associations = @data[:associations] %> 113 | <%= if associations != nil and !Enum.empty?(associations) do %> 114 |
Attribute Associations
115 |
116 | <%= raw associations(associations) %> 117 |
118 | <% end %> 119 | 120 | <%= if observables != nil and !Enum.empty?(observables) do %> 121 |
Class-Specific Attribute Path Observables
122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | <%= for {attribute_path, observable_type_id} <- observables do %> 131 | 132 | 133 | 134 | 135 | <% end %> 136 | 137 |
Attribute PathObservable Type ID
<%= attribute_path %>#type_id-<%= observable_type_id %>"><%= observable_type_id %>
138 | <% end %> 139 | 140 | 161 | -------------------------------------------------------------------------------- /lib/schema_web/templates/page/class_graph.html.eex: -------------------------------------------------------------------------------- 1 | <%!-- 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software 7 | distributed under the License is distributed on an "AS IS" BASIS, 8 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | See the License for the specific language governing permissions and 10 | limitations under the License. 11 | --%> 12 | <% class = @data[:class] %> 13 | <% category = class[:category] %> 14 | <% extension = class[:extension] %> 15 | 16 |
17 |
18 | <% path = Routes.static_path(@conn, "/categories/" <> category) %> 19 |

20 | <%= class[:caption] %> 21 | [<%= class[:uid] %>]<%= if extension && extension != "" do %><%= extension %><% end %> 22 | Class 23 | 24 |

25 | 26 |

27 | <%= class[:category_name] %> Category 28 |

29 | 30 |
31 | <%= raw class[:description] %> 32 |
33 |
34 |
35 | 36 |
37 | 38 | 63 | -------------------------------------------------------------------------------- /lib/schema_web/templates/page/classes.html.eex: -------------------------------------------------------------------------------- 1 | <%!-- 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software 7 | distributed under the License is distributed on an "AS IS" BASIS, 8 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | See the License for the specific language governing permissions and 10 | limitations under the License. 11 | --%> 12 |
13 |
14 |

Classes

15 |
16 | The list of all the OCSF event classes, currently available. 17 |
18 |
19 | 22 |
23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | <%= for {class_key, class} <- @data do %> 36 | <% class_key_str = Atom.to_string(class_key) %> 37 | <% class_path = Routes.static_path(@conn, "/classes/" <> class_key_str) %> 38 | " <%= raw format_profiles(class[:profiles])%>> 39 | 40 | 41 | <% uid = class[:uid] %> 42 | <%= if uid != nil do %> 43 | 44 | <% else %> 45 | 46 | <% end %> 47 | 48 | 49 | <% end %> 50 | 51 |
NameCaptionIDDescription
<%= class_key_str %><%= raw format_caption(class_key_str, class) %><%= uid %><%= raw description(class) %>
52 |
53 | 54 | 58 | -------------------------------------------------------------------------------- /lib/schema_web/templates/page/data_types.html.eex: -------------------------------------------------------------------------------- 1 | <%!-- 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software 7 | distributed under the License is distributed on an "AS IS" BASIS, 8 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | See the License for the specific language governing permissions and 10 | limitations under the License. 11 | --%> 12 |
13 |
14 |

15 | <%= @data[:caption] %> 16 |

17 |
18 | <%= raw @data[:description] %> 19 |
20 |
21 |
22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | <%= for {key, field} <- @data[:attributes] do %> 36 | <% name = Atom.to_string(key) %> 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | <% end %> 45 | 46 |
NameCaptionBase TypeConstraintsDescription
<%= name %><%= raw format_attribute_caption(@conn, key, field) %><%= raw format_type(@conn, field) %><%= raw format_constraints(key, field) %><%= raw field[:description] %>
47 |
48 | 51 | -------------------------------------------------------------------------------- /lib/schema_web/templates/page/dictionary.html.eex: -------------------------------------------------------------------------------- 1 | <%!-- 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software 7 | distributed under the License is distributed on an "AS IS" BASIS, 8 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | See the License for the specific language governing permissions and 10 | limitations under the License. 11 | --%> 12 | 13 |
14 |
15 |

<%= @data[:caption] %>

16 |
17 | <%= raw @data[:description] %> 18 |
19 |
20 | Note: a superscript "O" after a caption indicates attribute is an observable. 21 | (Class-specific and object-specific attribute observables are not shown on this page.) 22 |
23 |
24 |
25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 | Expand All and Collapse All are slow — be patient 33 |
34 |
35 |
36 |
37 | 38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | <%= for {attribute_key, attribute} <- @data[:attributes] do %> 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | <% end %> 59 | 60 |
NameCaptionTypeReferenced ByDescription
<%= format_attribute_name(attribute_key) %><%= raw format_attribute_caption(@conn, attribute_key, attribute) %><%= raw format_type(@conn, attribute) %><%= raw dictionary_links(@conn, attribute_key, attribute[:_links]) %><%= raw format_dictionary_attribute_desc(attribute_key, attribute) %>
61 |
62 | 63 | 67 | -------------------------------------------------------------------------------- /lib/schema_web/templates/page/index.html.eex: -------------------------------------------------------------------------------- 1 | <%!-- 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software 7 | distributed under the License is distributed on an "AS IS" BASIS, 8 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | See the License for the specific language governing permissions and 10 | limitations under the License. 11 | --%> 12 | 13 |
14 |
15 |

<%= @data[:caption] %>

16 |
17 | <%= raw @data[:description] %> 18 |
19 |
20 | 23 |
24 | 25 |
26 | <%= for {category_key, category} <- @data[:attributes] do %> 27 | <% category_key_str = Atom.to_string(category_key) %> 28 | <% category_path = Routes.static_path(@conn, "/categories/" <> category_key_str) %> 29 |
30 |
<%= raw format_caption(category_key_str, category) %>
31 | <%= for {class_key, class} <- category[:classes] do %> 32 | <% class_key_str = Atom.to_string(class_key) %> 33 | <% class_path = Routes.static_path(@conn, "/classes/" <> class_key_str) %> 34 |
" <%= raw format_profiles(class[:profiles])%>><%= raw format_linked_class_caption(class_path, class_key_str, class) %>
35 | <% end %> 36 |
37 | <% end %> 38 |
39 | 40 | 44 | -------------------------------------------------------------------------------- /lib/schema_web/templates/page/object.html.eex: -------------------------------------------------------------------------------- 1 | <%!-- 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software 7 | distributed under the License is distributed on an "AS IS" BASIS, 8 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | See the License for the specific language governing permissions and 10 | limitations under the License. 11 | --%> 12 | 13 | <% references = @data[:references] %> 14 | <% constraints = @data[:constraints] %> 15 | 16 |
17 |
18 |

19 | <%= @data[:caption] %> 20 | <% observable = @data[:observable] %> 21 | <%= if is_nil(observable) do %> 22 | <%= if @data[:extension] && @data[:extension] != "" do %><%= @data[:extension] %><% end %> 23 | Object 24 | 25 | <% else %> 26 | <%= if @data[:extension] && @data[:extension] != "" do %><%= @data[:extension] %><% end %> 27 | [<%= observable %>] object 28 | 29 | <% end %> 30 | <% extends = @data[:extends] || "object" %> 31 | 32 | <%= if extends != "object" and not String.starts_with?(extends, "_") do %> 33 | 34 | extends extends) %>"><%= extends %> 35 | 36 | <% end %> 37 | 38 | <%= raw profile_badges(@conn, @data, @profiles) %> 39 |

40 | 41 |
42 | <%= raw description(@data) %> 43 |
44 | <%= raw object_references_section(references) %> 45 |
46 | 49 |
50 | 51 |
52 |
53 |
54 | 55 | 56 |
57 |
58 |
59 | 65 |
66 |
67 | 68 |
69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | <%= for {attribute_key, attribute} <- @data[:attributes] do %> 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | <% end %> 89 | 90 |
NameCaptionRequirementTypeDescription
<%= format_attribute_name(attribute_key) %><%= raw format_attribute_caption(@conn, attribute_key, attribute) %><%= raw format_requirement(constraints, attribute_key, attribute) %><%= raw format_type(@conn, attribute) %><%= raw format_attribute_desc(attribute_key, attribute) %>
91 |
92 | 93 | <% links = @data[:_links] %> 94 | <%= if Enum.empty?(links) do %> 95 |
96 | <% else %> 97 |
98 | 99 | 102 |
103 | <% end %> 104 | <%= if constraints != nil and map_size(constraints) > 0 do %> 105 |
Constraints
106 |
107 | <%= raw constraints(constraints) %> 108 |
109 | <% end %> 110 | 111 | 132 | -------------------------------------------------------------------------------- /lib/schema_web/templates/page/object_graph.html.eex: -------------------------------------------------------------------------------- 1 | <%!-- 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software 7 | distributed under the License is distributed on an "AS IS" BASIS, 8 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | See the License for the specific language governing permissions and 10 | limitations under the License. 11 | --%> 12 | <% class = @data[:class] %> 13 | <% name = class[:caption] %> 14 | <% extension = class[:extension] %> 15 | 16 |
17 |
18 |

19 | <%= name %> 20 | <%= if extension && extension != "" do %><%= extension %><% end %> 21 | <% observable = class[:observable] %> 22 | <%= unless is_nil(observable) do %> 23 | [<%= observable %>] 24 | <% end %> 25 | object 26 |

27 | 28 |
29 | <%= raw class[:description] %> 30 |
31 |
32 |
33 | 34 |
35 | 36 | 61 | -------------------------------------------------------------------------------- /lib/schema_web/templates/page/objects.html.eex: -------------------------------------------------------------------------------- 1 | <%!-- 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software 7 | distributed under the License is distributed on an "AS IS" BASIS, 8 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | See the License for the specific language governing permissions and 10 | limitations under the License. 11 | --%> 12 |
13 |
14 |

Objects

15 |
16 | The OCSF objects. An object is a complex data type, which is a collection of other attributes. Some objects represent entities or artifacts, but not all. 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 | <%= for {object_key, object} <- @data do %> 42 | <% object_key_str = Atom.to_string(object_key) %> 43 | <% object_path = Routes.static_path(@conn, "/objects/" <> object_key_str) %> 44 | " <%= raw format_profiles(object[:profiles])%>> 45 | 46 | 47 | 48 | 49 | 50 | <% end %> 51 | 52 |
NameCaptionReferenced ByDescription
<%= object_key_str %><%= raw format_attribute_caption(@conn, object_key_str, object) %><%= raw object_links(@conn, object[:name], object[:_links], :collapse) %><%= raw description(object) %>
53 |
54 | 55 | 59 | -------------------------------------------------------------------------------- /lib/schema_web/templates/page/profile.html.eex: -------------------------------------------------------------------------------- 1 | <%!-- 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software 7 | distributed under the License is distributed on an "AS IS" BASIS, 8 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | See the License for the specific language governing permissions and 10 | limitations under the License. 11 | --%> 12 |
13 |
14 |

15 | <%= @data[:caption] %> 16 | [<%= @data[:name] %>] 17 | <%= if @data[:extension] && @data[:extension] != "" do %><% end %> 18 | Profile 19 | 20 |

21 | 22 |
23 | <%= raw @data[:description] %> 24 |
25 |
26 | 29 |
30 | 31 |
32 |
33 |
34 | 46 |
47 |
48 | 49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | <%= for {key, field} <- @data[:attributes] do %> 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | <% end %> 72 | 73 |
NameCaptionGroupRequirementTypeDescription
<%= key %><%= raw format_attribute_caption(@conn, key, field) %><%= field[:group] %><%= raw format_requirement(nil, key, field) %><%= raw format_type(@conn, field) %><%= raw format_attribute_desc(key, field) %>
74 |
75 | <% links = @data[:_links] %> 76 | <%= if Enum.empty?(links) do %> 77 |
78 | <% else %> 79 |
80 | 81 | 84 |
85 | <% end %> 86 | 87 | 92 | -------------------------------------------------------------------------------- /lib/schema_web/templates/page/profiles.html.eex: -------------------------------------------------------------------------------- 1 | <%!-- 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software 7 | distributed under the License is distributed on an "AS IS" BASIS, 8 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | See the License for the specific language governing permissions and 10 | limitations under the License. 11 | --%> 12 |
13 |
14 |

Profiles

15 |
16 | The OCSF Profiles. A profile is an optional overlay on event classes and objects that reference it. 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 | <%= for {name, map} <- @data do %> 42 | <% path = Routes.static_path(@conn, "/profiles/" <> name) %> 43 | 44 | 45 | 46 | 47 | 48 | 49 | <% end %> 50 | 51 |
NameCaptionReferenced ByDescription
<%= name %><%= raw format_caption(name, map) %><%= raw profile_links(@conn, map[:name], map[:_links], :collapse) %><%= raw map[:description] %>
52 |
53 | 54 | 58 | -------------------------------------------------------------------------------- /lib/schema_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule SchemaWeb.ErrorView do 2 | use SchemaWeb, :view 3 | 4 | # If you want to customize a particular status code 5 | # for a certain format, you may uncomment below. 6 | # def render("500.html", _assigns) do 7 | # "Internal Server Error" 8 | # end 9 | 10 | # By default, Phoenix returns the status message from 11 | # the template name. For example, "404.html" becomes 12 | # "Not Found". 13 | def template_not_found(template, _assigns) do 14 | Phoenix.Controller.status_message_from_template(template) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/schema_web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule SchemaWeb.LayoutView do 2 | use SchemaWeb, :view 3 | 4 | def format_profile(profile) do 5 | Enum.reduce(profile[:attributes], [], fn {name, _}, acc -> 6 | [Atom.to_string(name) | acc] 7 | end) 8 | |> Enum.join("\n") 9 | end 10 | 11 | def format_extension(extension) do 12 | caption = "#{extension[:caption]}" 13 | uid = " [#{extension[:uid]}]" 14 | 15 | case extension[:version] do 16 | nil -> 17 | [caption, uid] 18 | 19 | ext_ver -> 20 | [caption, uid, "
", "v", ext_ver] 21 | end 22 | end 23 | 24 | def select_versions(_conn) do 25 | current = Schema.version() 26 | 27 | case Schemas.versions() do 28 | [] -> 29 | [ 30 | "" 35 | ] 36 | 37 | versions -> 38 | Enum.map(versions, fn {version, _path} -> 39 | [ 40 | "" 49 | ] 50 | end) 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/schemas.ex: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # http://www.apache.org/licenses/LICENSE-2.0 5 | # Unless required by applicable law or agreed to in writing, software 6 | # distributed under the License is distributed on an "AS IS" BASIS, 7 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | # See the License for the specific language governing permissions and 9 | # limitations under the License. 10 | defmodule Schemas do 11 | @moduledoc """ 12 | This module provides function to work with multiple schema versions. 13 | """ 14 | 15 | use Agent 16 | 17 | require Logger 18 | 19 | # The Schema version file 20 | @version_file "version.json" 21 | 22 | def start_link(nil) do 23 | Agent.start_link(fn -> [] end, name: __MODULE__) 24 | end 25 | 26 | def start_link(path) do 27 | Agent.start_link(fn -> ls(path) end, name: __MODULE__) 28 | end 29 | 30 | @doc """ 31 | Returns a list of available schemas. 32 | 33 | Returns {:ok, list(map())}. 34 | """ 35 | def versions do 36 | Agent.get(__MODULE__, & &1) 37 | end 38 | 39 | @doc """ 40 | Returns a list of schemas is the given directory. 41 | 42 | Returns list of {version, path} tuples in case of success, {:error, reason} otherwise. 43 | """ 44 | @spec ls(Path.t()) :: list({String.t(), String.t()}) | {:error, File.posix()} 45 | def ls(path) do 46 | with {:ok, list} <- File.ls(path) do 47 | Stream.map(list, fn name -> 48 | Path.join(path, name) 49 | end) 50 | |> Stream.map(fn dir -> 51 | with {:ok, data} <- File.read(Path.join(dir, @version_file)), 52 | {:ok, json} <- Jason.decode(data), 53 | {:ok, version} <- version(json) do 54 | {:ok, version, dir} 55 | else 56 | {:error, reason} when is_atom(reason) -> 57 | err_msg = :file.format_error(reason) 58 | Logger.error("No schema version file found in #{dir}. Error: #{err_msg}") 59 | {:error, err_msg, dir} 60 | 61 | {:error, reason} -> 62 | Logger.error("Invalid schema version file in #{dir}. Error: #{inspect(reason)}") 63 | {:error, reason, dir} 64 | end 65 | end) 66 | |> Stream.filter(fn 67 | {:ok, _, _} -> true 68 | {_er, _, _} -> false 69 | end) 70 | |> Stream.map(fn {_, version, path} -> 71 | {version, path, Schema.Utils.parse_version(version)} 72 | end) 73 | |> Enum.sort(fn {_, _, v1}, {_, _, v2} -> Schema.Utils.version_sorter_desc(v1, v2) end) 74 | |> Enum.map(fn {version, path, _parsed_version} -> {version, path} end) 75 | else 76 | {:error, reason} -> 77 | err_msg = :file.format_error(reason) 78 | Logger.error("Invalid schema directory #{inspect(path)}: #{err_msg}") 79 | {:error, err_msg} 80 | end 81 | end 82 | 83 | defp version(data) do 84 | case data["version"] do 85 | nil -> 86 | {:error, "Missing 'version' attribute"} 87 | 88 | version -> 89 | {:ok, version} 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # http://www.apache.org/licenses/LICENSE-2.0 5 | # Unless required by applicable law or agreed to in writing, software 6 | # distributed under the License is distributed on an "AS IS" BASIS, 7 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | # See the License for the specific language governing permissions and 9 | # limitations under the License. 10 | defmodule Schema.MixProject do 11 | use Mix.Project 12 | 13 | @version "3.1.1" 14 | 15 | def project do 16 | build = System.get_env("GITHUB_RUN_NUMBER") || "SNAPSHOT" 17 | 18 | [ 19 | releases: [ 20 | schema_server: [ 21 | steps: [:assemble, :tar, &write_version/1], 22 | include_executables_for: [:unix] 23 | ] 24 | ], 25 | app: :schema_server, 26 | version: "#{@version}-#{build}", 27 | elixir: "~> 1.14", 28 | elixirc_paths: elixirc_paths(Mix.env()), 29 | compilers: Mix.compilers() ++ [:phoenix_swagger], 30 | start_permanent: Mix.env() == :prod, 31 | deps: deps() 32 | ] 33 | end 34 | 35 | # Configuration for the OTP application. 36 | # 37 | # Type `mix help compile.app` for more information. 38 | def application do 39 | [ 40 | mod: {Schema.Application, []}, 41 | extra_applications: [:logger, :crypto, :runtime_tools] 42 | ] 43 | end 44 | 45 | # Specifies which paths to compile per environment. 46 | defp elixirc_paths(:test), do: ["lib", "test/support"] 47 | defp elixirc_paths(_), do: ["lib"] 48 | 49 | # Specifies your project dependencies. 50 | # 51 | # Type `mix help deps` for examples and options. 52 | defp deps do 53 | [ 54 | {:phoenix, "~> 1.7.0"}, 55 | {:phoenix_view, "~> 2.0"}, 56 | {:phoenix_html, "~> 4.0"}, 57 | {:phoenix_live_reload, "~> 1.5", only: :dev}, 58 | {:jason, "~> 1.4"}, 59 | {:plug_cowboy, "~> 2.7"}, 60 | {:number, "~> 1.0"}, 61 | {:elixir_uuid, "~> 1.6", hex: :uuid_utils}, 62 | {:phoenix_swagger, "~> 0.8"}, 63 | {:logger_file_backend, "~> 0.0.13"}, 64 | {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, 65 | {:sobelow, "~> 0.13", only: [:dev, :test], runtime: false} 66 | ] 67 | end 68 | 69 | # Write the version number 70 | defp write_version(rel) do 71 | case System.argv() do 72 | ["release" | _] -> 73 | # Write the version number 74 | version = Mix.Project.config()[:version] 75 | File.write(".version", version) 76 | 77 | _ -> 78 | :ok 79 | end 80 | 81 | rel 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 | "castore": {:hex, :castore, "1.0.14", "4582dd7d630b48cf5e1ca8d3d42494db51e406b7ba704e81fbd401866366896a", [:mix], [], "hexpm", "7bc1b65249d31701393edaaac18ec8398d8974d52c647b7904d01b964137b9f4"}, 4 | "cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"}, 5 | "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, 6 | "cowlib": {:hex, :cowlib, "2.15.0", "3c97a318a933962d1c12b96ab7c1d728267d2c523c25a5b57b0f93392b6e9e25", [:make, :rebar3], [], "hexpm", "4f00c879a64b4fe7c8fcb42a4281925e9ffdb928820b03c3ad325a617e857532"}, 7 | "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, 8 | "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, 9 | "elixir_uuid": {:hex, :uuid_utils, "1.6.5", "bafd6ffcbec895513a7c10855df3954f29909fb5d05ee52681e30e84297b1a80", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "36aaeee10740eae4d357231f48571a2687cb541730f94f47cbd3f186dc07899c"}, 10 | "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, 11 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 12 | "logger_file_backend": {:hex, :logger_file_backend, "0.0.14", "774bb661f1c3fed51b624d2859180c01e386eb1273dc22de4f4a155ef749a602", [:mix], [], "hexpm", "071354a18196468f3904ef09413af20971d55164267427f6257b52cfba03f9e6"}, 13 | "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, 14 | "number": {:hex, :number, "1.0.5", "d92136f9b9382aeb50145782f116112078b3465b7be58df1f85952b8bb399b0f", [:mix], [{:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "c0733a0a90773a66582b9e92a3f01290987f395c972cb7d685f51dd927cd5169"}, 15 | "phoenix": {:hex, :phoenix, "1.7.21", "14ca4f1071a5f65121217d6b57ac5712d1857e40a0833aff7a691b7870fc9a3b", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "336dce4f86cba56fed312a7d280bf2282c720abb6074bdb1b61ec8095bdd0bc9"}, 16 | "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, 17 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.0", "2791fac0e2776b640192308cc90c0dbcf67843ad51387ed4ecae2038263d708d", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b3a1fa036d7eb2f956774eda7a7638cf5123f8f2175aca6d6420a7f95e598e1c"}, 18 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, 19 | "phoenix_swagger": {:hex, :phoenix_swagger, "0.8.4", "13b86b9e2f9c28ff16bbe806dd7511c583668bd4354d9c579ffde615a5f13f57", [:mix], [{:ex_json_schema, "~> 0.9.1", [hex: :ex_json_schema, repo: "hexpm", optional: true]}, {:jason, "~> 1.4.4", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.14.2", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 6.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "be431713d2988015ab1b4931ec9be7b3c04a27c5dcfce19a0b8e832976fbbf0b"}, 20 | "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, 21 | "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, 22 | "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"}, 23 | "plug_cowboy": {:hex, :plug_cowboy, "2.7.4", "729c752d17cf364e2b8da5bdb34fb5804f56251e88bb602aff48ae0bd8673d11", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9b85632bd7012615bae0a5d70084deb1b25d2bcbb32cab82d1e9a1e023168aa3"}, 24 | "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, 25 | "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, 26 | "sobelow": {:hex, :sobelow, "0.14.0", "dd82aae8f72503f924fe9dd97ffe4ca694d2f17ec463dcfd365987c9752af6ee", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "7ecf91e298acfd9b24f5d761f19e8f6e6ac585b9387fb6301023f1f2cd5eed5f"}, 27 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, 28 | "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, 29 | "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, 30 | } 31 | -------------------------------------------------------------------------------- /priv/cert/placeholder: -------------------------------------------------------------------------------- 1 | The SSL certificate home 2 | -------------------------------------------------------------------------------- /priv/data/enterprise-tactics.json: -------------------------------------------------------------------------------- 1 | { 2 | "TA0001": "Initial Access | The adversary is trying to get into your network.", 3 | "TA0002": "Execution The adversary is trying to run malicious code.", 4 | "TA0003": "Persistence The adversary is trying to maintain their foothold.", 5 | "TA0004": "Privilege Escalation | The adversary is trying to gain higher-level permissions.", 6 | "TA0005": "Defense Evasion The adversary is trying to avoid being detected.", 7 | "TA0006": "Credential Access The adversary is trying to steal account names and passwords.", 8 | "TA0007": "Discovery The adversary is trying to figure out your environment.", 9 | "TA0008": "Lateral Movement | The adversary is trying to move through your environment.", 10 | "TA0009": "Collection | The adversary is trying to gather data of interest to their goal.", 11 | "TA0010": "Exfiltration | The adversary is trying to steal data.", 12 | "TA0011": "Command and Control The adversary is trying to communicate with compromised systems to control them.", 13 | "TA0040": "Impact | The adversary is trying to manipulate, interrupt, or destroy your systems and data.", 14 | "TA0042": "Resource Development | The adversary is trying to establish resources they can use to support operations.", 15 | "TA0043": "Reconnaissance | The adversary is trying to gather information they can use to plan future operations." 16 | } -------------------------------------------------------------------------------- /priv/data/files.txt: -------------------------------------------------------------------------------- 1 | .3DM Rhino 3D Model 2 | .3DS 3D Studio Scene 3 | .3G2 3GPP2 Multimedia File 4 | .3GP 3GPP Multimedia File 5 | .7Z 7-Zip Compressed File 6 | .ACCDB Access 2007 Database File 7 | .AI Adobe Illustrator File 8 | .AIF Audio Interchange File Format 9 | .APK Android Package File 10 | .APP macOS Application 11 | .ASF Advanced Systems Format File 12 | .ASP Active Server Page 13 | .ASPX Active Server Page Extended File 14 | .AVI Audio Video Interleave File 15 | .B Grand Theft Auto 3 Saved Game File 16 | .BAK Backup File 17 | .BAT DOS Batch File 18 | .BIN Binary Disc Image 19 | .BMP Bitmap Image File 20 | .C C/C++ Source Code File 21 | .CAB Windows Cabinet File 22 | .CBR Comic Book RAR Archive 23 | .CER Internet Security Certificate 24 | .CFG Configuration File 25 | .CFM ColdFusion Markup File 26 | .CGI Common Gateway Interface Script 27 | .CLASS Java Class File 28 | .COM DOS Command File 29 | .CPL Windows Control Panel Item 30 | .CPP C++ Source Code File 31 | .CRDOWNLOAD Chrome Partially Downloaded File 32 | .CRX Chrome Extension 33 | .CS C# Source Code File 34 | .CSR Certificate Signing Request File 35 | .CSS Cascading Style Sheet 36 | .CSV Comma Separated Values File 37 | .CUE Cue Sheet File 38 | .CUR Windows Cursor 39 | .DAT Data File 40 | .DB Database File 41 | .DBF Database File 42 | .DCR Shockwave Media File 43 | .DDS DirectDraw Surface Image 44 | .DEB Debian Software Package 45 | .DEM Video Game Demo File 46 | .DESKTHEMEPACK Windows 8 Desktop Theme Pack File 47 | .DLL Dynamic Link Library 48 | .DMG Apple Disk Image 49 | .DMP Windows Memory Dump 50 | .DOC Microsoft Word Document (Legacy) 51 | .DOCX Microsoft Word Document 52 | .DRV Device Driver 53 | .DTD Document Type Definition File 54 | .DWG AutoCAD Drawing Database File 55 | .DXF Drawing Exchange Format File 56 | .EPS Encapsulated PostScript File 57 | .EXE Windows Executable File 58 | .FLA Adobe Animate Animation 59 | .FLV Flash Video File 60 | .FNT Windows Font File 61 | .FON Windows Font Library 62 | .GADGET Windows Gadget 63 | .GAM Saved Game File 64 | .GED GEDCOM Genealogy Data File 65 | .GIF Graphical Interchange Format File 66 | .GPX GPS Exchange File 67 | .GZ Gnu Zipped Archive 68 | .H C/C++/Objective-C Header File 69 | .HEIC High Efficiency Image Format 70 | .HQX BinHex 4.0 Encoded File 71 | .HTM Hypertext Markup Language File 72 | .HTML Hypertext Markup Language File 73 | .ICNS macOS Icon Resource File 74 | .ICO Icon File 75 | .ICS Calendar File 76 | .IFF Interchange File Format 77 | .INDD Adobe InDesign Document 78 | .INI Windows Initialization File 79 | .ISO Disc Image File 80 | .JAR Java Archive File 81 | .JAVA Java Source Code File 82 | .JPG JPEG Image 83 | .JS JavaScript File 84 | .JSP Java Server Page 85 | .KEY Apple Keynote Presentation 86 | .KEYCHAIN Mac OS X Keychain File 87 | .KML Keyhole Markup Language File 88 | .KMZ Google Earth Placemark File 89 | .LNK Windows Shortcut 90 | .LOG Log File 91 | .LUA Lua Source File 92 | .M Objective-C Implementation File 93 | .M3U Media Playlist File 94 | .M4A MPEG-4 Audio File 95 | .M4V iTunes Video File 96 | .MAX 3ds Max Scene File 97 | .MDB Microsoft Access Database 98 | .MDF Media Disc Image File 99 | .MID MIDI File 100 | .MIM Multi-Purpose Internet Mail Message File 101 | .MOV Apple QuickTime Movie 102 | .MP3 MP3 Audio File 103 | .MP4 MPEG-4 Video File 104 | .MPA MPEG-2 Audio File 105 | .MPG MPEG Video File 106 | .MSG Outlook Mail Message 107 | .MSI Windows Installer Package 108 | .NES Nintendo (NES) ROM File 109 | .OBJ Wavefront 3D Object File 110 | .ODT OpenDocument Text Document 111 | .OTF OpenType Font 112 | .PAGES Apple Pages Document 113 | .PART Partially Downloaded File 114 | .PCT Picture File 115 | .PDB Program Database 116 | .PDF Portable Document Format File 117 | .PHP PHP Source Code File 118 | .PKG Mac OS X Installer Package 119 | .PL Perl Script 120 | .PLUGIN Mac OS X Plugin 121 | .PNG Portable Network Graphic 122 | .PPT Microsoft PowerPoint Presentation (Legacy) 123 | .PPTX Microsoft PowerPoint Presentation 124 | .PRF Outlook Profile File 125 | .PSD Adobe Photoshop Document 126 | .PSPIMAGE PaintShop Pro Image 127 | .PY Python Script 128 | .RAR WinRAR Compressed Archive 129 | .RM RealMedia File 130 | .ROM N64 Game ROM File 131 | .RPM Red Hat Package Manager File 132 | .RSS Rich Site Summary 133 | .RTF Rich Text Format File 134 | .SAV Saved Game 135 | .SDF Standard Data File 136 | .SH Bash Shell Script 137 | .SITX StuffIt X Archive 138 | .SLN Visual Studio Solution File 139 | .SQL Structured Query Language Data File 140 | .SRT SubRip Subtitle File 141 | .SVG Scalable Vector Graphics File 142 | .SWF Shockwave Flash Movie 143 | .SWIFT Swift Source Code File 144 | .SYS Windows System File 145 | .TAR Consolidated Unix File Archive 146 | .TAR.GZ Compressed Tarball File 147 | .TAX2016 TurboTax 2016 Tax Return 148 | .TAX2020 TurboTax 2020 Tax Return 149 | .TEX LaTeX Source Document 150 | .TGA Targa Graphic 151 | .THM Thumbnail Image File 152 | .TIF Tagged Image File 153 | .TIFF Tagged Image File Format 154 | .TMP Temporary File 155 | .TOAST Toast Disc Image 156 | .TORRENT BitTorrent File 157 | .TTF TrueType Font 158 | .TXT Plain Text File 159 | .UUE Uuencoded File 160 | .VB Visual Basic Project Item File 161 | .VCD Virtual CD 162 | .VCF vCard File 163 | .VCXPROJ Visual C++ Project 164 | .VOB DVD Video Object File 165 | .WAV WAVE Audio File 166 | .WMA Windows Media Audio File 167 | .WMV Windows Media Video 168 | .WPD WordPerfect Document 169 | .WPS Microsoft Works Word Processor Document 170 | .WSF Windows Script File 171 | .XCODEPROJ Xcode Project 172 | .XHTML Extensible Hypertext Markup Language File 173 | .XLR Works Spreadsheet 174 | .XLS Microsoft Excel Spreadsheet (Legacy) 175 | .XLSX Microsoft Excel Spreadsheet 176 | .XML XML File 177 | .YUV YUV Encoded Image File 178 | .ZIP Zipped File 179 | .ZIPX Extended Zip Archive -------------------------------------------------------------------------------- /priv/static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocsf/ocsf-server/caf8877bd503c1ca32de03466dc9594d6af18b98/priv/static/apple-touch-icon.png -------------------------------------------------------------------------------- /priv/static/css/app.css: -------------------------------------------------------------------------------- 1 | /* OCSF Server - Optimized CSS Architecture */ 2 | /* Import order is important - variables first, then base styles, then components */ 3 | 4 | @import url('./variables.css'); 5 | @import url('./base.css'); 6 | @import url('./components.css'); 7 | @import url('./layout.css'); 8 | @import url('./tables.css'); 9 | -------------------------------------------------------------------------------- /priv/static/css/base.css: -------------------------------------------------------------------------------- 1 | /* Base Typography and Layout */ 2 | body { 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; 4 | font-size: 0.875rem; 5 | line-height: 1.6; 6 | color: var(--text-primary); 7 | background-color: var(--background-secondary) !important; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | } 11 | 12 | /* Typography Elements */ 13 | h1, h2, h3, h4, h5, h6 { 14 | color: var(--text-primary); 15 | } 16 | 17 | p, div, span, td, th, li { 18 | color: var(--text-primary); 19 | } 20 | 21 | strong { 22 | color: var(--text-primary); 23 | font-weight: 600; 24 | } 25 | 26 | a { 27 | color: var(--primary-color); 28 | text-decoration: none; 29 | transition: var(--transition-fast); 30 | } 31 | 32 | a:hover, a:focus, a:active { 33 | color: var(--accent-color); 34 | text-decoration: underline; 35 | } 36 | 37 | /* Code Elements */ 38 | code { 39 | color: #dc2626; 40 | font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; 41 | font-size: 0.875em; 42 | font-weight: 500; 43 | white-space: normal; 44 | word-wrap: break-word; 45 | word-break: break-word; 46 | overflow-wrap: break-word; 47 | } 48 | 49 | [data-theme="dark"] code { 50 | color: #f87171; 51 | } 52 | 53 | pre { 54 | color: var(--text-primary); 55 | background: var(--background-secondary); 56 | border: 1px solid var(--border-color); 57 | border-radius: var(--radius-sm); 58 | padding: var(--spacing-md); 59 | overflow-x: auto; 60 | } 61 | 62 | /* Utility Classes */ 63 | .text-secondary { 64 | color: var(--text-secondary) !important; 65 | } 66 | 67 | .text-muted { 68 | color: var(--text-muted) !important; 69 | } 70 | 71 | .required { 72 | color: var(--error-color); 73 | font-weight: 500; 74 | } 75 | 76 | .optional { 77 | color: var(--text-secondary); 78 | font-weight: 300; 79 | } 80 | 81 | .capitalize { 82 | text-transform: capitalize; 83 | } 84 | 85 | .uid { 86 | font-weight: 400; 87 | color: var(--text-secondary); 88 | } 89 | 90 | 91 | .enum-value, 92 | span[style*="color: red"] { 93 | color: #dc2626; 94 | white-space: nowrap; 95 | } 96 | 97 | [data-theme="dark"] .enum-value, 98 | [data-theme="dark"] span[style*="color: red"] { 99 | color: #f87171; 100 | } 101 | 102 | 103 | /* Special Elements */ 104 | blockquote { 105 | font-style: italic; 106 | color: var(--text-secondary); 107 | border-left: 4px solid var(--accent-color); 108 | padding-left: var(--spacing-md); 109 | background: var(--background-secondary); 110 | } 111 | 112 | blockquote small { 113 | font-style: normal; 114 | } 115 | 116 | sup { 117 | font-weight: 500; 118 | color: var(--text-secondary); 119 | } 120 | 121 | hr { 122 | margin-top: 0.1rem; 123 | margin-bottom: 0.3rem; 124 | background-color: #68558e80; 125 | border: none; 126 | height: 1px; 127 | } 128 | 129 | [data-theme="dark"] hr { 130 | background-color: rgba(20, 184, 166, 0.5); 131 | } 132 | 133 | /* Description lists */ 134 | dt { 135 | color: var(--text-primary); 136 | font-weight: 600; 137 | } 138 | 139 | dd { 140 | color: var(--text-secondary); 141 | } 142 | 143 | /* Status badges */ 144 | .status-badge { 145 | display: inline-flex; 146 | align-items: center; 147 | gap: var(--spacing-xs); 148 | padding: var(--spacing-xs) var(--spacing-sm); 149 | border-radius: var(--radius-sm); 150 | font-size: 0.75rem; 151 | font-weight: 500; 152 | } 153 | 154 | .status-badge.required { 155 | background: rgba(220, 53, 69, 0.1); 156 | color: var(--error-color); 157 | } 158 | 159 | .status-badge.optional { 160 | background: rgba(108, 117, 125, 0.1); 161 | color: var(--text-secondary); 162 | } 163 | 164 | .status-badge.deprecated { 165 | background: rgba(255, 193, 7, 0.1); 166 | color: var(--warning-color); 167 | } 168 | 169 | /* Deprecated "D" notation styling */ 170 | .bg-warning, 171 | sup.bg-warning { 172 | background-color: #ffc107 !important; 173 | color: #212529 !important; 174 | padding: 2px 4px; 175 | border-radius: var(--radius-sm); 176 | font-weight: 600; 177 | font-size: 0.7em; 178 | line-height: 1; 179 | } 180 | 181 | [data-theme="dark"] .bg-warning, 182 | [data-theme="dark"] sup.bg-warning { 183 | background-color: #f59e0b !important; 184 | color: #1f2937 !important; 185 | box-shadow: 0 1px 3px rgba(245, 158, 11, 0.3); 186 | } 187 | 188 | 189 | /* Smooth deprecated element animations */ 190 | .deprecated { 191 | transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), 192 | transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), 193 | max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1); 194 | overflow: hidden; 195 | } 196 | 197 | .deprecated-hidden { 198 | opacity: 0; 199 | transform: scaleY(0.8) translateY(-10px); 200 | max-height: 0; 201 | padding-top: 0 !important; 202 | padding-bottom: 0 !important; 203 | margin-top: 0 !important; 204 | margin-bottom: 0 !important; 205 | pointer-events: none; 206 | } 207 | 208 | .deprecated-showing { 209 | opacity: 0; 210 | transform: scaleY(0.8) translateY(-10px); 211 | max-height: 1000px; 212 | } 213 | 214 | .deprecated-visible { 215 | opacity: 1; 216 | transform: scaleY(1) translateY(0); 217 | max-height: 1000px; 218 | } 219 | 220 | .deprecated-hiding { 221 | opacity: 0; 222 | transform: scaleY(0.8) translateY(-10px); 223 | max-height: 0; 224 | padding-top: 0 !important; 225 | padding-bottom: 0 !important; 226 | margin-top: 0 !important; 227 | margin-bottom: 0 !important; 228 | } 229 | 230 | [data-theme="dark"] .deprecated { 231 | opacity: 0.8; 232 | } 233 | 234 | [data-theme="dark"] .deprecated:hover { 235 | opacity: 1; 236 | } 237 | 238 | [data-theme="dark"] .deprecated-visible { 239 | opacity: 0.8; 240 | } 241 | 242 | [data-theme="dark"] .deprecated-visible:hover { 243 | opacity: 1; 244 | } 245 | 246 | /* Collapsible Elements */ 247 | [data-toggle="collapse"].collapsed .if-not-collapsed { 248 | display: none; 249 | } 250 | 251 | [data-toggle="collapse"]:not(.collapsed) .if-collapsed { 252 | display: none; 253 | } 254 | 255 | /* Tooltips */ 256 | .tooltip-inner { 257 | white-space: pre-wrap; 258 | word-wrap: normal; 259 | max-width: 500px; 260 | color: var(--text-inverse); 261 | text-align: left; 262 | background: var(--primary-color); 263 | border: 1px solid var(--primary-color); 264 | border-radius: var(--radius-md); 265 | box-shadow: var(--shadow-lg); 266 | } 267 | 268 | 269 | /* Source Indicators - Icon-based approach for extensions and profiles */ 270 | .source-indicator { 271 | font-size: 0.7rem; 272 | margin-left: 3px; 273 | cursor: help; 274 | transition: var(--transition-fast); 275 | } 276 | 277 | .extension-indicator { 278 | color: var(--primary-color); 279 | } 280 | 281 | .extension-indicator:hover { 282 | color: var(--accent-color); 283 | transform: scale(1.1); 284 | } 285 | 286 | .profile-indicator { 287 | color: var(--primary-color); 288 | } 289 | 290 | .profile-indicator:hover { 291 | color: var(--accent-color); 292 | transform: scale(1.1); 293 | } 294 | 295 | [data-theme="dark"] .extension-indicator { 296 | color: var(--primary-color); 297 | } 298 | 299 | [data-theme="dark"] .extension-indicator:hover { 300 | color: var(--accent-color); 301 | } 302 | 303 | [data-theme="dark"] .profile-indicator { 304 | color: var(--primary-color); 305 | } 306 | 307 | [data-theme="dark"] .profile-indicator:hover { 308 | color: var(--accent-color); 309 | } 310 | 311 | /* Profile highlighting in sidebar */ 312 | .profile-item { 313 | transition: var(--transition-fast); 314 | } 315 | 316 | .profile-item.profile-applicable { 317 | background: linear-gradient(90deg, rgba(8, 145, 178, 0.1) 0%, transparent 100%); 318 | padding-left: 4px; 319 | margin-left: -4px; 320 | } 321 | 322 | .profile-item.profile-applicable label { 323 | font-weight: 600; 324 | color: var(--primary-color); 325 | } 326 | 327 | .profile-item.profile-not-applicable { 328 | opacity: 0.4; 329 | } 330 | 331 | .profile-item.profile-not-applicable:hover { 332 | opacity: 0.7; 333 | } 334 | 335 | [data-theme="dark"] .profile-item.profile-applicable { 336 | background: linear-gradient(90deg, rgba(20, 184, 166, 0.15) 0%, transparent 100%); 337 | border-left-color: var(--primary-color); 338 | } 339 | 340 | [data-theme="dark"] .profile-item.profile-applicable label { 341 | color: var(--primary-color); 342 | } 343 | -------------------------------------------------------------------------------- /priv/static/css/tables.css: -------------------------------------------------------------------------------- 1 | /* Tables - Consolidated and Simplified */ 2 | table { 3 | background: var(--surface-color); 4 | border: 1px solid var(--border-light); 5 | border-radius: var(--radius-lg); 6 | overflow: hidden; 7 | box-shadow: var(--shadow-sm); 8 | margin-bottom: var(--spacing-sm); 9 | width: 100%; 10 | table-layout: fixed; 11 | word-wrap: break-word; 12 | } 13 | 14 | /* Table headers */ 15 | table thead th, 16 | table th { 17 | background: var(--surface-elevated); 18 | color: var(--text-primary); 19 | padding: var(--spacing-sm) var(--spacing-md); 20 | font-weight: 600; 21 | border-bottom: 1px solid var(--border-light); 22 | border-right: 1px solid var(--border-light); 23 | } 24 | 25 | table thead th:last-child, 26 | table th:last-child { 27 | border-right: none; 28 | } 29 | 30 | /* Table cells */ 31 | table tbody td, 32 | table td { 33 | color: var(--text-primary); 34 | background: var(--surface-color); 35 | padding: var(--spacing-sm) var(--spacing-md); 36 | border-bottom: 1px solid var(--border-light); 37 | border-right: 1px solid var(--border-light); 38 | vertical-align: top; 39 | } 40 | 41 | table tbody td:last-child, 42 | table td:last-child { 43 | border-right: none; 44 | } 45 | 46 | table tbody tr:last-child td { 47 | border-bottom: none; 48 | } 49 | 50 | table tbody tr:hover td { 51 | background: var(--surface-elevated); 52 | } 53 | 54 | /* Table column widths and styling */ 55 | .col-name { width: 10%; } 56 | .col-caption { width: 10%; } 57 | .col-group { width: 10%; } 58 | .col-requirement { width: 10%; } 59 | .col-type { width: 8%; } 60 | .col-id { width: 5%; } 61 | .col-constraints { width: 15%; } 62 | .col-references { width: 25%; } 63 | .col-description { 64 | width: 40%; 65 | word-wrap: break-word; 66 | word-break: break-word; 67 | overflow-wrap: break-word; 68 | max-width: 300px; 69 | white-space: normal; 70 | } 71 | 72 | /* Name column styling - make it stand out */ 73 | table td.name { 74 | background: rgba(15, 27, 92, 0.05); 75 | font-weight: 600; 76 | } 77 | 78 | table td.name a { 79 | color: var(--primary-color); 80 | font-weight: 600; 81 | text-decoration: none; 82 | transition: var(--transition-fast); 83 | } 84 | 85 | table td.name a:hover { 86 | color: var(--accent-color); 87 | text-decoration: underline; 88 | } 89 | 90 | /* Dark mode adjustments for name column */ 91 | [data-theme="dark"] table td.name { 92 | background: rgba(20, 184, 166, 0.1); 93 | } 94 | 95 | [data-theme="dark"] table td.name a { 96 | color: var(--primary-color); 97 | } 98 | 99 | [data-theme="dark"] table td.name a:hover { 100 | color: var(--accent-color); 101 | } 102 | 103 | /* Hover effect for name column rows */ 104 | table tbody tr:hover td.name { 105 | background: rgba(15, 27, 92, 0.1); 106 | } 107 | 108 | [data-theme="dark"] table tbody tr:hover td.name { 109 | background: rgba(20, 184, 166, 0.15); 110 | } 111 | 112 | /* Description column word wrapping */ 113 | table td:nth-child(3), 114 | table th:nth-child(3), 115 | table td[data-column="description"], 116 | table th[data-column="description"], 117 | table .description-column { 118 | word-wrap: break-word; 119 | word-break: break-word; 120 | overflow-wrap: break-word; 121 | max-width: 300px; 122 | white-space: normal; 123 | } 124 | 125 | /* Code elements in table description columns should wrap */ 126 | table td code, 127 | .col-description code, 128 | td:last-child code { 129 | white-space: normal; 130 | word-wrap: break-word; 131 | word-break: break-word; 132 | overflow-wrap: break-word; 133 | display: inline-block; 134 | max-width: 100%; 135 | } 136 | 137 | /* Special table classes */ 138 | tr.thead-color { 139 | background: var(--background-secondary); 140 | color: var(--text-primary); 141 | } 142 | 143 | tr.thead-color th { 144 | background: var(--background-secondary); 145 | color: var(--text-primary); 146 | padding: var(--spacing-sm) var(--spacing-md); 147 | font-weight: 600; 148 | border: none; 149 | border-bottom: 1px solid var(--border-light); 150 | border-right: 1px solid var(--border-light); 151 | } 152 | 153 | tr.thead-color th:last-child { 154 | border-right: none; 155 | } 156 | 157 | tr.thead-color th a { 158 | color: var(--primary-color); 159 | text-decoration: none; 160 | } 161 | 162 | tr.thead-color th a:hover { 163 | color: var(--accent-color); 164 | text-decoration: underline; 165 | } 166 | 167 | table.table-borderless { 168 | border: none; 169 | box-shadow: none; 170 | } 171 | 172 | table.table-borderless td, 173 | table.table-borderless th { 174 | border: none; 175 | } 176 | 177 | /* Enum table styling - fix overlapping and spacing issues */ 178 | .table-borderless tbody tr { 179 | margin-bottom: 0; 180 | } 181 | 182 | .table-borderless tbody td { 183 | padding: 0.2rem 0.5rem; 184 | border: none; 185 | vertical-align: top; 186 | } 187 | 188 | /* Enum number column - ensure adequate width */ 189 | .table-borderless tbody td[style*="width: 25px"] { 190 | width: 5rem !important; 191 | min-width: 5rem; 192 | padding-right: 1rem; 193 | text-align: right; 194 | } 195 | 196 | /* Enum description column */ 197 | .table-borderless tbody td:last-child { 198 | width: auto; 199 | } 200 | 201 | /* Reduce spacing in enum item descriptions */ 202 | .table-borderless .text-secondary { 203 | margin-top: 0.125rem; 204 | line-height: 1.3; 205 | font-size: 0.8rem; 206 | } 207 | 208 | /* Dark mode table adjustments */ 209 | [data-theme="dark"] table, 210 | [data-theme="dark"] table tbody td, 211 | [data-theme="dark"] table td, 212 | [data-theme="dark"] table thead th, 213 | [data-theme="dark"] table th { 214 | border-color: var(--border-color); 215 | } 216 | 217 | [data-theme="dark"] table tbody tr:hover td { 218 | background: var(--surface-elevated); 219 | } 220 | 221 | /* Multi-column layout for categories */ 222 | .multi-col { 223 | -webkit-columns: 4 18rem; 224 | -moz-columns: 4 18rem; 225 | columns: 4 18rem; 226 | } 227 | 228 | /* Categories and Multi-column Layout */ 229 | section.category { 230 | background: var(--surface-color); 231 | border: 1px solid var(--border-light); 232 | border-radius: var(--radius-lg); 233 | padding: var(--spacing-md); 234 | margin-bottom: var(--spacing-md); 235 | box-shadow: var(--shadow-sm); 236 | transition: box-shadow var(--transition-normal), border-color var(--transition-normal), background-color var(--transition-normal); 237 | -webkit-break-inside: avoid; 238 | -moz-break-inside: avoid; 239 | break-inside: avoid; 240 | } 241 | 242 | section.category:hover { 243 | box-shadow: var(--shadow-md); 244 | border-color: var(--accent-color); 245 | background: var(--surface-elevated); 246 | } 247 | 248 | section.category header { 249 | background: var(--primary-color); 250 | color: var(--text-inverse); 251 | font-weight: 600; 252 | padding: var(--spacing-sm); 253 | margin: calc(-1 * var(--spacing-md)) calc(-1 * var(--spacing-md)) var(--spacing-sm) calc(-1 * var(--spacing-md)); 254 | border-radius: var(--radius-lg) var(--radius-lg) 0 0; 255 | display: flex; 256 | align-items: center; 257 | justify-content: space-between; 258 | } 259 | 260 | section.category header a { 261 | color: var(--text-inverse); 262 | text-decoration: none; 263 | font-size: 1rem; 264 | } 265 | 266 | section.category header a:hover { 267 | color: var(--accent-light); 268 | text-decoration: none; 269 | } 270 | 271 | section.category div.ocsf-class { 272 | margin-left: 0; 273 | padding: var(--spacing-xs) 0; 274 | border-bottom: 1px solid var(--border-light); 275 | transition: var(--transition-fast); 276 | } 277 | 278 | section.category div.ocsf-class:last-child { 279 | border-bottom: none; 280 | padding-bottom: 0; 281 | } 282 | 283 | section.category div.ocsf-class:hover { 284 | background: var(--background-secondary); 285 | padding-left: var(--spacing-sm); 286 | border-radius: var(--radius-sm); 287 | } 288 | 289 | section.category div.ocsf-class a { 290 | color: var(--primary-color); 291 | font-weight: 500; 292 | text-decoration: none; 293 | } 294 | 295 | section.category div.ocsf-class a:hover { 296 | color: var(--accent-color); 297 | } 298 | 299 | 300 | /* Responsive improvements */ 301 | @media (max-width: 768px) { 302 | section.category { 303 | margin-bottom: var(--spacing-md); 304 | } 305 | 306 | .multi-col { 307 | -webkit-columns: 1; 308 | -moz-columns: 1; 309 | columns: 1; 310 | } 311 | 312 | /* Make tables more mobile-friendly */ 313 | table { 314 | font-size: 0.8rem; 315 | } 316 | 317 | table td, 318 | table th { 319 | padding: var(--spacing-sm); 320 | } 321 | 322 | /* Stack table columns on very small screens */ 323 | @media (max-width: 480px) { 324 | table, thead, tbody, th, td, tr { 325 | display: block; 326 | } 327 | 328 | thead tr { 329 | position: absolute; 330 | top: -9999px; 331 | left: -9999px; 332 | } 333 | 334 | tr { 335 | border: 1px solid var(--border-color); 336 | margin-bottom: var(--spacing-sm); 337 | border-radius: var(--radius-md); 338 | padding: var(--spacing-sm); 339 | } 340 | 341 | td { 342 | border: none !important; 343 | position: relative; 344 | padding-left: 50% !important; 345 | padding-top: var(--spacing-xs); 346 | padding-bottom: var(--spacing-xs); 347 | } 348 | 349 | td:before { 350 | content: attr(data-label) ": "; 351 | position: absolute; 352 | left: 6px; 353 | width: 45%; 354 | padding-right: 10px; 355 | white-space: nowrap; 356 | font-weight: 600; 357 | color: var(--text-secondary); 358 | } 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /priv/static/css/variables.css: -------------------------------------------------------------------------------- 1 | /* CSS Custom Properties - Variables Only */ 2 | :root { 3 | /* Refined Color Palette - Sophisticated blue tones */ 4 | --primary-color: #0F1B5C; 5 | --primary-light: #1E3A8A; 6 | --primary-dark: #03144e; 7 | --accent-color: #0891B2; 8 | --accent-light: #06B6D4; 9 | --accent-dark: #0E7490; 10 | 11 | /* RGB values for alpha transparency */ 12 | --accent-color-rgb: 8, 145, 178; 13 | --primary-color-rgb: 15, 27, 92; 14 | 15 | /* Neutral Colors - Light Mode */ 16 | --background-primary: #FEFEFE; 17 | --background-secondary: #F9FAFB; 18 | --surface-color: #FFFFFF; 19 | --surface-elevated: #F8FAFC; 20 | --border-color: #E2E8F0; 21 | --border-light: #E2E8F0; 22 | 23 | /* Text Colors - Light Mode */ 24 | --text-primary: #1E293B; 25 | --text-secondary: #55585e; 26 | --text-muted: #94A3B8; 27 | --text-inverse: #FFFFFF; 28 | 29 | /* Status Colors */ 30 | --success-color: #059669; 31 | --warning-color: #D97706; 32 | --error-color: #DC2626; 33 | --info-color: #0284C7; 34 | 35 | /* Spacing */ 36 | --spacing-xs: 0.25rem; 37 | --spacing-sm: 0.5rem; 38 | --spacing-md: 1rem; 39 | --spacing-lg: 1.5rem; 40 | --spacing-xl: 2rem; 41 | 42 | /* Border Radius */ 43 | --radius-sm: 0.25rem; 44 | --radius-md: 0.5rem; 45 | --radius-lg: 0.75rem; 46 | 47 | /* Shadows - Light Mode */ 48 | --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.03); 49 | --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -1px rgba(0, 0, 0, 0.04); 50 | --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -2px rgba(0, 0, 0, 0.04); 51 | 52 | /* Transitions */ 53 | --transition-fast: 150ms ease-in-out; 54 | --transition-normal: 250ms ease-in-out; 55 | 56 | /* Theme Toggle Sizes */ 57 | --toggle-width: 36px; 58 | --toggle-height: 18px; 59 | --toggle-slider-size: 13px; 60 | --toggle-icon-size: 8px; 61 | } 62 | 63 | /* Dark Mode Variables */ 64 | [data-theme="dark"], 65 | :root:not([data-theme="light"]):is([data-theme="dark"], :has(*)) { 66 | --background-primary: #141018; 67 | --background-secondary: #1f1a24; 68 | --surface-color: #2a2530; 69 | --surface-elevated: #35303c; 70 | --border-color: #35303c; 71 | --border-light: #35303c; 72 | 73 | --text-primary: #f8f6fa; 74 | --text-secondary: #e2d8e8; 75 | --text-muted: #b8a9c9; 76 | --text-inverse: #141018; 77 | 78 | --shadow-sm: 0 1px 2px 0 rgba(20, 16, 24, 0.5); 79 | --shadow-md: 0 4px 6px -1px rgba(20, 16, 24, 0.6), 0 2px 4px -1px rgba(20, 16, 24, 0.5); 80 | --shadow-lg: 0 10px 15px -3px rgba(20, 16, 24, 0.6), 0 4px 6px -2px rgba(20, 16, 24, 0.5); 81 | 82 | --primary-color: #14B8A6; 83 | --primary-light: #5EEAD4; 84 | --primary-dark: #0F766E; 85 | --accent-color: #2DD4BF; 86 | --accent-light: #7DD3FC; 87 | --accent-dark: #0891B2; 88 | 89 | /* RGB values for alpha transparency - Dark Mode */ 90 | --accent-color-rgb: 45, 212, 191; 91 | --primary-color-rgb: 20, 184, 166; 92 | } 93 | -------------------------------------------------------------------------------- /priv/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocsf/ocsf-server/caf8877bd503c1ca32de03466dc9594d6af18b98/priv/static/favicon.ico -------------------------------------------------------------------------------- /priv/static/images/ocsf-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocsf/ocsf-server/caf8877bd503c1ca32de03466dc9594d6af18b98/priv/static/images/ocsf-logo.png -------------------------------------------------------------------------------- /priv/static/images/ocsf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocsf/ocsf-server/caf8877bd503c1ca32de03466dc9594d6af18b98/priv/static/images/ocsf.png -------------------------------------------------------------------------------- /priv/static/js/profiles.js: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // http://www.apache.org/licenses/LICENSE-2.0 5 | // Unless required by applicable law or agreed to in writing, software 6 | // distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | // See the License for the specific language governing permissions and 9 | // limitations under the License. 10 | 11 | function get_selected_profiles() { 12 | // First check URL parameters, then fall back to localStorage 13 | const urlParams = new URLSearchParams(window.location.search); 14 | const profilesParam = urlParams.get('profiles'); 15 | 16 | if (profilesParam !== null) { 17 | return profilesParam === '' ? [] : profilesParam.split(',').map(p => p.trim()); 18 | } 19 | 20 | return JSON.parse(localStorage.getItem('schema_profiles')) || []; 21 | } 22 | 23 | function set_selected_profiles(profiles) { 24 | localStorage.setItem("schema_profiles", JSON.stringify(profiles)); 25 | } 26 | 27 | function select_profiles(selected) { 28 | if (selected && selected.length > 0) { 29 | return '&profiles=' + selected.join(','); 30 | } 31 | return ''; 32 | } 33 | 34 | function init_selected_profiles(profiles) { 35 | if (profiles == null) 36 | profiles = get_selected_profiles(); 37 | 38 | if (profiles.length == 0) { 39 | $(".ocsf-class").each(function(i, e) { 40 | e.classList.remove('d-none'); 41 | }); 42 | } else { 43 | $.each(profiles, function(index, element) { 44 | $("#" + element.replace("/", "-")).prop('checked', true); 45 | }); 46 | 47 | $(".ocsf-class").each(function(i, e) { 48 | let n = 0; 49 | let list = (e.dataset["profiles"] || "").split(","); 50 | 51 | $.each(profiles, function(index, element) { 52 | if (list.indexOf(element) >= 0) 53 | n = n + 1; 54 | }); 55 | 56 | if (profiles.length == n) 57 | e.classList.remove('d-none'); 58 | else 59 | e.classList.add('d-none'); 60 | }); 61 | } 62 | } 63 | 64 | function init_class_profiles() { 65 | let profiles = $("#profiles-list :checkbox"); 66 | profiles.on("change", function() { 67 | selected_profiles = []; 68 | profiles.each(function(){ 69 | if (this.checked) 70 | selected_profiles.push(this.dataset["profile"]) 71 | }); 72 | 73 | set_selected_profiles(selected_profiles); 74 | 75 | // Update URL with both extensions and profiles using the unified function 76 | const selected_extensions = get_selected_extensions(); 77 | const params = build_url_params(selected_extensions, selected_profiles); 78 | 79 | // Update the URL 80 | window.location.search = params; 81 | }); 82 | } 83 | 84 | function init_extension_profile_dependencies() { 85 | // Hide/show profiles based on extension selection on page load 86 | updateProfileVisibility(); 87 | 88 | let extensions = $("#extensions-list :checkbox"); 89 | extensions.on("change", function() { 90 | const extensionName = this.id; 91 | const isExtensionChecked = this.checked; 92 | 93 | if (!isExtensionChecked) { 94 | // When extension is unchecked, uncheck all profiles that belong to this extension 95 | let profiles = $("#profiles-list :checkbox"); 96 | profiles.each(function() { 97 | const profileName = this.dataset["profile"]; 98 | if (profileName && profileName.startsWith(extensionName + "/")) { 99 | this.checked = false; 100 | } 101 | }); 102 | 103 | // Update the selected profiles list 104 | let selected_profiles = []; 105 | profiles.each(function(){ 106 | if (this.checked) 107 | selected_profiles.push(this.dataset["profile"]) 108 | }); 109 | 110 | set_selected_profiles(selected_profiles); 111 | init_selected_profiles(selected_profiles); 112 | if (typeof refresh_selected_profiles === 'function') { 113 | refresh_selected_profiles(); 114 | } 115 | } 116 | 117 | // Update profile visibility when extension selection changes 118 | updateProfileVisibility(); 119 | }); 120 | } 121 | 122 | function updateProfileVisibility() { 123 | let extensions = $("#extensions-list :checkbox"); 124 | let profiles = $("#profiles-list .profile-item"); 125 | 126 | // Get list of selected extensions 127 | let selectedExtensions = []; 128 | extensions.each(function() { 129 | if (this.checked) { 130 | selectedExtensions.push(this.id); 131 | } 132 | }); 133 | 134 | // Show/hide profiles based on extension selection 135 | profiles.each(function() { 136 | const profileItem = $(this); 137 | const profileName = profileItem.data("profile-name"); 138 | 139 | if (profileName) { 140 | // Check if this profile belongs to an extension 141 | let belongsToExtension = false; 142 | let shouldShow = true; 143 | 144 | for (let extension of selectedExtensions) { 145 | if (profileName.startsWith(extension + "/")) { 146 | belongsToExtension = true; 147 | break; 148 | } 149 | } 150 | 151 | // If profile belongs to an extension, only show it if that extension is selected 152 | if (profileName.includes("/")) { 153 | // This is an extension profile 154 | shouldShow = belongsToExtension; 155 | } else { 156 | // This is a core profile, always show it 157 | shouldShow = true; 158 | } 159 | 160 | if (shouldShow) { 161 | profileItem.show(); 162 | } else { 163 | profileItem.hide(); 164 | // Also uncheck hidden profiles 165 | profileItem.find("input[type='checkbox']").prop('checked', false); 166 | } 167 | } 168 | }); 169 | 170 | // Update the selected profiles list after hiding profiles 171 | let selected_profiles = []; 172 | $("#profiles-list :checkbox:visible").each(function(){ 173 | if (this.checked) 174 | selected_profiles.push(this.dataset["profile"]) 175 | }); 176 | 177 | set_selected_profiles(selected_profiles); 178 | init_selected_profiles(selected_profiles); 179 | if (typeof refresh_selected_profiles === 'function') { 180 | refresh_selected_profiles(); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /priv/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | User-agent: * 5 | Disallow: / 6 | -------------------------------------------------------------------------------- /priv/static/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocsf/ocsf-server/caf8877bd503c1ca32de03466dc9594d6af18b98/priv/static/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /rel/env.sh.eex: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Sets and enables heart (recommended only in daemon mode) 4 | case $RELEASE_COMMAND in 5 | daemon*) 6 | HEART_COMMAND="$RELEASE_ROOT/bin/$RELEASE_NAME $RELEASE_COMMAND" 7 | export HEART_COMMAND 8 | export ELIXIR_ERL_OPTIONS="-heart" 9 | ;; 10 | *) 11 | ;; 12 | esac 13 | 14 | # Set the release to work across nodes. If using the long name format like 15 | # the one below (my_app@127.0.0.1), you need to also uncomment the 16 | # RELEASE_DISTRIBUTION variable below. 17 | # export RELEASE_DISTRIBUTION=icd_schema 18 | # export RELEASE_NODE=<%= @release.name %>@127.0.0.1 19 | -------------------------------------------------------------------------------- /rel/overlays/bin/server: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd -P -- "$(dirname -- "$0")" 3 | PHX_SERVER=true exec ./schema_server start 4 | -------------------------------------------------------------------------------- /rel/overlays/bin/server.bat: -------------------------------------------------------------------------------- 1 | set PHX_SERVER=true 2 | call "%~dp0\schema_server" start 3 | -------------------------------------------------------------------------------- /rel/vm.args.eex: -------------------------------------------------------------------------------- 1 | ## Customize flags given to the VM: http://erlang.org/doc/man/erl.html 2 | ## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here 3 | 4 | ## Number of dirty schedulers doing IO work (file, sockets, etc) 5 | ##+SDio 5 6 | 7 | ## Increase number of concurrent ports/sockets 8 | ##+Q 65536 9 | 10 | ## Tweak GC to run more often 11 | ##-env ERL_FULLSWEEP_AFTER 10 12 | -------------------------------------------------------------------------------- /test/schema/check_enums.exs: -------------------------------------------------------------------------------- 1 | defmodule Schema.CheckEnums do 2 | def classes() do 3 | Enum.each(Schema.classes(), fn {name, _class} -> 4 | each(Schema.class(name)) |> print(name) 5 | end) 6 | end 7 | 8 | def objects() do 9 | Enum.each(Schema.objects(), fn {name, obj} -> 10 | each(obj) |> print(name) 11 | end) 12 | end 13 | 14 | defp print([], _name) do 15 | :ok 16 | end 17 | 18 | defp print(list, name) do 19 | text = Enum.join(list, ", ") 20 | IO.puts(name) 21 | IO.puts(" #{text}") 22 | end 23 | 24 | defp each(map) do 25 | Map.get(map, :attributes) |> Map.new() |> check() 26 | end 27 | 28 | defp check(attributes) do 29 | Enum.reduce(attributes, [], fn {name, attribute}, acc -> 30 | if is_enum?(attribute) do 31 | check_enum(attributes, name, attribute, acc) 32 | else 33 | acc 34 | end 35 | end) 36 | end 37 | 38 | defp is_enum?(attribute) do 39 | # and Map.get(attribute, :requirement) != "reserved" 40 | Map.has_key?(attribute, :enum) 41 | end 42 | 43 | defp check_enum(attributes, name, attribute, acc) do 44 | enum = Map.get(attribute, :enum) 45 | key = Schema.Enums.sibling(name, attribute) 46 | 47 | case Map.get(attributes, key) do 48 | nil -> 49 | name = Atom.to_string(name) 50 | if Map.has_key?(enum, :"-1") do 51 | ["#{name}*" | acc] 52 | else 53 | [name | acc] 54 | end 55 | 56 | sibling -> 57 | if attribute[:requirement] != sibling[:requirement] do 58 | IO.puts("requirement for #{name} differ from #{key}") 59 | end 60 | acc 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/schema/compilation_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Schema.CompilationTest do 2 | use ExUnit.Case 3 | 4 | test "Schema compilation should not have any errors" do 5 | # Find the most recent test log file 6 | log_dir = "log" 7 | 8 | # Ensure the log directory exists 9 | File.mkdir_p!(log_dir) 10 | 11 | try do 12 | # Find all test log files and get the most recent one 13 | {:ok, files} = File.ls(log_dir) 14 | 15 | log_path = files 16 | |> Enum.filter(&String.starts_with?(&1, "test_")) 17 | |> Enum.filter(&String.ends_with?(&1, ".log")) 18 | |> Enum.sort(:desc) 19 | |> List.first() 20 | |> case do 21 | nil -> nil 22 | filename -> Path.join(log_dir, filename) 23 | end 24 | 25 | # Read the log file if it exists 26 | log_content = case log_path do 27 | nil -> "" 28 | path -> case File.read(path) do 29 | {:ok, content} -> content 30 | {:error, _} -> "" 31 | end 32 | end 33 | 34 | # Check for error messages in the log 35 | error_lines = log_content 36 | |> String.split("\n") 37 | |> Enum.filter(fn line -> 38 | String.contains?(line, "[error]") and String.trim(line) != "" 39 | end) 40 | 41 | # If there are any error lines, fail the test with those errors 42 | if length(error_lines) > 0 do 43 | formatted_errors = error_lines 44 | |> Enum.map(fn line -> 45 | # Extract just the error message part 46 | case Regex.run(~r/\[error\]\s*(.*)/, line) do 47 | [_full_match, error_msg] -> " - #{String.trim(error_msg)}" 48 | _ -> " - #{line}" 49 | end 50 | end) 51 | |> Enum.join("\n") 52 | 53 | flunk("Schema compilation errors found:\n#{formatted_errors}") 54 | end 55 | 56 | assert true, "Schema compilation passed with no errors" 57 | after 58 | # Clean up all test log files after processing (always runs) 59 | case File.ls(log_dir) do 60 | {:ok, files} -> 61 | files 62 | |> Enum.filter(&String.starts_with?(&1, "test_")) 63 | |> Enum.filter(&String.ends_with?(&1, ".log")) 64 | |> Enum.each(fn old_file -> 65 | File.rm(Path.join(log_dir, old_file)) 66 | end) 67 | {:error, _} -> :ok 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /test/schema/update_types.exs: -------------------------------------------------------------------------------- 1 | defmodule Schema.UpdateTypes do 2 | def update(path) do 3 | File.read!(path) 4 | |> Jason.decode!() 5 | |> Map.update!("attributes", fn attributes -> 6 | Enum.into(attributes, %{}, fn {name, a} -> 7 | updated = 8 | case Map.pop(a, "object_type") do 9 | {nil, _} -> 10 | a 11 | 12 | {type, map} -> 13 | Map.put(map, "type", type) 14 | end 15 | 16 | {name, updated} 17 | end) 18 | end) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/schema_web/views/page_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SchemaWeb.PageViewTest do 2 | end 3 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule SchemaWeb.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with channels 21 | use Phoenix.ChannelTest 22 | 23 | # The default endpoint for testing 24 | @endpoint SchemaWeb.Endpoint 25 | end 26 | end 27 | 28 | setup _tags do 29 | :ok 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule SchemaWeb.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with connections 21 | use Phoenix.ConnTest 22 | alias SchemaWeb.Router.Helpers, as: Routes 23 | 24 | # The default endpoint for testing 25 | @endpoint SchemaWeb.Endpoint 26 | end 27 | end 28 | 29 | setup _tags do 30 | {:ok, conn: Phoenix.ConnTest.build_conn()} 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/categories.json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "Categories", 3 | "name": "category", 4 | "description": "The OCSF categories organize event classes, each aligned with a specific domain or area of focus.", 5 | "attributes": { 6 | "system": { 7 | "caption": "System Activity", 8 | "description": "System Activity events.", 9 | "uid": 1 10 | }, 11 | "findings": { 12 | "caption": "Findings", 13 | "description": "Findings events report findings, detections, and possible resolutions of malware, anomalies, or other actions performed by security products.", 14 | "uid": 2 15 | }, 16 | "iam": { 17 | "caption": "Identity & Access Management", 18 | "description": "Identity & Access Management (IAM) events relate to the supervision of the system's authentication and access control model. Examples of such events are the success or failure of authentication, granting of authority, password change, entity change, privileged use etc.", 19 | "uid": 3 20 | }, 21 | "network": { 22 | "caption": "Network Activity", 23 | "description": "Network Activity events.", 24 | "uid": 4 25 | }, 26 | "discovery": { 27 | "caption": "Discovery", 28 | "description": "Discovery events report the existence and state of devices, files, configurations, processes, registry keys, and other objects.", 29 | "uid": 5 30 | }, 31 | "application": { 32 | "caption": "Application Activity", 33 | "description": "Application Activity events report detailed information about the behavior of applications and services.", 34 | "uid": 6 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/events/alpha.json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "Alpha", 3 | "category": "network", 4 | "description": "The Alpha example event class.", 5 | "name": "alpha", 6 | "extends": "ghost", 7 | "uid": 1, 8 | "profiles": [], 9 | "references": [ 10 | { 11 | "url": "https://example.com/alpha", 12 | "description": "Alpha at example.com " 13 | } 14 | ], 15 | "attributes": { 16 | "alpha": { 17 | "requirement": "required", 18 | "observable": 100 19 | }, 20 | "delta": { 21 | "requirement": "optional", 22 | "observable": 101, 23 | "source": "This is from physics! (class attribute)", 24 | "references": [ 25 | { 26 | "url": "https://example.com/delta", 27 | "description": "Delta at example.com " 28 | } 29 | ] 30 | }, 31 | "ob_by_dict_type_1": { 32 | "requirement": "optional", 33 | "description": "Example 1 observable by dictionary type." 34 | }, 35 | "ob_by_dict_type_2": { 36 | "requirement": "optional", 37 | "description": "Example 2 observable by dictionary type with class attribute observable.", 38 | "observable": 10000 39 | }, 40 | "ob_by_dict_attr_1": { 41 | "requirement": "optional", 42 | "description": "Example 1 observable by dictionary attribute." 43 | }, 44 | "ob_by_dict_attr_2": { 45 | "requirement": "optional", 46 | "description": "Example 2 observable by dictionary attribute with class attribute observable.", 47 | "observable": 10001 48 | } 49 | }, 50 | "observables": { 51 | "message": 102 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/events/base_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "Base Event", 3 | "category": "other", 4 | "description": "The base event is a generic and concrete event. It also defines a set of attributes available in most event classes. As a generic event that does not belong to any event category, it could be used to log events that are not otherwise defined by the schema.", 5 | "name": "base_event", 6 | "profiles": ["datetime"], 7 | "attributes": { 8 | "$include": [ 9 | "profiles/datetime.json" 10 | ], 11 | "activity_name": { 12 | "requirement": "optional" 13 | }, 14 | "activity_id": { 15 | "enum": { 16 | "0": { 17 | "caption": "Unknown" 18 | }, 19 | "99": { 20 | "caption": "Other" 21 | } 22 | }, 23 | "requirement": "required" 24 | }, 25 | "category_name": { 26 | "requirement": "optional" 27 | }, 28 | "category_uid": { 29 | "enum": { 30 | "0": { 31 | "caption": "Uncategorized" 32 | } 33 | }, 34 | "requirement": "required" 35 | }, 36 | "class_name": { 37 | "requirement": "optional" 38 | }, 39 | "class_uid": { 40 | "enum": { 41 | "0": { 42 | "caption": "Base Event" 43 | } 44 | }, 45 | "requirement": "required" 46 | }, 47 | "message": { 48 | "group": "primary", 49 | "requirement": "recommended" 50 | }, 51 | "metadata": { 52 | "group": "context", 53 | "requirement": "required" 54 | }, 55 | "observables": { 56 | "group": "primary", 57 | "requirement": "recommended" 58 | }, 59 | "time": { 60 | "requirement": "required" 61 | }, 62 | "type_name": { 63 | "requirement": "optional" 64 | }, 65 | "type_uid": { 66 | "requirement": "required" 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/events/beta.json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "Beta", 3 | "category": "system", 4 | "description": "The Beta example event class.", 5 | "name": "beta", 6 | "extends": "ghost", 7 | "uid": 2, 8 | "profiles": [], 9 | "references": [ 10 | {"url": "https://example.com/beta?n=1", "description": "Beta (1) on example.com "}, 11 | {"url": "https://example.com/beta?n=2", "description": "Beta (2) on example.com "} 12 | ], 13 | "attributes": { 14 | "beta": { 15 | "requirement": "required", 16 | "observable": 103 17 | }, 18 | "gammas": { 19 | "requirement": "optional" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/events/eta.json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "Eta", 3 | "category": "system", 4 | "description": "The Eta example event class.", 5 | "name": "eta", 6 | "extends": "base_event", 7 | "uid": 3, 8 | "profiles": [], 9 | "attributes": { 10 | "name": { 11 | "description": "The name of this eta.", 12 | "requirement": "required", 13 | "observable": 104 14 | }, 15 | "service": { 16 | "requirement": "recommended" 17 | } 18 | }, 19 | "observables": { 20 | "service.name": 105 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/events/ghost_(hidden).json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "Ghost", 3 | "category": "network", 4 | "description": "The hidden Ghost example event class.", 5 | "name": "ghost", 6 | "extends": "base_event", 7 | "profiles": [], 8 | "attributes": { 9 | "hidden_thing": { 10 | "requirement": "required" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/events/network.json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "Network", 3 | "category": "network", 4 | "description": "The Network event represents a network connection.", 5 | "name": "network", 6 | "extends": "alpha", 7 | "uid": 4, 8 | "profiles": [ 9 | "host" 10 | ], 11 | "attributes": { 12 | "$include": [ 13 | "profiles/host.json" 14 | ], 15 | "destination_node": { 16 | "requirement": "required" 17 | }, 18 | "destination_port": { 19 | "requirement": "recommended" 20 | }, 21 | "source_node": { 22 | "requirement": "required" 23 | }, 24 | "source_port": { 25 | "requirement": "recommended" 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /test/test_ocsf_schema/events/race_flagged.json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "Race Flagged", 3 | "category": "system", 4 | "description": "Occurs when a driver or entire race is flagged.", 5 | "name": "race_flagged", 6 | "extends": "base_event", 7 | "uid": 5, 8 | "profiles": [], 9 | "attributes": { 10 | "flag_ids": { 11 | "caption": "Racing Flag IDs", 12 | "description": "The list of racing flag IDs.", 13 | "enum": { 14 | "1": { 15 | "caption": "Green Flag" 16 | }, 17 | "2": { 18 | "caption": "Yellow Flag" 19 | }, 20 | "3": { 21 | "caption": "Full Track Yellow Flag" 22 | }, 23 | "4": { 24 | "caption": "Surface Flag" 25 | }, 26 | "5": { 27 | "caption": "White Flag" 28 | }, 29 | "6": { 30 | "caption": "Red Flag" 31 | }, 32 | "7": { 33 | "caption": "Black Flag" 34 | }, 35 | "8": { 36 | "caption": "Black with Orange Circle Flag" 37 | }, 38 | "9": { 39 | "caption": "Checkered Flag" 40 | }, 41 | "10": { 42 | "caption": "Grey Flag", 43 | "@deprecated": { 44 | "message": "Grey is confusing. Use 5 (White) or 7 (Black) instead.", 45 | "since": "0.1.0-test" 46 | } 47 | } 48 | }, 49 | "requirement": "required" 50 | }, 51 | "flags": { 52 | "caption": "Racing Flags", 53 | "description": "The list of racing flags.", 54 | "requirement": "optional" 55 | }, 56 | "car_number": { 57 | "description": "When applicable, the individual car that was flagged.", 58 | "requirement": "optional" 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/extensions/rpg/categories.json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "RPC Categories", 3 | "name": "category", 4 | "description": "The OCSF categories for the RPG extension.", 5 | "attributes": { 6 | "battle": { 7 | "caption": "Battle", 8 | "description": "Battle events.", 9 | "uid": 1 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/extensions/rpg/dictionary.json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "RPG Attribute Dictionary", 3 | "description": "The Attribute Dictionary for the RPG extension.", 4 | "name": "dictionary", 5 | "attributes": { 6 | "alpha_extra": { 7 | "caption": "Alpha Extra", 8 | "description": "The alpha extra. Extra stuff for alpha. This is for testing.", 9 | "type": "string_t" 10 | }, 11 | "damage": { 12 | "caption": "Damage", 13 | "description": "Damage taken.", 14 | "type": "integer_t" 15 | }, 16 | "damage_range": { 17 | "caption": "Damage Range", 18 | "description": "The amount of damage an item can inflict, represented as a string.", 19 | "type": "string_t" 20 | }, 21 | "epsilon": { 22 | "caption": "Epsilon", 23 | "description": "Epsilon is yet another Greek alphabet thing.", 24 | "type": "string_t" 25 | }, 26 | "hammer": { 27 | "caption": "Hammer", 28 | "description": "A hammer.", 29 | "type": "hammer" 30 | }, 31 | "hp": { 32 | "caption": "Hit Points", 33 | "description": "Current hit points.", 34 | "type": "hp_t" 35 | }, 36 | "kind": { 37 | "caption": "Kind", 38 | "description": "A kind.", 39 | "type": "string_t" 40 | }, 41 | "mana": { 42 | "caption": "Mana", 43 | "description": "Current mana, a measure of magical reserves.", 44 | "type": "mana_t", 45 | "observable": 42001 46 | }, 47 | "max_hp": { 48 | "caption": "Max Hit Points", 49 | "description": "Maximum hit points.", 50 | "type": "integer_t" 51 | }, 52 | "max_mana": { 53 | "caption": "Mana", 54 | "description": "Maximum mana, a measure of magical reserves.", 55 | "type": "mana_t" 56 | }, 57 | "name": { 58 | "caption": "RPG Name", 59 | "description": "The name of the RPG entity. See specific usage.", 60 | "type": "string_t", 61 | "observable": 42002 62 | }, 63 | "style": { 64 | "caption": "Style", 65 | "description": "The style of something.", 66 | "type": "string_t" 67 | } 68 | }, 69 | "types": { 70 | "caption": "RPG Data Types", 71 | "description": "The predefined data types for the RPG extension.", 72 | "attributes": { 73 | "entity_thing_t": { 74 | "caption": "Entity Thing", 75 | "description": "An entity's thingy that's made an observable by type in the RPG extension.", 76 | "observable": 42003, 77 | "type": "string_t", 78 | "type_name": "String" 79 | }, 80 | "hp_t": { 81 | "caption": "Hit points type", 82 | "description": "Hit points are a measure of the ability to take damage.", 83 | "observable": 42004, 84 | "type": "integer_t", 85 | "type_name": "Integer" 86 | }, 87 | "mana_t": { 88 | "caption": "Mana type", 89 | "description": "Mana is a measure of magical reserves.", 90 | "type": "integer_t", 91 | "type_name": "Integer" 92 | } 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /test/test_ocsf_schema/extensions/rpg/events/alpha_(patch).json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "RPG Alpha", 3 | "description": "Patch extends of the Alpha example event class.", 4 | "extends": "alpha", 5 | "profiles": [], 6 | "references": [ 7 | { 8 | "url": "https://example.com/alpha?patched=1", 9 | "description": "Alpha (patch) at example.com " 10 | } 11 | ], 12 | "attributes": { 13 | "alpha_extra": { 14 | "requirement": "required" 15 | }, 16 | "epsilon": { 17 | "requirement": "recommended" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/extensions/rpg/events/damage.json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "Damage", 3 | "category": "battle", 4 | "description": "The Damage event represents an instance of damage taken.", 5 | "name": "damage", 6 | "extends": "base_event", 7 | "uid": 1, 8 | "attributes": { 9 | "damage": { 10 | "requirement": "required" 11 | }, 12 | "hp": { 13 | "description": "Hit points after taking damage.", 14 | "requirement": "required" 15 | }, 16 | "mana": { 17 | "description": "Mana after taking damage.", 18 | "requirement": "recommended" 19 | }, 20 | "max_hp": { 21 | "requirement": "recommended" 22 | }, 23 | "max_mana": { 24 | "requirement": "recommended" 25 | }, 26 | "hammer": { 27 | "requirement": "optional" 28 | } 29 | }, 30 | "observables": { 31 | "damage": 42101, 32 | "hammer.damage_range": 42102 33 | } 34 | } -------------------------------------------------------------------------------- /test/test_ocsf_schema/extensions/rpg/events/eta_(patch).json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eta", 3 | "attributes": { 4 | "service": { 5 | "requirement": "recommended", 6 | "observable": 42103 7 | } 8 | }, 9 | "observables": { 10 | "name": 42104, 11 | "service.name": 42105 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/extensions/rpg/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "RPG", 3 | "description": "The RPG extension defines role-playing game (RPG) specific attributes, objects and classes.", 4 | "name": "rpg", 5 | "uid": 42, 6 | "version": "0.1.1-rpg-test" 7 | } 8 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/extensions/rpg/objects/device_(patch).json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "RPG Device", 3 | "description": "Patch extends of Device for the RPG extension.", 4 | "extends": "device", 5 | "attributes": { 6 | "mana": { 7 | "description": "The device's magical reserves.", 8 | "requirement": "optional" 9 | }, 10 | "uid": { 11 | "description": "The RPG Device's unique identifier.", 12 | "requirement": "recommended" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/extensions/rpg/objects/hammer.json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "Hammer", 3 | "description": "The Hammer object represents a, uh, hammer.", 4 | "name": "hammer", 5 | "attributes": { 6 | "desc": { 7 | "caption": "Description", 8 | "description": "The description of the hammer, ordinarily as reported by the manufacturer.", 9 | "requirement": "optional" 10 | }, 11 | "name": { 12 | "description": "The hammer's name. Often this is "Hammer", but could just as well be "Colin".", 13 | "requirement": "recommended" 14 | }, 15 | "damage_range": { 16 | "description": "The amount of damage that could be inflicted by this hammer.", 17 | "requirement": "recommended", 18 | "observable": 42201 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/extensions/rpg/objects/war_hammer.json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "War Hammer", 3 | "description": "The War Hammer object is a big hammer for, like, war stuff.", 4 | "name": "war_hammer", 5 | "extends": "hammer", 6 | "attributes": { 7 | "style": { 8 | "description": "The style of this war hammer.", 9 | "requirement": "recommended" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/extensions/rpg/objects/zeta_(patch).json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "RPG Zeta", 3 | "description": "Patch extends of Zeta for the RPG extension.", 4 | "extends": "zeta", 5 | "observable": 42202, 6 | "attributes": { 7 | "name": { 8 | "description": "Patched name.", 9 | "observable": 42203 10 | }, 11 | "numeric_value": { 12 | "description": "Patched numeric value.", 13 | "observable": 42204 14 | }, 15 | "kind": { 16 | "requirement": "recommended", 17 | "observable": 42205 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/extensions2/rpg2/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "RPG 2", 3 | "description": "The second RPG extension extends the RPG extension.", 4 | "name": "rpg2", 5 | "uid": 43, 6 | "version": "0.1.1-rpg2-test" 7 | } 8 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/extensions2/rpg2/objects/device_(patch).json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "RPG 2 Device", 3 | "description": "Patch extends of Device for the RPG 2 extension.", 4 | "extends": "device", 5 | "attributes": { 6 | "rpg/hp": { 7 | "requirement": "optional" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/extensions2/rpg2/objects/hammer_(patch).json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "RPG 2 Hammer Patch", 3 | "extends": "rpg/hammer", 4 | "attributes": { 5 | "rpg/hp": { 6 | "requirement": "optional" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/extensions2/rpg2/objects/sword.json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "Sword", 3 | "description": "The Sword object represents a stabby instrument of death.", 4 | "name": "sword", 5 | "attributes": { 6 | "desc": { 7 | "caption": "Description", 8 | "description": "The description of the sword, ordinarily as reported by the blacksmith.", 9 | "requirement": "optional" 10 | }, 11 | "name": { 12 | "description": "The sword's name. Named swords are just cooler.", 13 | "requirement": "recommended" 14 | }, 15 | "rpg/damage_range": { 16 | "description": "The amount of damage that could be inflicted by this sword.", 17 | "requirement": "recommended" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/objects/_entity_(hidden).json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "Entity", 3 | "name": "_entity", 4 | "description": "The Entity object is an unordered collection of attributes, with a name and unique identifier. It serves as a base object that defines a set of attributes and default constraints available in all objects that extend it.", 5 | "attributes": { 6 | "name": { 7 | "description": "The name of the entity.", 8 | "requirement": "recommended" 9 | }, 10 | "uid": { 11 | "description": "The unique identifier of the entity.", 12 | "requirement": "recommended" 13 | }, 14 | "entity_thing": { 15 | "requirement": "optional" 16 | } 17 | }, 18 | "constraints": { 19 | "at_least_one": [ 20 | "name", 21 | "uid" 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/objects/_zeta_base_(hidden).json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "Zeta Base", 3 | "description": "A zeta base.", 4 | "name": "_zeta_base", 5 | "attributes": { 6 | "name": { 7 | "description": "The zeta base's human-friendly name", 8 | "requirement": "recommended" 9 | }, 10 | "numeric_value": { 11 | "requirement": "required" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/objects/device.json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "Device", 3 | "description": "The Device object represents an addressable computer system or host, which is typically connected to a computer network and participates in the transmission or processing of data within the computer network. Defined by D3FEND d3f:Host.", 4 | "extends": "endpoint", 5 | "name": "device", 6 | "observable": 200, 7 | "references": [ 8 | { 9 | "url": "https://example.com/device", 10 | "description": "Device at example.com " 11 | } 12 | ], 13 | "attributes": { 14 | "desc": { 15 | "caption": "Description", 16 | "description": "The description of the device, ordinarily as reported by the operating system.", 17 | "requirement": "optional" 18 | }, 19 | "hostname": { 20 | "description": "The device hostname.", 21 | "requirement": "recommended", 22 | "references": [ 23 | { 24 | "url": "https://example.com/device_hostname", 25 | "description": "Device hostname at example.com " 26 | } 27 | ] 28 | }, 29 | "ip": { 30 | "description": "The device IP address, in either IPv4 or IPv6 format.", 31 | "requirement": "recommended" 32 | }, 33 | "name": { 34 | "description": "The alternate device name, ordinarily as assigned by an administrator.

Note: The Name could be any other string that helps to identify the device, such as a phone number; for example 310-555-1234.

", 35 | "requirement": "recommended" 36 | }, 37 | "type": { 38 | "description": "The device type. For example: unknown, server, desktop, laptop, tablet, mobile, virtual, browser, or other.", 39 | "requirement": "optional" 40 | }, 41 | "type_id": { 42 | "description": "The device type ID.", 43 | "requirement": "required" 44 | }, 45 | "uid": { 46 | "description": "The unique identifier of the device. For example the Windows TargetSID or AWS EC2 ARN.", 47 | "requirement": "recommended" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/objects/endpoint.json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "Endpoint", 3 | "description": "The Endpoint object describes a physical or virtual device that connects to and exchanges information with a computer network. Some examples of endpoints are mobile devices, desktop computers, virtual machines, embedded devices, and servers. Internet-of-Things devices—like cameras, lighting, refrigerators, security systems, smart speakers, and thermostats—are also endpoints.", 4 | "extends": "_entity", 5 | "name": "endpoint", 6 | "attributes": { 7 | "hostname": { 8 | "description": "The fully qualified name of the endpoint.", 9 | "requirement": "recommended" 10 | }, 11 | "ip": { 12 | "description": "The IP address of the endpoint, in either IPv4 or IPv6 format.", 13 | "requirement": "recommended" 14 | }, 15 | "name": { 16 | "description": "The short name of the endpoint.", 17 | "requirement": "recommended" 18 | }, 19 | "type": { 20 | "caption": "Type", 21 | "description": "The endpoint type. For example: unknown, server, desktop, laptop, tablet, mobile, virtual, browser, or other.", 22 | "requirement": "optional", 23 | "observable": 201 24 | }, 25 | "type_id": { 26 | "caption": "Type ID", 27 | "description": "The endpoint type ID.", 28 | "enum": { 29 | "1": { 30 | "caption": "Server", 31 | "description": "A server." 32 | }, 33 | "2": { 34 | "caption": "Desktop", 35 | "description": "A desktop computer." 36 | }, 37 | "3": { 38 | "caption": "Laptop", 39 | "description": "A laptop computer." 40 | }, 41 | "4": { 42 | "caption": "Tablet", 43 | "description": "A tablet computer." 44 | }, 45 | "5": { 46 | "caption": "Mobile", 47 | "description": "A mobile phone." 48 | }, 49 | "6": { 50 | "caption": "Virtual", 51 | "description": "A virtual machine." 52 | }, 53 | "7": { 54 | "caption": "IOT", 55 | "description": "A IOT (Internet of Things) device." 56 | }, 57 | "8": { 58 | "caption": "Browser", 59 | "description": "A web browser." 60 | }, 61 | "9": { 62 | "caption": "Firewall", 63 | "description": "A networking firewall." 64 | }, 65 | "10": { 66 | "caption": "Switch", 67 | "description": "A networking switch." 68 | }, 69 | "11": { 70 | "caption": "Hub", 71 | "description": "A networking hub." 72 | }, 73 | "12": { 74 | "caption": "Bogart", 75 | "description": "A botched thing.", 76 | "references": [{"url": "https://example.com/bogart", "description": "Bogart on example.com"}], 77 | "@deprecated": { 78 | "message": "Use 13 (Bogus) instead.", 79 | "since": "0.1.0-test" 80 | } 81 | }, 82 | "13": { 83 | "caption": "Bogus", 84 | "description": "Like, it's no good.", 85 | "source": "E_BOTCHED from RickNix", 86 | "references": [{"url": "https://example.com/bogus", "description": "Bogus on example.com"}] 87 | } 88 | }, 89 | "requirement": "recommended", 90 | "observable": 202 91 | }, 92 | "uid": { 93 | "description": "The unique identifier of the endpoint.", 94 | "requirement": "recommended" 95 | } 96 | }, 97 | "constraints": { 98 | "at_least_one": [ 99 | "ip", 100 | "uid", 101 | "name", 102 | "hostname" 103 | ] 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/objects/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "Metadata", 3 | "description": "The Metadata object describes the metadata associated with the event. Defined by D3FEND d3f:Metadata.", 4 | "name": "metadata", 5 | "attributes": { 6 | "correlation_uid": { 7 | "requirement": "optional" 8 | }, 9 | "uid": { 10 | "caption": "Event UID", 11 | "description": "The logging system-assigned unique identifier of an event instance.", 12 | "requirement": "optional" 13 | }, 14 | "version": { 15 | "description": "The version of the OCSF schema, using Semantic Versioning Specification (SemVer). For example: 1.0.0. Event consumers use the version to determine the available event attributes.", 16 | "requirement": "required" 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /test/test_ocsf_schema/objects/network_endpoint.json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "Network Endpoint", 3 | "description": "The Network Endpoint object describes characteristics of a network endpoint. These can be a source or destination of a network connection.", 4 | "extends": "endpoint", 5 | "name": "network_endpoint", 6 | "attributes": { 7 | "port": { 8 | "requirement": "recommended" 9 | }, 10 | "service": { 11 | "requirement": "recommended" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/objects/network_node.json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "Network Node", 3 | "description": "The Network Node object represents a computing device on a network. This is simplified view; the actual OCSF Schema uses far more elaborate model.", 4 | "name": "network_node", 5 | "extends": "_entity", 6 | "observable": 203, 7 | "attributes": { 8 | "ip": { 9 | "requirement": "required" 10 | }, 11 | "hostname": { 12 | "requirement": "recommended" 13 | }, 14 | "ob_by_dict_type_1": { 15 | "requirement": "optional", 16 | "description": "Example 1 observable by dictionary type." 17 | }, 18 | "ob_by_dict_type_2": { 19 | "requirement": "optional", 20 | "description": "Example 2 observable by dictionary type with object attribute observable.", 21 | "observable": 20000 22 | }, 23 | "ob_by_dict_attr_1": { 24 | "requirement": "optional", 25 | "description": "Example 1 observable by dictionary attribute." 26 | }, 27 | "ob_by_dict_attr_2": { 28 | "requirement": "optional", 29 | "description": "Example 2 observable by dictionary attribute with object attribute observable.", 30 | "observable": 20001 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /test/test_ocsf_schema/objects/observable.json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "Observable", 3 | "description": "The observable object is a pivot element that contains related information found in many places in the event.", 4 | "name": "observable", 5 | "attributes": { 6 | "name": { 7 | "description": "The full name of the observable attribute. The name is a pointer/reference to an attribute within the event data. For example: file.name.", 8 | "requirement": "required" 9 | }, 10 | "type": { 11 | "description": "The observable value type name.", 12 | "requirement": "optional" 13 | }, 14 | "type_id": { 15 | "description": "The observable value type identifier.", 16 | "requirement": "required", 17 | "enum": { 18 | "0": { 19 | "caption": "Unknown", 20 | "description": "Unknown observable data type." 21 | }, 22 | "99": { 23 | "caption": "Other", 24 | "description": "The observable data type is not mapped. See the type attribute, which may contain data source specific value." 25 | } 26 | } 27 | }, 28 | "value": { 29 | "description": "The value associated with the observable attribute. The meaning of the value depends on the observable type.
If the name refers to a scalar attribute, then the value is the value of the attribute.
If the name refers to an object attribute, then the value is not populated.", 30 | "requirement": "optional" 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /test/test_ocsf_schema/objects/service.json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "Service", 3 | "description": "A service.", 4 | "name": "service", 5 | "attributes": { 6 | "name": { 7 | "description": "The service's human-friendly name", 8 | "requirement": "recommended" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/objects/specific_device.json: -------------------------------------------------------------------------------- 1 | { 2 | "caption": "Specific Device", 3 | "description": "A object based on Device that exists simply to be a grandchild of Endpoint.", 4 | "extends": "device", 5 | "name": "specific_device", 6 | "attributes": { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/objects/zeta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zeta", 3 | "extends": "_zeta_base", 4 | "observable": 204, 5 | "attributes": { 6 | "name": { 7 | "description": "The zeta's human-friendly name", 8 | "requirement": "recommended" 9 | }, 10 | "numeric_value": { 11 | "requirement": "required", 12 | "observable": 205 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/test_ocsf_schema/profiles/datetime.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "This profile defines date/time attributes as defined in RFC-3339. For example 1985-04-12T23:20:50.52Z.", 3 | "meta": "profile", 4 | "caption": "Date/Time", 5 | "name": "datetime", 6 | "attributes": {} 7 | } -------------------------------------------------------------------------------- /test/test_ocsf_schema/profiles/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "The attributes that identify host/device attributes.", 3 | "meta": "profile", 4 | "caption": "Host", 5 | "name": "host", 6 | "annotations": { 7 | "group": "primary" 8 | }, 9 | "attributes": { 10 | "device": { 11 | "requirement": "recommended", 12 | "references": [ 13 | { 14 | "url": "https://example.com/device?profile=host", 15 | "description": "Device from host profile on example.com " 16 | } 17 | ] 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /test/test_ocsf_schema/version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.1-test" 3 | } 4 | 5 | --------------------------------------------------------------------------------