@cccsaurora/howler-ui 2.12.0-dev.38 → 2.12.0-dev.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ # Using Actions in Howler
2
+
3
+ Actions are a feature in Howler that allow users to perform particular tasks on a large number of hits, through automating the execution of a task on each hit. There are currently `action_count` operations supported in howler:
4
+
5
+ `action_list`
6
+
7
+ All of these operations can be combined together into unique actions - that is, operations are essentially the building blocks of actions in Howler. Each operation can only appear once per action, and all operations are configured through one unified UI. In this document, we will walk through the steps necessary to run and save an action.
8
+
9
+ ## Configuring an Action
10
+
11
+ In order to begin configuring your action, decide if this will be a one-off action or a saved action you want to run several times. If you want to run it once, use the `t(route.actions.change)` entry in the sidebar, while a saved action is best configured under the `t(route.actions.manager)` by pressing "`t(route.actions.create)`".
12
+
13
+ The first step of any action will be to design a query on which you want this action to run. The search box at the top of the accepts any lucene query - the same format as searching for hits.
14
+
15
+ `tui_phrase`
16
+
17
+ Once you are satisfied with the hits that will be included in this query, you can begin adding operations. You can do so by selecting the operation you want to add from the dropdown:
18
+
19
+ `operation_select`
20
+
21
+ Once you've selected the operation you want to add, it will prompt you for a list of parameters you will need to fill out. Below is an example for adding a label.
22
+
23
+ `operation_configuration`
24
+
25
+ Once the operation validates successfully, you can repeat this process with the next operation. Once you've added all the operations you're interested in, you can execute or save the action using the button below the search bar. This will generate a report of the steps taken.
26
+
27
+ `report`
28
+
29
+ Occaisionally, actions will result in an error, either on validation or on execution. In these cases, an error alert will be shown, helping you solve the issue.
30
+
31
+ ## Automating an Action
32
+
33
+ In order to automate an action, open any saved action. The options available to you for automation (`automation_options`) will show up as checkboxes. Checking the box will ensure this action will run then - no further work required.
@@ -0,0 +1,261 @@
1
+ <!-- docs/ingestion/authentication.md -->
2
+
3
+ # Howler Authentication
4
+
5
+ Howler's API supports a number of authentication approaches when accessing the API. This document will outline the
6
+ different approaches, and explain how to use each of them.
7
+
8
+ ## Types of Authentication
9
+
10
+ There are four methods of authentication one can use to connect to the howler API:
11
+
12
+ 1. Username and Password
13
+ 2. Username and API Key
14
+ 3. Username and app token (after login)
15
+ 4. OAuth Access Token
16
+
17
+ We will outline the use case for each authentication type next.
18
+
19
+ ### Username/Password Authentication
20
+
21
+ Username and password authentication is the simplest and least secure method of authentication. Unlikely to be enabled
22
+ in a production environment, it allows users to easily connect to the Howler API and make changes as the given user,
23
+ without having to worry about creating API keys or using an OAuth provider like Keycloak or Azure. Users can connect
24
+ using username and password authentication in one of two ways:
25
+
26
+ #### Direct Call to the Requisite Endpoint (Password)
27
+
28
+ This is far and away the simplest method. Simply add a basic Authorization header to the HTTP request you want to
29
+ make, and everything is taken care of:
30
+
31
+ ```bash
32
+ echo -n "user:user" | base64 -w0
33
+ # -> dXNlcjp1c2Vy
34
+ ```
35
+
36
+ ```http
37
+ GET $CURRENT_URL/api/v1/user/whoami
38
+ Authorization: Basic dXNlcjp1c2Vy
39
+ ```
40
+
41
+ #### Exchanging for an App Token (Password)
42
+
43
+ This is a slightly more complex approach, but carries the benefit of not exposing the username and password on every
44
+ request. You can leverage the `v1/auth/login` endpoint to exchange your username and password for an app token. The
45
+ token works similarly to an OAuth access token - you provide it with each subsequent request, and it authenticates you
46
+ until the token expires.
47
+
48
+ ```http
49
+ POST $CURRENT_URL/api/v1/auth/login/
50
+ Content-Type: application/json
51
+
52
+ {
53
+ "user": "user",
54
+ "password": "user"
55
+ }
56
+ ```
57
+
58
+ Will return something like:
59
+
60
+ ```json
61
+ {
62
+ "api_error_message": "",
63
+ "api_response": {
64
+ "app_token": "user:5791a142067745c3af51d6596da7da8f86357a9fa92ad78d1ce118ea7d89d34e",
65
+ "provider": null,
66
+ "refresh_token": null
67
+ },
68
+ "api_server_version": "0.0.0.dev0",
69
+ "api_status_code": 200
70
+ }
71
+ ```
72
+
73
+ Using this token in another API call:
74
+
75
+ ```http
76
+ GET $CURRENT_URL/api/v1/user/whoami
77
+ Authorization: Bearer user:5791a142067745c3af51d6596da7da8f86357a9fa92ad78d1ce118ea7d89d34e
78
+ ```
79
+
80
+ ### Username/API Key Authentication
81
+
82
+ The username and API Key authentication works largely the same as username/password from the perspective of the client.
83
+ On the server side, however, API keys have several key advantages. Firstly, they can easily be revoked by the user.
84
+ Secondly, their privileges can be limited, allowing only a subset of permissions.
85
+
86
+ There are two methods of authentication, mirroring the username and password:
87
+
88
+ #### Direct Call to the Requisite Endpoint (API Key)
89
+
90
+ Simply add a basic Authorization header containing the username and API key to the HTTP request you want to make, and
91
+ everything is taken care of:
92
+
93
+ ```bash
94
+ # note the format is <username>:<apikeyname>:<secret>
95
+ echo -n "user:devkey:user" | base64 -w0
96
+ # -> dXNlcjpkZXZrZXk6dXNlcg==
97
+ ```
98
+
99
+ ```http
100
+ GET $CURRENT_URL/api/v1/user/whoami
101
+ Authorization: Basic dXNlcjpkZXZrZXk6dXNlcg==
102
+ ```
103
+
104
+ #### Exchanging for an App Token (API Key)
105
+
106
+ You can also leverage the `v1/auth/login` endpoint to exchange your username and API key for an app token. The
107
+ token works similarly to an OAuth access token - you provide it with each subsequent request, and it authenticates you
108
+ until the token expires.
109
+
110
+ ```http
111
+ POST $CURRENT_URL/api/v1/auth/login/
112
+ Content-Type: application/json
113
+
114
+ {
115
+ "user": "user",
116
+ "apikey": "devkey:user"
117
+ }
118
+ ```
119
+
120
+ Will return something like:
121
+
122
+ ```json
123
+ {
124
+ "api_error_message": "",
125
+ "api_response": {
126
+ "app_token": "user:f220eb76ff8404abfece8c0c2f3368c7d89618c776bedcd3a506843dc4a952e4",
127
+ "provider": null,
128
+ "refresh_token": null
129
+ },
130
+ "api_server_version": "0.0.0.dev0",
131
+ "api_status_code": 200
132
+ }
133
+ ```
134
+
135
+ Using this token in another API call:
136
+
137
+ ```http
138
+ GET $CURRENT_URL/api/v1/user/whoami
139
+ Authorization: Bearer user:f220eb76ff8404abfece8c0c2f3368c7d89618c776bedcd3a506843dc4a952e4
140
+ ```
141
+
142
+ Note that when logging in using an api key, the permissions of the api key continue to apply. So if you try and access
143
+ an endpoint that requires the "write" permission with an API key that only has read permission, this will cause a 403
144
+ Forbidden error.
145
+
146
+ ### Access Token Authentication
147
+
148
+ In addition to the authentication methods provided internally by Howler, you can also authenticate with an external
149
+ OAuth provider like Azure or Keycloak. To do this, you must obtain an access token from one of these providers. In
150
+ order to get an access token, there are generally two flows - On-Behalf-Of, or the user login flow (see [here](https://www.rfc-editor.org/rfc/rfc6749#section-4.1) for a rundown
151
+ of that). In the case of Howler, the `v1/auth/login` endpoint is used for step D & E in the linked user login flow, and
152
+ it returns the following:
153
+
154
+ ```json
155
+ {
156
+ "api_response": {
157
+ "app_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyIn0",
158
+ "provider": "keycloak",
159
+ "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJyZWZyZXNoIn0"
160
+ }
161
+ }
162
+ ```
163
+
164
+ In this case, the `app_token` is the JSON Web Token the application uses as an access token. Similarly, the
165
+ `refresh_token` is another JWT that is used to refresh the access token if needed. Finally, the `provider` field
166
+ outlines which provider this access token corresponds to.
167
+
168
+ Once you have that access token, you can simply pass it along in the Authorization header:
169
+
170
+ ```http
171
+ GET $CURRENT_URL/api/v1/user/whoami
172
+ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyIn0
173
+ ```
174
+
175
+ Howler will automatically detect this is a JWT, and treat it as such. If the token is valid, the user will be
176
+ authenticated.
177
+
178
+ In order to refresh an expired access token, you can use the following API call:
179
+
180
+ ```http
181
+ POST $CURRENT_URL/api/v1/auth/login/
182
+ Content-Type: application/json
183
+
184
+ {
185
+ "oauth_provider": "keycloak",
186
+ "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJyZWZyZXNoIn0"
187
+ }
188
+ ```
189
+
190
+ And you'll get something like this back:
191
+
192
+ ```json
193
+ {
194
+ "api_response": {
195
+ "app_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMiJ9",
196
+ "provider": "keycloak",
197
+ "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJyZWZyZXNodG9rZW4yIn0"
198
+ }
199
+ }
200
+ ```
201
+
202
+ ## Impersonation
203
+
204
+ The final important bit of the authentication flow is the impersonation functionality. In order for a service account
205
+ or other user to impersonate you (i.e. to create alerts on your behalf), Howler allows users to provide a second API key
206
+ in order to authenticate as another user.
207
+
208
+ Before you use another user's API key to authenticate as them, it's important that the API key you're using has been
209
+ marked as valid for impersonation - check with the user that provided it. If it has, you can use it by, formating your
210
+ request as follows:
211
+
212
+ ```bash
213
+ # Your credentials
214
+ echo -n "admin:devkey:admin" | base64 -w0
215
+ # -> YWRtaW46ZGV2a2V5OmFkbWlu
216
+
217
+ # Their credentials
218
+ echo -n "user:impersonate_admin:user" | base64 -w0
219
+ # -> dXNlcjppbXBlcnNvbmF0ZV9hZG1pbjp1c2Vy
220
+ ```
221
+
222
+ ```alert
223
+ Any form of authentication can be used by the impersonator, but validated API keys are the only permitted
224
+ authentication method for the person you're impersonating.
225
+ ```
226
+
227
+ ```http
228
+ GET $CURRENT_URL/api/v1/user/whoami
229
+ Authorization: Basic YWRtaW46ZGV2a2V5OmFkbWlu
230
+ X-Impersonating: Basic dXNlcjppbXBlcnNvbmF0ZV9hZG1pbjp1c2Vy
231
+ ```
232
+
233
+ If set up correctly, the result will look like:
234
+
235
+ ```json
236
+ {
237
+ "api_error_message": "",
238
+ "api_response": {
239
+ "avatar": null,
240
+ "classification": "TLP:W",
241
+ "email": "user@howler.cyber.gc.ca",
242
+ "groups": ["USERS"],
243
+ "is_active": true,
244
+ "is_admin": false,
245
+ "name": "User",
246
+ "roles": ["user"],
247
+ "username": "user"
248
+ },
249
+ "api_server_version": "0.0.0.dev0",
250
+ "api_status_code": 200
251
+ }
252
+ ```
253
+
254
+ Note that the server returned this as if you were `user`, NOT `admin`. It is clearly marked in the logs, however, that you are making the request:
255
+
256
+ ```log
257
+ 22/12/05 14:15:02 INFO howler.api.security | Authenticating user for path /api/v1/user/whoami/
258
+ 22/12/05 14:15:03 WARNING howler.api.security | admin is impersonating user
259
+ 22/12/05 14:15:03 INFO howler.api.security | Logged in as user from 127.0.0.1
260
+ 22/12/05 14:15:03 INFO howler.api | GET /api/v1/user/whoami/ - 200
261
+ ```
@@ -0,0 +1,70 @@
1
+ <!-- docs/ingestion/bundles.md -->
2
+
3
+ # Howler Hit Bundles
4
+
5
+ Hit bundles can be used to easily package together a large number of similar alerts, allowing analysts to easily triage them as a single incident. For example, consider a single computer that repeatedly makes a network call to `baddomain.ru` - while an alert may be generated for every instance of this computer hitting that domain, it makes sense for analysts to treat all these alerts as a single case.
6
+
7
+ ## Creating bundles through the Howler Client
8
+
9
+ There are a couple of ways to create a bundle through the howler client:
10
+
11
+ ```python
12
+ from howler_client import get_client
13
+
14
+ howler = get_client("https://howler.dev.analysis.cyber.gc.ca")
15
+
16
+ """Creating a howler bundle and the hits at the same time"""
17
+ howler.bundle.create(
18
+ # First argument is the bundle hit
19
+ {
20
+ "howler.analytic": "example-test",
21
+ "howler.score": 0
22
+ },
23
+ # Second argument is a hit or list of hits to include in the bundle
24
+ [
25
+ {
26
+ "howler.analytic": "example-test",
27
+ "howler.score": 0
28
+ },
29
+ {
30
+ "howler.analytic": "example-test",
31
+ "howler.score": 0
32
+ }
33
+ ]
34
+ )
35
+
36
+ """Creating a howler bundle from existing hits"""
37
+ howler.bundle.create(
38
+ {
39
+ "howler.analytic": "example-test",
40
+ "howler.score": 0,
41
+ "howler.hits": ["YcUsL8QsjmwwIdstieROk", "6s7MztwuSvz6tM0PgGJhvz"]
42
+ },
43
+ # Note: In future releases, you won't need to include this argument
44
+ []
45
+ )
46
+
47
+
48
+ """Creating from a map"""
49
+ bundle_hit = {
50
+ "score": 0,
51
+ "bundle": True
52
+ }
53
+
54
+ map = {
55
+ "score": ["howler.score"],
56
+ "bundle": ["howler.is_bundle"]
57
+ }
58
+
59
+ howler.bundle.create_from_map("example-test", bundle_hit, map, [{"score": 0}])
60
+ ```
61
+
62
+ ## Viewing bundles on the Howler UI
63
+
64
+ In order to view created bundles on the Howler UI, you can use the query `howler.is_bundle:true`. This will provide a list of created bundles you can look through.
65
+
66
+ Clicking on a bundle will open up a slightly different search UI to normal. In this case, we automatically filter the search results to include only hits that are included in the bundle. To make this obvious, the header representing the bundle will appear above the search bar.
67
+
68
+ You can continue to filter through hits using the same queries as usual, and view them as usual. When triaging a bundle, assessing it will apply this assessment to all hits in the bundle, **except those that have already been triaged**. That is, if the bundle is open, all open hits will be assessed when you assess it.
69
+
70
+ Bundles also have a **Summary** tab not available for regular hits. This summary tab will aid you in aggregating data about all the hits in the bundle. Simply open the tab and click "Create Summary". Note that this may take some time, as a large number of queries are being run to aggregate the data.
@@ -0,0 +1,213 @@
1
+ # Howler Client Documentation
2
+
3
+ This documentation will outline how to interact with the howler API using the howler client in both Java and python development environments. We will outline the basic process of creating a new hit in each environment as well as searching howler for hits matching your query.
4
+
5
+ ## Getting started
6
+
7
+ ### Installation
8
+
9
+ In order to use the howler client, you need to list it as a dependency in your project.
10
+
11
+ #### **Python**
12
+
13
+ Simply install through pip:
14
+
15
+ ```bash
16
+ pip install howler-client
17
+ ```
18
+
19
+ You can also add it to your requirements.txt, or whatever dependency management system you use.
20
+
21
+ ### Authentication
22
+
23
+ As outlined in the [Authentication Documentation](/help/auth), there's a number of ways users can choose to authenticate. In order to interface with the howler client, however, the suggested flow is to use an API key. So before we start, let's generate a key.
24
+
25
+ 1. Open the Howler UI you'd like to interface with.
26
+ 2. Log in, then click your profile in the top right.
27
+ 3. Under user menu, click Settings.
28
+ 4. Under User Security, press the (+) icon on the API Keys row.
29
+ 5. Name your key, and give it the requisite permissions.
30
+ 6. Press Create, and copy the supplied string somewhere safe. **You will not see this string again.**
31
+
32
+ This API Key will be supplied to your code later on.
33
+
34
+ ## Python Client
35
+
36
+ In order to connect with howler using the python client, there is a fairly simple process to follow:
37
+
38
+ ```python
39
+ from howler_client import get_client
40
+
41
+ USERNAME = 'user' # Obtain this from the user settings page of the Howler UI
42
+ APIKEY = 'apikey_name:apikey_data'
43
+
44
+ apikey = (USERNAME, APIKEY)
45
+
46
+ howler = get_client("$CURRENT_URL", apikey=apikey)
47
+ ```
48
+
49
+ ```alert
50
+ You can skip generating an API Key and providing it if you're executing this code within HOGWARTS (i.e., on jupyterhub or airflow). OBO will handle authentication for you!
51
+ ```
52
+
53
+ That's it! You can now use the `howler` object to interact with the server. So what does that actually look like?
54
+
55
+ ### Creating hits in Python
56
+
57
+ For the python client, you can create hits using either the `howler.hit.create` or `howler.hit.create_from_map` functions.
58
+
59
+ #### `create`
60
+
61
+ This function takes in a single argument - either a single hit, or a list of them, conforming to the [Howler Schema](/help/hit?tab=schema). Here is a simple example:
62
+
63
+ ```python
64
+ # Some bogus data in the Howler Schema format
65
+ example_hit = {
66
+ "howler": {
67
+ "analytic": "example",
68
+ "score": 10.0
69
+ },
70
+ "event": {
71
+ "reason": "Example hit"
72
+ }
73
+ }
74
+
75
+ howler.hit.create(example_hit)
76
+ ```
77
+
78
+ You can also ingest data in a flat format:
79
+
80
+ ```python
81
+ example_hit = {
82
+ "howler.analytic": "example",
83
+ "howler.score": 10.0,
84
+ "event.reason": "Example hit"
85
+ }
86
+
87
+ howler.hit.create(example_hit)
88
+ ```
89
+
90
+ #### `create_from_map`
91
+
92
+ This function takes in three arguments:
93
+
94
+ - `tool name`: The name of the analytic creating the hit
95
+ - `map`: A mapping between the raw data you have and the howler schema
96
+ - The format is a dictionary where the keys are the flattened path of the raw data, and the values are a list of flattened paths for Howler's fields where the data will be copied into.
97
+ - `documents`: The raw data you want to add to howler
98
+
99
+ Here is a simple example:
100
+
101
+ ```python
102
+ # The mapping from our data to howler's schema
103
+ hwl_map = {
104
+ "file.sha256": ["file.hash.sha256", "howler.hash"],
105
+ "file.name": ["file.name"],
106
+ "src_ip": ["source.ip", "related.ip"],
107
+ "dest_ip": ["destination.ip", "related.ip"],
108
+ "time.created": ["event.start"],
109
+ }
110
+
111
+ # Some bogus data in a custom format we want to add to howler
112
+ example_hit = {
113
+ "src_ip": "0.0.0.0",
114
+ "dest_ip": "8.8.8.8",
115
+ "file": {
116
+ "name": "hello.exe",
117
+ "sha256": sha256(str("hello.exe").encode()).hexdigest()
118
+ },
119
+ "time": {
120
+ "created": datetime.now().isoformat()
121
+ },
122
+ }
123
+
124
+ # Note that the third argument is of type list!
125
+ howler.hit.create_from_map("example_ingestor", hwl_map, [example_hit])
126
+ ```
127
+
128
+ ### Querying Hits
129
+
130
+ Querying hits using the howler python client is done using the `howler.search.hit` function. It has a number of required and optional arguments:
131
+
132
+ - Required:
133
+ - `query`: lucene query (string)
134
+ - Optional:
135
+ - `filters`: Additional lucene queries used to filter the data (list of strings)
136
+ - `fl`: List of fields to return (comma separated string of fields)
137
+ - `offset`: Offset at which the query items should start (integer)
138
+ - `rows`: Number of records to return (integer)
139
+ - `sort`: Field used for sorting with direction (string: ex. 'id desc')
140
+ - `timeout`: Max amount of milliseconds the query will run (integer)
141
+ - `use_archive`: Also query the archive
142
+ - `track_total_hits`: Number of hits to track (default: 10k)
143
+
144
+ Here are some example queries:
145
+
146
+ ```python
147
+ # Search for all hits created by assemblyline, show the first 50, and return only their ids
148
+ howler.search.hit("howler.analytic:assemblyline", fl="howler.id", rows=50)
149
+
150
+ # Search for all resolved hits created in the last five days, returning their id and the analytic that created them. Show only ten, offset by 40
151
+ howler.search.hit("howler.status:resolved", filters=['event.created:[now-5d TO now]'] fl="howler.id,howler.analytic", rows=10, offset=40)
152
+
153
+ # Search for all hits, timeout if the query takes more than 100ms
154
+ howler.search.hit("howler.id:*", track_total_hits=100000000, timeout=100, use_archive=True)
155
+ ```
156
+
157
+ ### Updating Hits
158
+
159
+ In order to update hits, there are a number of supported functions:
160
+
161
+ - `howler.hit.update(...)`
162
+ - `howler.hit.update_by_query(...)`
163
+ - `howler.hit.overwrite(...)`
164
+
165
+ #### `update()`
166
+
167
+ If you want to update a hit in a transactional way, you can use the following code:
168
+
169
+ ```python
170
+ hit_to_update = client.search.hit("howler.id:*", rows=1, sort="event.created desc")["items"][0]
171
+
172
+ result = client.hit.update(hit_to_update["howler"]["id"], [(UPDATE_SET, "howler.score", hit_to_update["howler"]["score"] + 100)])
173
+ ```
174
+
175
+ The following operations can be run to update a hit.
176
+
177
+ **List Operations:**
178
+
179
+ - `UPDATE_APPEND`: Used to append a value to a given list
180
+ - `UPDATE_APPEND_IF_MISSING`: Used to append a value to a given list if the value isn't already in the list
181
+ - `UPDATE_REMOVE`: Will remove a given value from a list
182
+
183
+ **Numeric Operations:**
184
+
185
+ - `UPDATE_DEC`: Decrement a numeric value by the specified amount
186
+ - `UPDATE_INC`: Increment a numeric value by the specified amount
187
+ - `UPDATE_MAX`: Will set a numeric value to the maximum of the existing value and the specified value
188
+ - `UPDATE_MIN`: Will set a numeric value to the minimum of the existing value and the specified value
189
+
190
+ **Multipurpose Operations:**
191
+
192
+ - `UPDATE_SET`: Set a field's value to the given value
193
+ - `UPDATE_DELETE`: Will delete a given field's value
194
+
195
+ #### `update_by_query()`
196
+
197
+ This function allows you to update a large number of hits by a query:
198
+
199
+ ```python
200
+ client.hit.update_by_query(f'howler.analytic:"Example Alert"', [(UPDATE_INC, "howler.score", 100)])
201
+ ```
202
+
203
+ The same operations as in `update()` can be used.
204
+
205
+ ### `overwrite()`
206
+
207
+ This function allows you to directly overwrite a hit with a partial hit object. This is the most easy to use, but loses some of the validation and additional processing of the update functions.
208
+
209
+ ```python
210
+ hit_to_update = client.search.hit("howler.id:*", rows=1, sort="event.created desc")["items"][0]
211
+
212
+ result = client.hit.overwrite(hit_to_update["howler"]["id"], {"source.ip": "127.0.0.1", "destination.ip": "8.8.8.8"})
213
+ ```
@@ -0,0 +1,37 @@
1
+ # Hit Links
2
+
3
+ In order to facilitate the addition of additional tools one can use to triage a hit, Howler allows users to specify a set of links, along with a title and icon to show. This documentation will walk you through how to use these links.
4
+
5
+ ## Specification
6
+
7
+ In order to add links, you can use the `howler.links` field. This field takes in a list of objects with three keys:
8
+
9
+ ```python
10
+ hit = {
11
+ "howler.links": [
12
+ {
13
+ "title": "Link Title with Internal Image",
14
+ "href": "https://example.com",
15
+ # Note that this specifies another application, not an image link
16
+ "icon": "superset",
17
+ },
18
+ {
19
+ "title": "Link Title with External Image",
20
+ "href": "https://www.britannica.com/animal/goose-bird",
21
+ # Note that this specifies an image link. We don't provide hosting, so you'll need to host it somewhere else!
22
+ "icon": "https://cdn.britannica.com/76/76076-050-39DDCBA1/goose-Canada-North-America.jpg",
23
+ },
24
+ ]
25
+ }
26
+ ```
27
+
28
+ Note the icon can either be:
29
+
30
+ 1. A name identifying a linked application (from the app switcher), to use its icon
31
+ 2. An external URL
32
+
33
+ If you'd like to use a linked app, the following values are currently supported:
34
+
35
+ $APP_LIST
36
+
37
+ Using any of these values will automatically use the corresponding icon. No need to host your own!