├── test ├── test_helper.exs └── osq_simulator_test.exs ├── .gitignore ├── start_servers ├── config.yaml.example ├── mix.lock ├── config └── config.exs ├── mix.exs ├── README.md └── lib └── endpoint.ex /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | config.yaml 6 | -------------------------------------------------------------------------------- /start_servers: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kf0jvt/osq_simulator/HEAD/start_servers -------------------------------------------------------------------------------- /test/osq_simulator_test.exs: -------------------------------------------------------------------------------- 1 | defmodule OsqSimulatorTest do 2 | use ExUnit.Case 3 | 4 | test "the truth" do 5 | assert 1 + 1 == 2 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /config.yaml.example: -------------------------------------------------------------------------------- 1 | base_url: http://localhost:4567 2 | enroll_secret: secret 3 | debug: true 4 | sleep: 500 5 | groups: 6 | default: 100 7 | test1: 100 8 | test2: 100 9 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"httpotion": {:hex, :httpotion, "2.1.0"}, 2 | "ibrowse": {:git, "git://github.com/cmullaparthi/ibrowse.git", "ea3305d21f37eced4fac290f64b068e56df7de80", [tag: "v4.1.2"]}, 3 | "json": {:hex, :json, "0.3.2"}, 4 | "yamerl": {:git, "https://github.com/yakaz/yamerl.git", "ae810a808817d9482b4628ae3e20d746e3729fe0", []}} 5 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for third- 9 | # party users, it should be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :logger, :console, 14 | # level: :info, 15 | # format: "$date $time [$level] $metadata$message\n", 16 | # metadata: [:user_id] 17 | 18 | # It is also possible to import configuration files, relative to this 19 | # directory. For example, you can emulate configuration per environment 20 | # by uncommenting the line below and defining dev.exs, test.exs and such. 21 | # Configuration from the imported file will override the ones defined 22 | # here (which is why it is important to import them last). 23 | # 24 | # import_config "#{Mix.env}.exs" 25 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule OsqSimulator.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :osq_simulator, 6 | version: "0.0.1", 7 | elixir: "~> 1.0", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps, 11 | escript: escript] 12 | end 13 | 14 | # Configuration for the OTP application 15 | # 16 | # Type `mix help compile.app` for more information 17 | def application do 18 | [applications: [:logger, :httpotion, :yamerl]] 19 | end 20 | 21 | # Dependencies can be Hex packages: 22 | # 23 | # {:mydep, "~> 0.3.0"} 24 | # 25 | # Or git/path repositories: 26 | # 27 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 28 | # 29 | # Type `mix help deps` for more examples and options 30 | defp deps do 31 | [ 32 | {:ibrowse, github: "cmullaparthi/ibrowse", tag: "v4.1.2"}, 33 | {:httpotion, "~> 2.1.0"}, 34 | {:json, "~> 0.3.0"}, 35 | {:yamerl, github: "yakaz/yamerl"} 36 | ] 37 | end 38 | 39 | def escript do 40 | [main_module: Endpoint, 41 | name: "start_servers"] 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OSQuery Endpoint Simulator 2 | 3 | If you're testing some software that will provide configurations to osquery 4 | endpoints but you don't want to spin up hundres of virtual machines, you can 5 | use this code (with some modifications) to simulate as many endpoints as you 6 | want. They generate the traffic that an endpoint would generate and you can 7 | see how your server is responding. 8 | 9 | ## Set up 10 | 11 | 1. Clone the repo 12 | 2. Make sure you have erlang installed 13 | 3. Copy `config.yaml.example` to `config.yaml` 14 | 4. Edit `config.yaml` to taste 15 | 5. run `./start_servers` 16 | 17 | ## Configuration 18 | 19 | The file `config.yaml` will set the base url that osq_simulator will try to 20 | contact to enroll the fake servers. This should be the dns address of the server 21 | with a protocol on the front, such as `http://localhost:4567`. 22 | 23 | Osquery servers will normally try to enroll using an enroll secret value which 24 | is also set in `config.yaml`. 25 | 26 | If you're using this with a server like [windmill](https://github.com/heroku/windmill) 27 | which takes the id and group with the enroll secret then in your `config.yaml` file 28 | make sure to set `send_id` and `send_group` to true. 29 | 30 | Finally, in the groups variable, set a group name and the number of endpoints 31 | you want from that group. 32 | -------------------------------------------------------------------------------- /lib/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule Endpoint do 2 | def start(group_name) do 3 | # Enroll the Endpoint 4 | final_secret = "" 5 | if Application.get_env(:osq_simulator, :send_id, false) == true do 6 | final_secret = final_secret <> "#{inspect(self())}:" 7 | end 8 | if Application.get_env(:osq_simulator, :send_group, false) == true do 9 | final_secret = final_secret <> "#{group_name}:" 10 | end 11 | final_secret = final_secret <> to_string(Application.get_env(:osq_simulator, :enroll_secret)) 12 | 13 | logdebug "Secret being sent to server is: #{final_secret}" 14 | 15 | try do 16 | response = HTTPotion.post "#{Application.get_env(:osq_simulator, :base_url)}/api/enroll", 17 | [body: "{\"enroll_secret\": \"#{final_secret}\"}", 18 | headers: ["User-Agent": "Elixir", "Content-Type": "application/json"]] 19 | 20 | logdebug(response.body) 21 | 22 | case JSON.decode(response.body) do 23 | {:ok, json} -> 24 | case json do 25 | %{"node_invalid" => _} -> 26 | raise "invalid node_key. Does NODE_ENROLL_SECRET match what the server is using?" 27 | %{"node_key" => node_key} -> 28 | spawn(fn -> loop(node_key) end) 29 | _ -> 30 | raise "Response from server was not node_invalid or node_key" 31 | end 32 | _ -> 33 | raise "Unable to JSON.decode the response from the server" 34 | end 35 | 36 | rescue 37 | e in HTTPotion.HTTPError -> logdebug "Error enrolling #{group_name} at #{Application.get_env(:osq_simulator, :base_url)}/api/enroll: #{e.message}" 38 | e in RuntimeError -> logdebug "Error enrolling at #{Application.get_env(:osq_simulator, :base_url)}/api/enroll: #{e.message}" 39 | _error -> logdebug "Got a generic error while enrolling at #{Application.get_env(:osq_simulator, :base_url)}/api/enroll" 40 | end 41 | 42 | end 43 | 44 | defp skew10(number) do 45 | low = number * 0.9 |> round 46 | high = number * 1.1 |> round 47 | range = high - low |> round 48 | low + :rand.uniform(range) 49 | end 50 | 51 | defp logdebug(message) do 52 | if Application.get_env(:osq_simulator, :debug, false) == true do 53 | IO.puts message 54 | end 55 | end 56 | 57 | defp msec_to_min(number) do 58 | number / 60 / 1000 |> round 59 | end 60 | 61 | defp loop(node_key) do 62 | logdebug "process #{inspect(self())} requesting a new config with node key #{node_key}" 63 | try do 64 | response = HTTPotion.post "#{Application.get_env(:osq_simulator, :base_url)}/api/config", 65 | [body: "{\"node_key\": \"#{node_key}\"}", 66 | headers: ["User-Agent": "Elixir", "Content-Type": "application/json"]] 67 | 68 | case JSON.decode(response.body) do 69 | {:ok, json} -> 70 | sleep_for = skew10(1000 * 60 * 60) 71 | logdebug "process #{inspect(self())} sleeping for #{round(sleep_for / 60)} seconds. (#{msec_to_min(sleep_for)} minutes)" 72 | :timer.sleep(sleep_for) # Check for updates every hour 73 | _ -> 74 | raise "Unable to JSON.decode the response from the server" 75 | end 76 | 77 | rescue 78 | e in HTTPotion.HTTPError -> logdebug("Error getting config from #{Application.get_env(:osq_simulator, :base_url)}/api/config: #{e.message}"); :timer.sleep(:rand.uniform(120*1000)) 79 | _error -> logdebug("Got a generic while trying to get config update from #{Application.get_env(:osq_simulator, :base_url)}/api/enroll"); :timer.sleep(:rand.uniform(120*1000)) 80 | end 81 | 82 | loop(node_key) # error or not, we will try again after sleeping for some amount of time 83 | end 84 | 85 | def launcher(name, size) do 86 | 1..size |> Enum.map(fn(_) -> spawn(fn -> Endpoint.start(name) end); :timer.sleep(Application.get_env(:osq_simulator, :sleep_time, 500)) end ) 87 | end 88 | 89 | def main(_) do 90 | try do 91 | [ config | _ ] = :yamerl_constr.file("config.yaml") 92 | Application.put_env(:osq_simulator, :base_url, :proplists.get_value('base_url', config)) 93 | Application.put_env(:osq_simulator, :enroll_secret, :proplists.get_value('enroll_secret', config)) 94 | Application.put_env(:osq_simulator, :debug, :proplists.get_value('debug', config)) 95 | Application.put_env(:osq_simulator, :groups, :proplists.get_value('groups', config)) 96 | Application.put_env(:osq_simulator, :send_id, :proplists.get_value('send_id', config)) 97 | Application.put_env(:osq_simulator, :send_group, :proplists.get_value('send_group', config)) 98 | 99 | if Application.get_env(:osq_simulator, :debug) != true do 100 | IO.puts "Debugging mode not set. Activate by setting debug to true in config/config.exs." 101 | end 102 | 103 | if Application.get_env(:osq_simulator, :enroll_secret, "unset") == "unset" do 104 | raise "You don't have anything set in config/config.exs for enroll_secret" 105 | end 106 | 107 | Application.get_env(:osq_simulator, :groups) |> Enum.each(fn(x) -> {name, size} = x; Endpoint.launcher(name, size); end) 108 | 109 | receive do 110 | {:quit} -> IO.puts "quitting" 111 | end 112 | catch 113 | {_, longerr} -> [inner] = longerr; elem(inner, 2) |> to_string |> raise 114 | end 115 | end 116 | end 117 | --------------------------------------------------------------------------------