hachi 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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