hachi 1.0.0 → 2.0.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d03f917e4b7693f16612d130fb9808df41a4f66f962d757ec8e2dbedd6f039e1
4
- data.tar.gz: 76ee4ec9ba69e732b17c7bff865fa5aaa5a251339b39b7daacc4e104000f99a3
3
+ metadata.gz: a88c7c477df9e4dbd8566ff3464424dbd994f9d976f9e235b6c1244200d32fd7
4
+ data.tar.gz: 1729ececbdb34f98abff15b7d80c189729d10d840f199876c87dc8eb22411581
5
5
  SHA512:
6
- metadata.gz: 9281deed1829c4efe3ea82ed2ecafa616535cb4038867ab9ef480e28a328b39ad66c270503b5ef40d80b18c99dce849231aaa31e6c59a24ec5073dfebf9c103b
7
- data.tar.gz: 637a3945b65862633bdfeeb1723f4179bd7a61ae989cc725b4427d94257dc5aa71c972100110afdadd61717294b7001957349ffb90a1978dcce70060186f82e6
6
+ metadata.gz: 40517bf2830268e88af14dc70ffc1af9f1e7c32d509e53883b6e125b48b7e689b81f628eb6225f2b76c517479ab19efb721ccea0b24b8bf9df11b598796aeb4d
7
+ data.tar.gz: e334655c99088a1038618b85a03f068fe3c459bd25f5eb179cc6a69bfb654aed185893e0d139ea270854cb87e80e6c62120c70a8f7e1a2f79e4325207b66c0be
@@ -4,23 +4,20 @@ on: [pull_request]
4
4
 
5
5
  jobs:
6
6
  build:
7
-
8
7
  runs-on: ubuntu-latest
9
8
 
10
9
  strategy:
11
10
  fail-fast: false
12
11
  matrix:
13
- ruby: [2.7, '3.0']
12
+ ruby: [2.7, "3.0", 3.1]
14
13
 
15
14
  steps:
16
- - uses: actions/checkout@v2
17
- - name: Set up Ruby
18
- uses: ruby/setup-ruby@v1
19
- with:
20
- ruby-version: ${{ matrix.ruby }}
21
- bundler-cache: true
22
- - name: Build and test with Rake
23
- run: |
24
- gem install bundler
25
- bundle install
26
- bundle exec rake
15
+ - uses: actions/checkout@v3
16
+ - name: Set up Ruby
17
+ uses: ruby/setup-ruby@v1
18
+ with:
19
+ ruby-version: ${{ matrix.ruby }}
20
+ bundler-cache: true
21
+ - name: Test with Rake
22
+ run: |
23
+ bundle exec rake
data/.gitignore CHANGED
@@ -11,7 +11,7 @@
11
11
  /tmp/
12
12
 
13
13
  # Used by dotenv library to load environment variables.
14
- # .env
14
+ .env
15
15
 
16
16
  ## Specific to RubyMotion:
17
17
  .dat*
@@ -51,3 +51,7 @@ Gemfile.lock
51
51
 
52
52
  # RSpec
53
53
  .rspec_status
54
+
55
+ # Docker
56
+ Dockerfile
57
+ docker-compose.yml
data/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  Hachi(`蜂`) is a dead simple [TheHive](https://github.com/TheHive-Project/TheHive) API wrapper for Ruby.
9
9
 
10
- **Note**: This library supports TheHive v4.
10
+ **Note**: This library supports TheHive v4 & v5.
11
11
 
12
12
  ## Installation
13
13
 
@@ -17,95 +17,21 @@ gem install hachi
17
17
 
18
18
  ## Usage
19
19
 
20
+ Hachi tries to load API settings from `ENV` by default. Or you can set them manually.
21
+
22
+ | Name | Default | Desc. |
23
+ |--------------|-----------------------------|-------------------------------------------------|
24
+ | api_key | ENV["THEHIVE_API_KEY"] | TheHive API key |
25
+ | api_endpoint | ENV["THEHIVE_API_ENDPOINT"] | TheHive API endpoint |
26
+ | api_version | ENV["THEHIVE_API_VERSION"] | TheHive API version (`nil` for v4, `v1` for v5) |
27
+
20
28
  ```ruby
21
29
  require "hachi"
22
30
 
23
- # when given nothing, it tries to load your API key from ENV["THEHIVE_API_KEY"] & API endpoint from ENV["THEHIVE_API_ENDPOINT"]
31
+ # when given nothing, it tries to load your API key from ENV
24
32
  api = Hachi::API.new
25
33
  # or you can set them manually
26
34
  api = Hachi::API.new(api_endpoint: "http://your_api_endpoint", api_key: "yoru_api_key")
27
-
28
- # list alerts
29
- api.alert.list
30
-
31
- # search artifacts
32
- query = { "_and": [{ "_or": [{ "_field": "data", "_value": "1.1.1.1" }, { "_field": "data", "_value": "example.com" }] }] }
33
- api.artifact.search(query)
34
- ```
35
-
36
- See `samples` for more.
37
-
38
- ## Implemented methods
39
-
40
- ### Alert
41
-
42
- | HTTP Method | URI | Action | API method |
43
- |-------------|-----------------------------------|-----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
44
- | GET | /api/alert | List alerts | `#api.alert.list` |
45
- | POST | /api/alert/_search | Find alerts | `#api.alert.search(query, range: "all")` |
46
- | PATCH | /api/alert/_bulk | Update alerts in bulk | N/A |
47
- | POST | /api/alert/_stats | Compute stats on alerts | N/A |
48
- | POST | /api/alert | Create an alert | `#api.alert.create(title:, description:, severity: nil, date: nil, tags: nil, tlp: nil, status: nil, type:, source:, source_ref: nil, artifacts: nil, follow: nil)` |
49
- | GET | /api/alert/:alertId | Get an alert | `#api.alert.get_by_id(id)` |
50
- | PATCH | /api/alert/:alertId | Update an alert | `#api.alert.update(id, title:, description:, severity: nil, tags: nil, tlp: nil, artifacts: nil)` |
51
- | DELETE | /api/alert/:alertId | Delete an alert | `#api.alert.delete_by_id(id)` |
52
- | POST | /api/alert/:alertId/markAsRead | Mark an alert as read | `#api.alert.mark_as_read(id)` |
53
- | POST | /api/alert/:alertId/markAsUnread | Mark an alert as unread | `#api.alert.mark_as_unread(id)` |
54
- | POST | /api/alert/:alertId/createCase | Create a case from an alert | `#api.alert.promote_to_case(id)` |
55
- | POST | /api/alert/:alertId/follow | Follow an alert | N/A |
56
- | POST | /api/alert/:alertId/unfollow | Unfollow an alert | N/A |
57
- | POST | /api/alert/:alertId/merge/:caseId | Merge an alert in a case | `#api.alert.merge_into_case(*ids, case_id)` |
58
-
59
- ### Artifact(Observable)
60
-
61
- | HTTP Method | URI | Action | API method |
62
- |-------------|----------------------------------------|---------------------------------|---------------------------------------------------------------------------------------|
63
- | POST | /api/case/artifact/_search | Find observables | `#api.artifact.search(query, range: "all")` |
64
- | POST | /api/case/artifact/_stats | Compute stats on observables | N/A |
65
- | POST | /api/case/:caseId/artifact | Create an observable | `#api.artifact.create(case_id, data:, data_type:, message: nil, tlp: nil, tags: nil)` |
66
- | GET | /api/case/artifact/:artifactId | Get an observable | `#api.artifact.get_by_id(id)` |
67
- | DELETE | /api/case/artifact/:artifactId | Remove an observable | `#api.artifact.delete_by_id(id)` |
68
- | PATCH | /api/case/artifact/:artifactId | Update an observable | N/A |
69
- | GET | /api/case/artifact/:artifactId/similar | Get list of similar observables | `#api.artifact.similar(id)` |
70
- | PATCH | /api/case/artifact/_bulk | Update observables in bulk | N/A |
71
-
72
- ### Case
73
-
74
- | HTTP Method | URI | Action | API method |
75
- |-------------|------------------------------------|---------------------------------------|----------------------------------------------------------------------------------------------------------------------|
76
- | GET | /api/case | List cases | `#api.case.list` |
77
- | POST | /api/case/_search | Find cases | `#api.case.search(query, range: "all")` |
78
- | PATCH | /api/case/_bulk | Update cases in bulk | N/A |
79
- | POST | /api/case/_stats | Compute stats on cases | N/A |
80
- | POST | /api/case | Create a case | `#api.case.create(title:, description:, severity: nil, start_date: nil, owner: nil, flag: nil, tlp: nil, tags: nil)` |
81
- | GET | /api/case/:caseId | Get a case | `#api.case.get_by_id(id)` |
82
- | PATCH | /api/case/:caseId | Update a case | N/A |
83
- | DELETE | /api/case/:caseId | Remove a case | `#api.case.delete_by_id(id)` |
84
- | GET | /api/case/:caseId/links | Get list of cases linked to this case | `#api.case.links(id)` |
85
- | POST | /api/case/:caseId1/_merge/:caseId2 | Merge two cases | `#api.case.merge(id1, id2)` |
86
-
87
- ### User
88
-
89
- | HTTP Method | URI | Action | API method |
90
- |-------------|-----------------------------------|---------------------|------------------------------------------------------|
91
- | GET | /api/logout | Logout | N/A |
92
- | POST | /api/login | User login | N/A |
93
- | GET | /api/user/current | Get current user | `#api.user.current` |
94
- | POST | /api/user/_search | Find user | N/A |
95
- | POST | /api/user | Create a user | `#api.user.create(login:, name:, roles:, password:)` |
96
- | GET | /api/user/:userId | Get a user | `#api.user.get_by_id(id)` |
97
- | DELETE | /api/user/:userId | Delete a case | `#api.user.delete_by_id(id)` |
98
- | PATCH | /api/user/:userId | Update user details | N/A |
99
- | POST | /api/user/:userId/password/set | Set password | N/A |
100
- | POST | /api/user/:userId/password/change | Change password | N/A |
101
-
102
-
103
- ## How to interact with unimplemented API endpoints
104
-
105
- `Hachi::API` exposes `get`, `post`, `delete` and `patch` methods. You can interact with the API endpoints via the methods.
106
-
107
- ```ruby
108
- alerts = api.get("/api/alert" ) { |json| json }
109
35
  ```
110
36
 
111
37
  ## License
data/hachi.gemspec CHANGED
@@ -24,10 +24,12 @@ Gem::Specification.new do |spec|
24
24
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
25
  spec.require_paths = ["lib"]
26
26
 
27
- spec.add_development_dependency "bundler", "~> 2.2"
28
- spec.add_development_dependency "coveralls", "~> 0.8"
27
+ spec.add_development_dependency "bundler", "~> 2.3"
28
+ spec.add_development_dependency "coveralls_reborn", "~> 0.24"
29
+ spec.add_development_dependency "dotenv", "~> 2.7"
29
30
  spec.add_development_dependency "rake", "~> 13.0"
30
- spec.add_development_dependency "rspec", "~> 3.10"
31
- spec.add_development_dependency "vcr", "~> 6.0"
32
- spec.add_development_dependency "webmock", "~> 3.12"
31
+ spec.add_development_dependency "rspec", "~> 3.11"
32
+ spec.add_development_dependency "simplecov-lcov", "~> 0.8.0"
33
+ spec.add_development_dependency "vcr", "~> 6.1"
34
+ spec.add_development_dependency "webmock", "~> 3.14"
33
35
  end
data/lib/hachi/api.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'forwardable'
3
+ require "forwardable"
4
4
 
5
5
  module Hachi
6
6
  class API
@@ -12,20 +12,26 @@ module Hachi
12
12
  # @return [String] TheHive API key
13
13
  attr_reader :api_key
14
14
 
15
+ # @return [String, nil] TheHive API version
16
+ attr_reader :api_version
17
+
15
18
  #
16
19
  # @param [String, nil] api_endpoint TheHive API endpoint
17
20
  # @param [String, nil] api_key TheHive API key
21
+ # @param [String, nil] api_version TheHive API version
18
22
  #
19
23
  # @raise [ArgumentError] When given or an empty endpoint or key
20
24
  #
21
- def initialize(api_endpoint: ENV["THEHIVE_API_ENDPOINT"], api_key: ENV["THEHIVE_API_KEY"])
25
+ def initialize(api_endpoint: ENV.fetch("THEHIVE_API_ENDPOINT", nil), api_key: ENV.fetch("THEHIVE_API_KEY", nil), api_version: ENV.fetch("THEHIVE_API_VERSION", nil))
22
26
  @api_endpoint = api_endpoint
23
27
  raise ArgumentError, "api_endpoint argument is required" unless api_endpoint
24
28
 
25
29
  @api_key = api_key
26
30
  raise ArgumentError, "api_key argument is required" unless api_key
27
31
 
28
- @base = Clients::Base.new(api_endpoint: api_endpoint, api_key: api_key)
32
+ @api_version = api_version
33
+
34
+ @base = Clients::Base.new(api_endpoint: api_endpoint, api_key: api_key, api_version: api_version)
29
35
  end
30
36
 
31
37
  def_delegators :@base, :get, :post, :delete, :push
@@ -36,7 +42,7 @@ module Hachi
36
42
  # @return [Clients::Alert]
37
43
  #
38
44
  def alert
39
- @alert ||= Clients::Alert.new(api_endpoint: api_endpoint, api_key: api_key)
45
+ @alert ||= Clients::Alert.new(api_endpoint: api_endpoint, api_key: api_key, api_version: api_version)
40
46
  end
41
47
 
42
48
  #
@@ -45,7 +51,7 @@ module Hachi
45
51
  # @return [Clients::Artifact]
46
52
  #
47
53
  def artifact
48
- @artifact ||= Clients::Artifact.new(api_endpoint: api_endpoint, api_key: api_key)
54
+ @artifact ||= Clients::Artifact.new(api_endpoint: api_endpoint, api_key: api_key, api_version: api_version)
49
55
  end
50
56
 
51
57
  #
@@ -54,7 +60,7 @@ module Hachi
54
60
  # @return [Clients::Case]
55
61
  #
56
62
  def case
57
- @case ||= Clients::Case.new(api_endpoint: api_endpoint, api_key: api_key)
63
+ @case ||= Clients::Case.new(api_endpoint: api_endpoint, api_key: api_key, api_version: api_version)
58
64
  end
59
65
 
60
66
  #
@@ -63,7 +69,25 @@ module Hachi
63
69
  # @return [Clients::User]
64
70
  #
65
71
  def user
66
- @user ||= Clients::User.new(api_endpoint: api_endpoint, api_key: api_key)
72
+ @user ||= Clients::User.new(api_endpoint: api_endpoint, api_key: api_key, api_version: api_version)
73
+ end
74
+
75
+ #
76
+ # Observable API endpoint client
77
+ #
78
+ # @return [Clients::Observable]
79
+ #
80
+ def observable
81
+ @observable ||= Clients::Observable.new(api_endpoint: api_endpoint, api_key: api_key, api_version: api_version)
82
+ end
83
+
84
+ #
85
+ # Query API endpoint client
86
+ #
87
+ # @return [Clients::Query]
88
+ #
89
+ def query
90
+ @query ||= Clients::Query.new(api_endpoint: api_endpoint, api_key: api_key, api_version: api_version)
67
91
  end
68
92
  end
69
93
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ # forked from https://github.com/technicalpanda/awrence
4
+ module Hachi
5
+ module Awrence
6
+ module Methods
7
+ # Recursively converts Rubyish snake_case hash keys to camelBack JSON-style
8
+ # hash keys suitable for use with a JSON API.
9
+ #
10
+ def to_camelback_keys(value = self)
11
+ process_value(:to_camelback_keys, value, first_upper: false)
12
+ end
13
+
14
+ # Recursively converts Rubyish snake_case hash keys to CamelCase JSON-style
15
+ # hash keys suitable for use with a JSON API.
16
+ #
17
+ def to_camel_keys(value = self)
18
+ process_value(:to_camel_keys, value, first_upper: true)
19
+ end
20
+
21
+ private
22
+
23
+ def camelize_key(key, first_upper: true)
24
+ case key
25
+ when Symbol
26
+ camelize(key.to_s, first_upper: first_upper).to_sym
27
+ when String
28
+ camelize(key, first_upper: first_upper)
29
+ else
30
+ key # Awrence can't camelize anything except strings and symbols
31
+ end
32
+ end
33
+
34
+ def camelize(snake_word, first_upper: true)
35
+ return snake_word if snake_word.start_with? "_"
36
+
37
+ if first_upper
38
+ str = snake_word.to_s
39
+ str = gsubbed(str, /(?:^|_)([^_\s]+)/)
40
+ gsubbed(str, %r{/([^/]*)}, "::")
41
+ else
42
+ parts = snake_word.split("_", 2)
43
+ parts[0] << camelize(parts[1]) if parts.size > 1
44
+ parts[0] || ""
45
+ end
46
+ end
47
+
48
+ def gsubbed(str, pattern, extra = "")
49
+ str.gsub(pattern) do
50
+ extra + (Hachi::Awrence.acronyms[Regexp.last_match(1)] || Regexp.last_match(1).capitalize)
51
+ end
52
+ end
53
+
54
+ def process_value(method, value, first_upper: true)
55
+ case value
56
+ when Array
57
+ value.map { |v| send(method, v) }
58
+ when Hash
59
+ value.to_h { |k, v| [camelize_key(k, first_upper: first_upper), send(method, v)] }
60
+ else
61
+ value
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -6,15 +6,6 @@ require "securerandom"
6
6
  module Hachi
7
7
  module Clients
8
8
  class Alert < Base
9
- #
10
- # List alerts
11
- #
12
- # @return [Array]
13
- #
14
- def list
15
- get("/api/alert") { |json| json }
16
- end
17
-
18
9
  #
19
10
  # Get an alert
20
11
  #
@@ -23,7 +14,7 @@ module Hachi
23
14
  # @return [Hash]
24
15
  #
25
16
  def get_by_id(id)
26
- get("/api/alert/#{id}") { |json| json }
17
+ get("/alert/#{id}") { |json| json }
27
18
  end
28
19
 
29
20
  #
@@ -34,130 +25,29 @@ module Hachi
34
25
  # @return [String]
35
26
  #
36
27
  def delete_by_id(id)
37
- delete("/api/alert/#{id}") { |json| json }
28
+ delete("/alert/#{id}") { |json| json }
38
29
  end
39
30
 
40
31
  #
41
32
  # Create an alert
42
33
  #
43
- # @param [String] title
44
- # @param [String] description
45
- # @param [String, nil] severity
46
- # @param [String, nil] date
47
- # @param [String, nil] tags
48
- # @param [String, nil] tlp
49
- # @param [String, nil] status
50
- # @param [String, nil] type
51
- # @param [String, nil] source
52
- # @param [String, nil] source_ref
53
- # @param [String, nil] artifacts
54
- # @param [String, nil] follow
55
- #
56
- # @return [Hash]
57
- #
58
- def create(title:, description:, type:, source:, severity: nil, date: nil, tags: nil, tlp: nil, status: nil, source_ref: nil, artifacts: nil, follow: nil)
59
- alert = Models::Alert.new(
60
- title: title,
61
- description: description,
62
- severity: severity,
63
- date: date,
64
- tags: tags,
65
- tlp: tlp,
66
- status: status,
67
- type: type,
68
- source: source,
69
- source_ref: source_ref,
70
- artifacts: artifacts,
71
- follow: follow,
72
- )
73
- post("/api/alert", json: alert.payload) { |json| json }
74
- end
75
-
76
- #
77
- # Find alerts
78
- #
79
- # @param [Hash] query
80
- # @param [String] range
81
- # @param [String, nil] sort
82
- #
83
- # @return [Array]
84
- #
85
- def search(query, range: "all", sort: nil)
86
- _search("/api/alert/_search", query: query, range: range, sort: sort) { |json| json }
87
- end
88
-
89
- #
90
- # Mark an alert as read
91
- #
92
- # @param [String] id Alert ID
93
- #
94
- # @return [Hash]
95
- #
96
- def mark_as_read(id)
97
- post("/api/alert/#{id}/markAsRead") { |json| json }
98
- end
99
-
100
- #
101
- # Mark an alert as unread
102
- #
103
- # @param [String] id Alert ID
104
- #
105
- # @return [Hash] hash
106
- #
107
- def mark_as_unread(id)
108
- post("/api/alert/#{id}/markAsUnread") { |json| json }
109
- end
110
-
111
- #
112
- # Create a case from an alert
113
- #
114
- # @param [String] id Alert ID
115
- #
116
- # @return [Hash]
117
- #
118
- def promote_to_case(id)
119
- post("/api/alert/#{id}/createCase") { |json| json }
120
- end
121
-
122
- #
123
- # Merge an alert / alerts in a case
124
- #
125
- # @param [String, Array] *ids Alert ID(s)
126
- # @param [String] case_id Case ID
34
+ # @param [Hash] payload
127
35
  #
128
36
  # @return [Hash]
129
37
  #
130
- def merge_into_case(*ids, case_id)
131
- params = {
132
- alertIds: ids.flatten,
133
- caseId: case_id
134
- }
135
- post("/api/alert/merge/_bulk", json: params) { |json| json }
38
+ def create(**payload)
39
+ post("/alert", json: payload) { |json| json }
136
40
  end
137
41
 
138
- #
139
42
  # Update an alert
140
43
  #
141
- # @param [String, nil] id
142
- # @param [String, nil] title
143
- # @param [String, nil] description
144
- # @param [String, nil] severity
145
- # @param [String, nil] tags
146
- # @param [String, nil] tlp
147
- # @param [String, nil] artifacts
44
+ # @param [String] id
45
+ # @param [Hash] payload
148
46
  #
149
47
  # @return [Hash]
150
48
  #
151
- def update(id, title: nil, description: nil, severity: nil, tags: nil, tlp: nil, artifacts: nil)
152
- attributes = {
153
- title: title,
154
- description: description,
155
- severity: severity,
156
- tags: tags,
157
- tlp: tlp,
158
- artifacts: artifacts,
159
- }.compact
160
- patch("/api/alert/#{id}", json: attributes) { |json| json }
49
+ def update(id, **payload)
50
+ patch("/alert/#{id}", json: payload) { |json| json }
161
51
  end
162
52
  end
163
53
  end
@@ -7,24 +7,12 @@ module Hachi
7
7
  # Create an artifact
8
8
  #
9
9
  # @param [String] case_id Artifact ID
10
- # @param [String] data
11
- # @param [String] data_type
12
- # @param [String, nil] message
13
- # @param [Integer, nil] tlp
14
- # @param [Array<String>, nil] tags
10
+ # @param [Hash] payload
15
11
  #
16
12
  # @return [Hash]
17
13
  #
18
- def create(case_id, data:, data_type:, message: nil, tlp: nil, tags: nil)
19
- artifact = Models::Artifact.new(
20
- data: data,
21
- data_type: data_type,
22
- message: message,
23
- tlp: tlp,
24
- tags: tags,
25
- )
26
-
27
- post("/api/case/#{case_id}/artifact", json: artifact.payload) { |json| json }
14
+ def create(case_id, **payload)
15
+ post("/api/case/#{case_id}/artifact", json: payload) { |json| json }
28
16
  end
29
17
 
30
18
  #
@@ -48,29 +36,6 @@ module Hachi
48
36
  def delete_by_id(id)
49
37
  delete("/api/case/artifact/#{id}") { |json| json }
50
38
  end
51
-
52
- #
53
- # Find artifacts
54
- #
55
- # @param [Hash] query
56
- # @param [String] range
57
- #
58
- # @return [Array]
59
- #
60
- def search(query, range: "all")
61
- _search("/api/case/artifact/_search", query: query, range: range) { |json| json }
62
- end
63
-
64
- #
65
- # Get list of similar observables
66
- #
67
- # @param [String] id Artifact ID
68
- #
69
- # @return [Array]
70
- #
71
- def similar(id)
72
- get("/api/case/artifact/#{id}/similar") { |json| json }
73
- end
74
39
  end
75
40
  end
76
41
  end