cased-elixir

A Cased client for Elixir applications in your organization to control and monitor the access of information within your organization.

Overview

Installation

The package can be installed by adding cased to your list of dependencies in mix.exs:

def deps do
[
{:cased, "~> 0.1.0"}
]
end

Configuration

cased-elixir follows Elixir's Library Guidelines, avoiding the use of a global :cased application configuration in favor of more flexible, ad hoc configuration at runtime (using your own application configuration, environment variables, etc).

For Publisher

Add a worker specification for Cased.Publisher.HTTP to your application's supervisor.

The publisher accepts the following options:

You can source your configuration values from your application configuration, runtime environment variables, or hard-code them directly; the following is just an example:

children = [
# Other workers...
{
Cased.Publisher.HTTP,
key: System.get_env("CASED_PUBLISH_KEY") || Application.fetch_env!(:your_app, :cased_publish_key),
silence: System.get_env("CASED_SILENCE") || Application.fetch_env!(:your_app, :cased_silence, false),
}
]
# Other config...
Supervisor.start_link(children, opts)

In the event you provide an invalid configuration, a Cased.ConfigurationError will be raised with details.

For Client

To access Cased API routes and functionality other than publishing, configure a client using Cased.Client.create/1:

The function accepts the following options:

These configuration values can be provided a number of ways, as shown in the examples below.

Examples

Create a client with the policy key for your :default audit trail:

iex> Cased.Client.create(key: "policy_live_...")
{:ok, %Cased.Client{...}}

Create a client key with policy keys for specific audit trails:

iex> Cased.Client.create(keys: [default: "policy_live_...", users: "policy_live_..."])
{:ok, %Cased.Client{...}}

Clients can be configured using runtime environment variables, your application configuration, hardcoded values, or any combination you choose:

# Just using runtime environment variable:
iex> Cased.Client.create(key: System.fetch_env!("CASED_POLICY_KEY"))
{:ok, %Cased.Client{...}}
# Just using application configuration:
iex> Cased.Client.create(key: Application.fetch_env!(:your_app, :cased_policy_key))
{:ok, %Cased.Client{...}}
# Either/or
iex> Cased.Client.create(
...> key: System.get_env("CASED_POLICY_KEY") || Application.fetch_env!(:your_app, :cased_policy_key)
...> )
{:ok, %Cased.Client{...}}

In the event your client is misconfigured, you'll get a Cased.ConfigurationError exception struct instead:

iex> Cased.Client.create()
{:error, %Cased.ConfigurationError{...}}

You can also use Cased.Client.create!/1 if you know you're passing the correct configuration options (otherwise it raises a Cased.ConfigurationError exception):

iex> Cased.Client.create!(key: "policy_live_...")
%Cased.Client{...}

To simplify using clients across your application, consider writing a centralized function to handle constructing them:

defmodule YourApp do
# Rest of contents ...
def cased_client do
default_policy_key = System.get_env("CASED_POLICY_KEY") || Application.fetch_env!(:your_app, :cased_policy_key)
Cased.Client.create!(key: default_policy_key)
end
end

For reuse, consider caching your client structs in GenServer state, ETS, or another Elixir caching mechanism.

Usage

Publishing events to Cased

Provided you've configured the Cased publisher, use Cased.publish/1:

iex> %{
...> action: "credit_card.charge",
...> amount: 2000,
...> currency: "usd",
...> source: "tok_amex",
...> description: "My First Test Charge (created for API docs)",
...> credit_card_id: "card_1dQpXqQwXxsQs9sohN9HrzRAV6y"
...> }
...> |> Cased.publish()
:ok

ℹ️ See the documentation for Cased.publish/2 for more options.

Retrieving events from a Cased Policy

If you plan on retrieving events from your audit trails you must use an Cased Policy token.

iex> {:ok, client} = Cased.Client.create(key: "policy_live_1dQpY5JliYgHSkEntAbMVzuOROh")
iex> events =
...> client
...> |> Cased.Event.query()
...> |> Cased.Request.stream()
iex> events
...> |> Enum.take(3)
[%Cased.Event{...}, %Cased.Event{...}, %Cased.Event{...}]

You can also retrieve an event by ID and audit trail:

iex> event =
...> client
...> |> Cased.Event.get("event_...", audit_trail: :organizations)
...> |> Cased.Request.run!()
%Cased.Event{...}

Retrieving events from a Cased Policy containing variables

Cased policies allow you to filter events by providing variables to your Cased Policy events query. One example of a Cased Policy is to have a single Cased Policy that you can use to query events for any user in your database without having to create a Cased Policy for each user.

For example, getting the first 3 events in the default audit trail that matches a specific user ID:

iex> client = Cased.Client.create!(key: "policy_live_...")
%Cased.Client{...}
iex> variables = [user_id: "user_..."]
iex> events =
...> client
...> |> Cased.Event.query(variables: variables)
...> |> Cased.Request.stream()
...> |> Enum.take(3)
[%Cased.Event{...}, %Cased.Event{...}, %Cased.Event{...}]

Retrieving events from multiple Cased Policies

To retrieve events from one or more Cased Policies you can configure multiple Cased Policy API keys and retrieve events for each one.

For example, getting the first 3 events for the user and organization audit trails:

iex> client = Cased.Client.create!(keys: [
...> users: "policy_live_1dQpY8bBgEwdpmdpVrrtDzMX4fH",
...> organizations: "policy_live_1dSHQRurWX8JMYMbkRdfzVoo62d"
...> ])
%Cased.Client{...}
iex> users_events =
...> client
...> |> Cased.Event.query(audit_trail: :users, variables: variables)
...> |> Cased.Request.stream()
...> |> Enum.take(3)
[%Cased.Event{...}, %Cased.Event{...}, %Cased.Event{...}]
iex> org_events =
...> client
...> |> Cased.Event.query(audit_trail: :organizations, variables: variables)
...> |> Cased.Request.stream()
...> |> Enum.take(3)
[%Cased.Event{...}, %Cased.Event{...}, %Cased.Event{...}]

Exporting events

Exporting events from a Cased Policy allows you to provide users with exports of their own data or to respond to data requests.

iex> export =
...> client
...> |> Cased.Export.create(audit_trails: [:organizations, :users], fields: ~w(action timestamp))
...> |> Cased.Request.run()
%Cased.Export{download_url: "https://api.cased.com/exports/export_...", ...}

The following options are available:

The only required option is :fields.

Retrieving an Export

You can retrieve data about an export using Cased.Export.get/2 (or Cased.Export.get/3):

iex> client
...> |> Cased.Export.get("export_...")
...> |> Cased.Request.run!
%Cased.Export{id: "export_...", ...}

To download the JSON data for an export, use Cased.Export.get_download/2 (or Cased.Export.get_download/3).

iex> client
...> |> Cased.Export.get_download("export_...")
...> |> Cased.Request.run!
"{...}"

(Note that the raw JSON string is returned; no automatic decoding is attempted.)

For more information, see the documentation for Cased.Export.get/3 and Cased.Export.get_download/3.

Masking & filtering sensitive information

If you are handling sensitive information on behalf of your users, you should consider masking or filtering any sensitive information.

You can do this manually by using Cased.Sensitive.String.new/2:

iex> %{
...> action: "credit_card.charge",
...> user: Cased.Sensitive.String.new("john@example.com", label: :email)
...> }
...> |> Cased.publish()
:ok

You can also use handlers to find sensitive values for you automatically. Here's an example checking for usernames:

iex> username_handler = {Cased.Sensitive.RegexHandler, :username, ~r<@\w+>}
iex> %{
...> action: "comment.create",
...> body: "@username, I'm not sure."
...> }
...> |> Cased.publish(handlers: [username_handler])
:ok

If you're regularly using the same handlers, consider storing them in your application config and defining your own function to use them in your application:

defmodule MyApp do
@doc """
Publish an audit event to Cased.
"""
@spec publish_to_cased(audit_event :: map()) :: :ok | {:error, any()}
def publish_to_cased(audit_event) do
handlers = Application.get_env(:my_app, :cased_handlers, [])
audit_event
|> Cased.publish(handlers: handlers)
end
end

For more information, see the Cased.Sensitive.Handler module.

Retrieving and policy information

To retrieve and modify policy information, you need to create your client with your Environment API Key.

For example:

iex> Cased.Client.create(
...> key: "policy_live_...",
...> environment_key: "environment_live_..."
...> )
{:ok, %Cased.Client{...}}

Use Cased.Policy.query/1 and Cased.Request.stream/1 in conjunction with one of the Enum functions to retrieve policies:

iex> first_3_policies =
...> client
...> |> Cased.Policy.query()
...> |> Cased.Request.stream()
...> |> Enum.take(3)
[%Cased.Policy{...}, %Cased.Policy{...}, %Cased.Policy{...}]

Creating policies

You can create a policy using Cased.Policy.create/2. You must provide a :name, :description, and at least one other option (see Cased.Policy.create/2 for details):

iex> policy =
...> client
...> |> Cased.Policy.create(
...> name: "limited",
...> description: "A limited time policy",
...> export: true,
...> pii: false,
...> window: [gte: begin_datetime, lte: end_datetime]
...> )
...> |> Cased.Request.run!()
%Cased.Policy{...}

Updating policies

You can update a policy by ID using Cased.Policy.update/3:

iex> client
...> |> Cased.Policy.update("THE-POLICY-ID", name: "unlimited", pii: true)
...> |> Cased.Request.run!()
%Cased.Policy{...}

Deleting Policies

You can delete a policy by ID using Cased.Policy.delete/2:

iex> client
...> |> Cased.Policy.delete("THE-POLICY-ID")
...> |> Cased.Request.run!()
:ok

Context

One of the most easiest ways to publish detailed events to Cased is to push contextual information into the Cased context.

Note that the Cased context is tied to the current process (it's actually stored in the process dictionary). Different process, different context.

iex> Cased.Context.merge(location: "hostname.local")
:ok
iex> %{
...> action: "console.start",
...> user: "john"
...> }
...> |> Cased.publish()
:ok

Any information stored using Cased.Context will be included any time an event is published.

{
"cased_id": "5f8559cd-4cd9-48c3-b1d0-6eedc4019ec1",
"action": "user.login",
"user": "john",
"location": "hostname.local",
"timestamp": "2020-06-22T21:43:06.157336"
}

You can provide Cased.Context.merge/2 a function and the context will only be present for the duration of the function execution:

iex> Cased.Context.merge(location: "hostname.local") do
...> # Will include { "location": "hostname.local" }
...> %{
...> action: "console.start",
...> user: "john"
...> }
...> |> Cased.publish()
...> end
:ok
iex> # Will not include {"location": "hostname.local"}
iex> %{
...> action: "console.end",
...> user: "john"
...> }
...> |> Cased.publish()
:ok

(You can also use Cased.Context.put/2 and Cased.Context.put/3 for single-value additions to the context.)

To reset the context, use Cased.Context.reset/0:

iex> Cased.Context.reset()
:ok # or `nil` if no data was stored in the context

See the Cased.Context module for more information.

Testing

See Cased.TestHelper.

Contributing

  1. Fork it ( https://github.com/cased/cased-elixir/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request