hachi 0.2.3 → 1.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: 293861a0525887a1d226fe9c8673de534787fc243b6fa8c0feddc61e3cf7ff48
4
- data.tar.gz: 4f2aecbe26c66918b393921e4f0666b34c62f2b8fc13d2c337d89789b86a22a3
3
+ metadata.gz: d03f917e4b7693f16612d130fb9808df41a4f66f962d757ec8e2dbedd6f039e1
4
+ data.tar.gz: 76ee4ec9ba69e732b17c7bff865fa5aaa5a251339b39b7daacc4e104000f99a3
5
5
  SHA512:
6
- metadata.gz: dd3136e1b68d371109ea98957d9a09b18459f3086d14ec94d9f023cda5b81ae06ae477d180535014d1a3ba9d33541d215e62283b87c9e101ad3a85c115bde9cf
7
- data.tar.gz: 4e866aed081ac5a526db9460c4b2ef148d0ee7ce17f5ffe758ef81cbbef6acb2970a7ec710ea4cc457825cb775c3a07bc7fdc46d1adc4ed0c04becd9e61a85c8
6
+ metadata.gz: 9281deed1829c4efe3ea82ed2ecafa616535cb4038867ab9ef480e28a328b39ad66c270503b5ef40d80b18c99dce849231aaa31e6c59a24ec5073dfebf9c103b
7
+ data.tar.gz: 637a3945b65862633bdfeeb1723f4179bd7a61ae989cc725b4427d94257dc5aa71c972100110afdadd61717294b7001957349ffb90a1978dcce70060186f82e6
@@ -0,0 +1,26 @@
1
+ name: Ruby CI
2
+
3
+ on: [pull_request]
4
+
5
+ jobs:
6
+ build:
7
+
8
+ runs-on: ubuntu-latest
9
+
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ ruby: [2.7, '3.0']
14
+
15
+ 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
data/README.md CHANGED
@@ -1,12 +1,14 @@
1
1
  # Hachi
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/hachi.svg)](https://badge.fury.io/rb/hachi)
4
- [![Build Status](https://travis-ci.org/ninoseki/hachi.svg?branch=master)](https://travis-ci.org/ninoseki/hachi)
4
+ [![Ruby CI](https://github.com/ninoseki/hachi/actions/workflows/test.yml/badge.svg)](https://github.com/ninoseki/hachi/actions/workflows/test.yml)
5
5
  [![Coverage Status](https://coveralls.io/repos/github/ninoseki/hachi/badge.svg?branch=master)](https://coveralls.io/github/ninoseki/hachi?branch=master)
6
6
  [![CodeFactor](https://www.codefactor.io/repository/github/ninoseki/hachi/badge)](https://www.codefactor.io/repository/github/ninoseki/hachi)
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.
11
+
10
12
  ## Installation
11
13
 
12
14
  ```bash
@@ -26,10 +28,9 @@ api = Hachi::API.new(api_endpoint: "http://your_api_endpoint", api_key: "yoru_ap
26
28
  # list alerts
27
29
  api.alert.list
28
30
 
29
- # search atrifacts
30
- api.artifact.search(data: "1.1.1.1", data_type: "ip")
31
- # you can do a bulk search by giving an array as an input
32
- api.artifact.search(data: %w(1.1.1.1 8.8.8.8 github.com))
31
+ # search artifacts
32
+ query = { "_and": [{ "_or": [{ "_field": "data", "_value": "1.1.1.1" }, { "_field": "data", "_value": "example.com" }] }] }
33
+ api.artifact.search(query)
33
34
  ```
34
35
 
35
36
  See `samples` for more.
@@ -41,25 +42,25 @@ See `samples` for more.
41
42
  | HTTP Method | URI | Action | API method |
42
43
  |-------------|-----------------------------------|-----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
43
44
  | GET | /api/alert | List alerts | `#api.alert.list` |
44
- | POST | /api/alert/_search | Find alerts | `#api.alert.search(attributes:, range: "all")` |
45
+ | POST | /api/alert/_search | Find alerts | `#api.alert.search(query, range: "all")` |
45
46
  | PATCH | /api/alert/_bulk | Update alerts in bulk | N/A |
46
47
  | POST | /api/alert/_stats | Compute stats on alerts | N/A |
47
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)` |
48
49
  | GET | /api/alert/:alertId | Get an alert | `#api.alert.get_by_id(id)` |
49
- | PATCH | /api/alert/:alertId | Update an alert | N/A |
50
+ | PATCH | /api/alert/:alertId | Update an alert | `#api.alert.update(id, title:, description:, severity: nil, tags: nil, tlp: nil, artifacts: nil)` |
50
51
  | DELETE | /api/alert/:alertId | Delete an alert | `#api.alert.delete_by_id(id)` |
51
- | POST | /api/alert/:alertId/markAsRead | Mark an alert as read | N/A |
52
- | POST | /api/alert/:alertId/markAsUnread | Mark an alert as unread | N/A |
53
- | POST | /api/alert/:alertId/createCase | Create a case from an alert | N/A |
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)` |
54
55
  | POST | /api/alert/:alertId/follow | Follow an alert | N/A |
55
56
  | POST | /api/alert/:alertId/unfollow | Unfollow an alert | N/A |
56
- | POST | /api/alert/:alertId/merge/:caseId | Merge an alert in a case | N/A |
57
+ | POST | /api/alert/:alertId/merge/:caseId | Merge an alert in a case | `#api.alert.merge_into_case(*ids, case_id)` |
57
58
 
58
59
  ### Artifact(Observable)
59
60
 
60
61
  | HTTP Method | URI | Action | API method |
61
62
  |-------------|----------------------------------------|---------------------------------|---------------------------------------------------------------------------------------|
62
- | POST | /api/case/artifact/_search | Find observables | `#api.artifact.search(attributes, range: "all")` |
63
+ | POST | /api/case/artifact/_search | Find observables | `#api.artifact.search(query, range: "all")` |
63
64
  | POST | /api/case/artifact/_stats | Compute stats on observables | N/A |
64
65
  | POST | /api/case/:caseId/artifact | Create an observable | `#api.artifact.create(case_id, data:, data_type:, message: nil, tlp: nil, tags: nil)` |
65
66
  | GET | /api/case/artifact/:artifactId | Get an observable | `#api.artifact.get_by_id(id)` |
@@ -73,7 +74,7 @@ See `samples` for more.
73
74
  | HTTP Method | URI | Action | API method |
74
75
  |-------------|------------------------------------|---------------------------------------|----------------------------------------------------------------------------------------------------------------------|
75
76
  | GET | /api/case | List cases | `#api.case.list` |
76
- | POST | /api/case/_search | Find cases | `#api.case.search(attributes, range: "all")` |
77
+ | POST | /api/case/_search | Find cases | `#api.case.search(query, range: "all")` |
77
78
  | PATCH | /api/case/_bulk | Update cases in bulk | N/A |
78
79
  | POST | /api/case/_stats | Compute stats on cases | N/A |
79
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,7 +82,31 @@ See `samples` for more.
81
82
  | PATCH | /api/case/:caseId | Update a case | N/A |
82
83
  | DELETE | /api/case/:caseId | Remove a case | `#api.case.delete_by_id(id)` |
83
84
  | GET | /api/case/:caseId/links | Get list of cases linked to this case | `#api.case.links(id)` |
84
- | POST | /api/case/:caseId1/_merge/:caseId2 | Merge two cases | N/A |
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
+ ```
85
110
 
86
111
  ## License
87
112
 
data/Rakefile CHANGED
@@ -3,4 +3,4 @@ require "rspec/core/rake_task"
3
3
 
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
- task :default => :spec
6
+ task default: :spec
data/hachi.gemspec CHANGED
@@ -24,10 +24,10 @@ 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.0"
27
+ spec.add_development_dependency "bundler", "~> 2.2"
28
28
  spec.add_development_dependency "coveralls", "~> 0.8"
29
- spec.add_development_dependency "rake", "~> 12.3"
30
- spec.add_development_dependency "rspec", "~> 3.8"
31
- spec.add_development_dependency "vcr", "~> 5.0"
32
- spec.add_development_dependency "webmock", "~> 3.6"
29
+ 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"
33
33
  end
data/lib/hachi.rb CHANGED
@@ -8,11 +8,13 @@ require "hachi/models/base"
8
8
  require "hachi/models/alert"
9
9
  require "hachi/models/artifact"
10
10
  require "hachi/models/case"
11
+ require "hachi/models/user"
11
12
 
12
13
  require "hachi/clients/base"
13
14
  require "hachi/clients/alert"
14
15
  require "hachi/clients/artifact"
15
16
  require "hachi/clients/case"
17
+ require "hachi/clients/user"
16
18
 
17
19
  module Hachi
18
20
  class Error < StandardError; end
data/lib/hachi/api.rb CHANGED
@@ -1,18 +1,69 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'forwardable'
4
+
3
5
  module Hachi
4
6
  class API
5
- attr_reader :alert
6
- attr_reader :artifact
7
- attr_reader :case
7
+ extend Forwardable
8
+
9
+ # @return [String] TheHive API endpoint
10
+ attr_reader :api_endpoint
8
11
 
12
+ # @return [String] TheHive API key
13
+ attr_reader :api_key
14
+
15
+ #
16
+ # @param [String, nil] api_endpoint TheHive API endpoint
17
+ # @param [String, nil] api_key TheHive API key
18
+ #
19
+ # @raise [ArgumentError] When given or an empty endpoint or key
20
+ #
9
21
  def initialize(api_endpoint: ENV["THEHIVE_API_ENDPOINT"], api_key: ENV["THEHIVE_API_KEY"])
10
- raise(ArgumentError, "api_endpoint argument is required") unless api_endpoint
11
- raise(ArgumentError, "api_key argument is required") unless api_key
22
+ @api_endpoint = api_endpoint
23
+ raise ArgumentError, "api_endpoint argument is required" unless api_endpoint
24
+
25
+ @api_key = api_key
26
+ raise ArgumentError, "api_key argument is required" unless api_key
27
+
28
+ @base = Clients::Base.new(api_endpoint: api_endpoint, api_key: api_key)
29
+ end
30
+
31
+ def_delegators :@base, :get, :post, :delete, :push
32
+
33
+ #
34
+ # Alert API endpoint client
35
+ #
36
+ # @return [Clients::Alert]
37
+ #
38
+ def alert
39
+ @alert ||= Clients::Alert.new(api_endpoint: api_endpoint, api_key: api_key)
40
+ end
41
+
42
+ #
43
+ # Artifact API endpoint client
44
+ #
45
+ # @return [Clients::Artifact]
46
+ #
47
+ def artifact
48
+ @artifact ||= Clients::Artifact.new(api_endpoint: api_endpoint, api_key: api_key)
49
+ end
50
+
51
+ #
52
+ # Case API endpoint client
53
+ #
54
+ # @return [Clients::Case]
55
+ #
56
+ def case
57
+ @case ||= Clients::Case.new(api_endpoint: api_endpoint, api_key: api_key)
58
+ end
12
59
 
13
- @alert = Clients::Alert.new(api_endpoint: api_endpoint, api_key: api_key)
14
- @artifact = Clients::Artifact.new(api_endpoint: api_endpoint, api_key: api_key)
15
- @case = Clients::Case.new(api_endpoint: api_endpoint, api_key: api_key)
60
+ #
61
+ # User API endpoint client
62
+ #
63
+ # @return [Clients::User]
64
+ #
65
+ def user
66
+ @user ||= Clients::User.new(api_endpoint: api_endpoint, api_key: api_key)
16
67
  end
17
68
  end
18
69
  end
@@ -6,19 +6,56 @@ require "securerandom"
6
6
  module Hachi
7
7
  module Clients
8
8
  class Alert < Base
9
+ #
10
+ # List alerts
11
+ #
12
+ # @return [Array]
13
+ #
9
14
  def list
10
15
  get("/api/alert") { |json| json }
11
16
  end
12
17
 
18
+ #
19
+ # Get an alert
20
+ #
21
+ # @param [String] id Alert ID
22
+ #
23
+ # @return [Hash]
24
+ #
13
25
  def get_by_id(id)
14
26
  get("/api/alert/#{id}") { |json| json }
15
27
  end
16
28
 
29
+ #
30
+ # Delete an alert
31
+ #
32
+ # @param [String] id Alert ID
33
+ #
34
+ # @return [String]
35
+ #
17
36
  def delete_by_id(id)
18
37
  delete("/api/alert/#{id}") { |json| json }
19
38
  end
20
39
 
21
- def create(title:, description:, severity: nil, date: nil, tags: nil, tlp: nil, status: nil, type:, source:, source_ref: nil, artifacts: nil, follow: nil)
40
+ #
41
+ # Create an alert
42
+ #
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)
22
59
  alert = Models::Alert.new(
23
60
  title: title,
24
61
  description: description,
@@ -33,11 +70,94 @@ module Hachi
33
70
  artifacts: artifacts,
34
71
  follow: follow,
35
72
  )
36
- post("/api/alert", alert.payload) { |json| json }
73
+ post("/api/alert", json: alert.payload) { |json| json }
37
74
  end
38
75
 
39
- def search(attributes, range: "all")
40
- _search("/api/alert/_search", attributes: attributes, range: range) { |json| json }
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
127
+ #
128
+ # @return [Hash]
129
+ #
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 }
136
+ end
137
+
138
+ #
139
+ # Update an alert
140
+ #
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
148
+ #
149
+ # @return [Hash]
150
+ #
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 }
41
161
  end
42
162
  end
43
163
  end
@@ -3,6 +3,18 @@
3
3
  module Hachi
4
4
  module Clients
5
5
  class Artifact < Base
6
+ #
7
+ # Create an artifact
8
+ #
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
15
+ #
16
+ # @return [Hash]
17
+ #
6
18
  def create(case_id, data:, data_type:, message: nil, tlp: nil, tags: nil)
7
19
  artifact = Models::Artifact.new(
8
20
  data: data,
@@ -12,21 +24,50 @@ module Hachi
12
24
  tags: tags,
13
25
  )
14
26
 
15
- post("/api/case/#{case_id}/artifact", artifact.payload) { |json| json }
27
+ post("/api/case/#{case_id}/artifact", json: artifact.payload) { |json| json }
16
28
  end
17
29
 
30
+ #
31
+ # Get an artifact
32
+ #
33
+ # @param [String] id Artifact ID
34
+ #
35
+ # @return [Hash]
36
+ #
18
37
  def get_by_id(id)
19
38
  get("/api/case/artifact/#{id}") { |json| json }
20
39
  end
21
40
 
41
+ #
42
+ # Delete an artifact
43
+ #
44
+ # @param [String] id Artifact ID
45
+ #
46
+ # @return [String]
47
+ #
22
48
  def delete_by_id(id)
23
49
  delete("/api/case/artifact/#{id}") { |json| json }
24
50
  end
25
51
 
26
- def search(attributes, range: "all")
27
- _search("/api/case/artifact/_search", attributes: attributes, range: range) { |json| json }
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 }
28
62
  end
29
63
 
64
+ #
65
+ # Get list of similar observables
66
+ #
67
+ # @param [String] id Artifact ID
68
+ #
69
+ # @return [Array]
70
+ #
30
71
  def similar(id)
31
72
  get("/api/case/artifact/#{id}/similar") { |json| json }
32
73
  end
@@ -2,18 +2,64 @@
2
2
 
3
3
  require "json"
4
4
  require "net/https"
5
+ require "uri"
5
6
 
6
7
  module Hachi
7
8
  module Clients
8
9
  class Base
9
- attr_reader :api_endpoint
10
- attr_reader :api_key
10
+ attr_reader :api_endpoint, :api_key
11
11
 
12
12
  def initialize(api_endpoint:, api_key:)
13
13
  @api_endpoint = URI(api_endpoint)
14
14
  @api_key = api_key
15
15
  end
16
16
 
17
+ def get(path, params: {}, &block)
18
+ url = url_for(path)
19
+ url.query = URI.encode_www_form(params) unless params.empty?
20
+
21
+ get = Net::HTTP::Get.new(url)
22
+ get.add_field "Authorization", "Bearer #{api_key}"
23
+ request(get, &block)
24
+ end
25
+
26
+ def post(path, params: {}, json: {}, &block)
27
+ url = url_for(path)
28
+ url.query = URI.encode_www_form(params) unless params.empty?
29
+
30
+ post = Net::HTTP::Post.new(url)
31
+ post.body = json.is_a?(Hash) ? json.to_json : json.to_s
32
+
33
+ post.add_field "Content-Type", "application/json"
34
+ post.add_field "Authorization", "Bearer #{api_key}"
35
+
36
+ request(post, &block)
37
+ end
38
+
39
+ def delete(path, params: {}, json: {}, &block)
40
+ url = url_for(path)
41
+ url.query = URI.encode_www_form(params) unless params.empty?
42
+
43
+ delete = Net::HTTP::Delete.new(url)
44
+ delete.body = json.is_a?(Hash) ? json.to_json : json.to_s
45
+
46
+ delete.add_field "Authorization", "Bearer #{api_key}"
47
+ request(delete, &block)
48
+ end
49
+
50
+ def patch(path, params: {}, json: {}, &block)
51
+ url = url_for(path)
52
+ url.query = URI.encode_www_form(params) unless params.empty?
53
+
54
+ patch = Net::HTTP::Patch.new(url)
55
+ patch.body = json.is_a?(Hash) ? json.to_json : json.to_s
56
+
57
+ patch.add_field "Content-Type", "application/json"
58
+ patch.add_field "Authorization", "Bearer #{api_key}"
59
+
60
+ request(patch, &block)
61
+ end
62
+
17
63
  private
18
64
 
19
65
  def base_url
@@ -73,36 +119,6 @@ module Hachi
73
119
  end
74
120
  end
75
121
 
76
- def get(path, params = {}, &block)
77
- url = url_for(path)
78
- url.query = URI.encode_www_form(params) unless params.empty?
79
-
80
- get = Net::HTTP::Get.new(url)
81
- get.add_field "Authorization", "Bearer #{api_key}"
82
- request(get, &block)
83
- end
84
-
85
- def post(path, params = {}, &block)
86
- url = url_for(path)
87
-
88
- post = Net::HTTP::Post.new(url)
89
- post.body = params.is_a?(Hash) ? params.to_json : params.to_s
90
-
91
- post.add_field "Content-Type", "application/json"
92
- post.add_field "Authorization", "Bearer #{api_key}"
93
-
94
- request(post, &block)
95
- end
96
-
97
- def delete(path, params = {}, &block)
98
- url = url_for(path)
99
- url.query = URI.encode_www_form(params) unless params.empty?
100
-
101
- delete = Net::HTTP::Delete.new(url)
102
- delete.add_field "Authorization", "Bearer #{api_key}"
103
- request(delete, &block)
104
- end
105
-
106
122
  def validate_range(range)
107
123
  return true if range == "all"
108
124
  raise ArgumentError, "range should be 'all' or `from-to`" unless range.match?(/(\d+)-(\d+)/)
@@ -113,49 +129,16 @@ module Hachi
113
129
  raise ArgumentError, "from should be smaller than to"
114
130
  end
115
131
 
116
- def _search(path, attributes:, range: "all")
132
+ def _search(path, query:, range: "all", sort: nil)
117
133
  validate_range range
118
134
 
119
- attributes = normalize_attributes(attributes)
120
- conditions = attributes.map do |key, value|
121
- if key == :data && value.is_a?(Array)
122
- { _or: decompose_data(value) }
123
- else
124
- { _string: "#{key}:#{value}" }
125
- end
126
- end
135
+ query_string = build_query_string(range: range, sort: sort)
127
136
 
128
- default_conditions = {
129
- _and: [
130
- { _not: { status: "Deleted" } },
131
- { _not: { _in: { _field: "_type", _values: ["dashboard", "data", "user", "analyzer", "caseTemplate", "reportTemplate", "action"] } } },
132
- ],
133
- }
134
-
135
- query = {
136
- _and: [conditions, default_conditions].flatten,
137
- }
138
-
139
- post("#{path}?range=#{range}", query: query) { |json| json }
140
- end
141
-
142
- def decompose_data(data)
143
- data.map do |elem|
144
- { _field: "data", _value: elem }
145
- end
146
- end
147
-
148
- def normalize_attributes(attributes)
149
- h = {}
150
- attributes.each do |key, value|
151
- h[camelize(key).to_sym] = value
152
- end
153
- h
137
+ post("#{path}?#{query_string}", json: { query: query }) { |json| json }
154
138
  end
155
139
 
156
- def camelize(string)
157
- head, *others = string.to_s.split("_")
158
- [head, others.map(&:capitalize)].flatten.join
140
+ def build_query_string(params)
141
+ URI.encode_www_form(params.reject { |_k, v| v.nil? })
159
142
  end
160
143
  end
161
144
  end
@@ -3,18 +3,51 @@
3
3
  module Hachi
4
4
  module Clients
5
5
  class Case < Base
6
+ #
7
+ # List cases
8
+ #
9
+ # @return [Array]
10
+ #
6
11
  def list
7
12
  get("/api/case") { |json| json }
8
13
  end
9
14
 
15
+ #
16
+ # Get a case
17
+ #
18
+ # @param [String] id Case ID
19
+ #
20
+ # @return [Hash]
21
+ #
10
22
  def get_by_id(id)
11
23
  get("/api/case/#{id}") { |json| json }
12
24
  end
13
25
 
26
+ #
27
+ # Delete a case
28
+ #
29
+ # @param [String] id Case ID
30
+ #
31
+ # @return [String]
32
+ #
14
33
  def delete_by_id(id)
15
34
  delete("/api/case/#{id}") { |json| json }
16
35
  end
17
36
 
37
+ #
38
+ # Create a case
39
+ #
40
+ # @param [String, nil] title
41
+ # @param [String, nil] description
42
+ # @param [Integer, nil] severity
43
+ # @param [String, nil] start_date
44
+ # @param [String, nil] owner
45
+ # @param [Boolean, nil] flag
46
+ # @param [Intege, nil] tlp
47
+ # @param [String, nil] tags
48
+ #
49
+ # @return [Hash]
50
+ #
18
51
  def create(title:, description:, severity: nil, start_date: nil, owner: nil, flag: nil, tlp: nil, tags: nil)
19
52
  kase = Models::Case.new(
20
53
  title: title,
@@ -27,16 +60,86 @@ module Hachi
27
60
  tags: tags,
28
61
  )
29
62
 
30
- post("/api/case", kase.payload) { |json| json }
63
+ post("/api/case", json: kase.payload) { |json| json }
31
64
  end
32
65
 
33
- def search(attributes, range: "all")
34
- _search("/api/case/_search", attributes: attributes, range: range) { |json| json }
66
+ #
67
+ # Find cases
68
+ #
69
+ # @param [Hash] query
70
+ # @param [String] range
71
+ #
72
+ # @return [Hash]
73
+ #
74
+ def search(query, range: "all")
75
+ _search("/api/case/_search", query: query, range: range) { |json| json }
35
76
  end
36
77
 
78
+ #
79
+ # Get list of cases linked to this case
80
+ #
81
+ # @param [String] id Case ID
82
+ #
83
+ # @return [Array]
84
+ #
37
85
  def links(id)
38
86
  get("/api/case/#{id}/links") { |json| json }
39
87
  end
88
+
89
+ #
90
+ # Merge two cases
91
+ #
92
+ # @param [String] id1 Case ID
93
+ # @param [String] id2 Case ID
94
+ #
95
+ # @return [Hash]
96
+ #
97
+ def merge(id1, id2)
98
+ post("/api/case/#{id1}/_merge/#{id2}") { |json| json }
99
+ end
100
+
101
+ #
102
+ # Update a case
103
+ #
104
+ # @param [String, nil] id
105
+ # @param [String, nil] title
106
+ # @param [String, nil] description
107
+ # @param [String, nil] severity
108
+ # @param [String, nil] start_date
109
+ # @param [String, nil] owner
110
+ # @param [Boolean, nil] flag
111
+ # @param [Integer, nil] tlp
112
+ # @param [String, nil] tags
113
+ # @param [String, nil] status
114
+ # @param [String, nil] resolution_status
115
+ # @param [String, nil] impact_status
116
+ # @param [String, nil] summary
117
+ # @param [String, nil] end_date
118
+ # @param [String, nil] metrics
119
+ # @param [String, nil] custom_fields
120
+ #
121
+ # @return [Hash]
122
+ #
123
+ def update(id, title: nil, description: nil, severity: nil, start_date: nil, owner: nil, flag: nil, tlp: nil, tags: nil, status: nil, resolution_status: nil, impact_status: nil, summary: nil, end_date: nil, metrics: nil, custom_fields: nil )
124
+ attributes = {
125
+ title: title,
126
+ description: description,
127
+ severity: severity,
128
+ startDate: start_date,
129
+ owner: owner,
130
+ flag: flag,
131
+ tlp: tlp,
132
+ tags: tags,
133
+ status: status,
134
+ resolutionStatus: resolution_status,
135
+ impactStatus: impact_status,
136
+ summary: summary,
137
+ endDate: end_date,
138
+ metrics: metrics,
139
+ customFields: custom_fields
140
+ }.compact
141
+ patch("/api/case/#{id}", json: attributes) { |json| json }
142
+ end
40
143
  end
41
144
  end
42
145
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hachi
4
+ module Clients
5
+ class User < Base
6
+ #
7
+ # Get current user
8
+ #
9
+ # @return [Hash]
10
+ #
11
+ def current
12
+ get("/api/user/current") { |json| json }
13
+ end
14
+
15
+ #
16
+ # Get a user
17
+ #
18
+ # @param [String] id User ID
19
+ #
20
+ # @return [Hash]
21
+ #
22
+ def get_by_id(id)
23
+ get("/api/user/#{id}") { |json| json }
24
+ end
25
+
26
+ #
27
+ # Delete a user
28
+ #
29
+ # @param [String] id User ID
30
+ #
31
+ # @return [String]
32
+ #
33
+ def delete_by_id(id)
34
+ delete("/api/user/#{id}") { |json| json }
35
+ end
36
+
37
+ #
38
+ # Create a user
39
+ #
40
+ # @param [String] login
41
+ # @param [String] name
42
+ # @param [Array<String>] roles
43
+ # @param [String] password
44
+ #
45
+ # @return [Hash]
46
+ #
47
+ def create(login:, name:, roles:, password:)
48
+ user = Models::User.new(
49
+ login: login,
50
+ name: name,
51
+ roles: roles,
52
+ password: password
53
+ )
54
+
55
+ post("/api/user", json: user.payload) { |json| json }
56
+ end
57
+ end
58
+ end
59
+ end
@@ -6,20 +6,9 @@ require "securerandom"
6
6
  module Hachi
7
7
  module Models
8
8
  class Alert < Base
9
- attr_reader :title
10
- attr_reader :description
11
- attr_reader :severity
12
- attr_reader :date
13
- attr_reader :tags
14
- attr_reader :tlp
15
- attr_reader :status
16
- attr_reader :type
17
- attr_reader :source
18
- attr_reader :source_ref
19
- attr_reader :artifacts
20
- attr_reader :follow
9
+ attr_reader :title, :description, :severity, :date, :tags, :tlp, :status, :type, :source, :source_ref, :artifacts, :follow
21
10
 
22
- def initialize(title:, description:, severity: nil, date: nil, tags: nil, tlp: nil, status: nil, type:, source:, source_ref: nil, artifacts: nil, follow: nil)
11
+ def initialize(title:, description:, type:, source:, severity: nil, date: nil, tags: nil, tlp: nil, status: nil, source_ref: nil, artifacts: nil, follow: nil)
23
12
  @title = title
24
13
  @description = description
25
14
  @severity = severity
@@ -30,7 +19,7 @@ module Hachi
30
19
  @type = type
31
20
  @source = source
32
21
  @source_ref = source_ref || SecureRandom.hex(10)
33
- @artifacts = artifacts.nil? ? nil : artifacts.map { |a| Artifact.new a }
22
+ @artifacts = artifacts.nil? ? nil : artifacts.map { |a| Artifact.new(**a) }
34
23
  @follow = follow
35
24
 
36
25
  validate_date if date
@@ -5,11 +5,7 @@ module Hachi
5
5
  class Artifact < Base
6
6
  DATA_TYPES = %w(filename file fqdn hash uri_path ip domain mail autonomous-system registry mail_subject regexp user-agent other url).freeze
7
7
 
8
- attr_reader :data
9
- attr_reader :data_type
10
- attr_reader :message
11
- attr_reader :tlp
12
- attr_reader :tags
8
+ attr_reader :data, :data_type, :message, :tlp, :tags
13
9
 
14
10
  def initialize(data:, data_type:, message: nil, tlp: nil, tags: nil)
15
11
  @data = data
@@ -3,14 +3,7 @@
3
3
  module Hachi
4
4
  module Models
5
5
  class Case < Base
6
- attr_reader :title
7
- attr_reader :description
8
- attr_reader :severity
9
- attr_reader :start_date
10
- attr_reader :owner
11
- attr_reader :flag
12
- attr_reader :tlp
13
- attr_reader :tags
6
+ attr_reader :title, :description, :severity, :start_date, :owner, :flag, :tlp, :tags
14
7
 
15
8
  def initialize(title:, description:, severity: nil, start_date: nil, owner: nil, flag: nil, tlp: nil, tags: nil)
16
9
  @title = title
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hachi
4
+ module Models
5
+ class User < Base
6
+ attr_reader :login, :name, :roles, :password
7
+
8
+ ROLES = %w(read write admin).freeze
9
+
10
+ def initialize(login:, name:, roles:, password:)
11
+ @login = login
12
+ @name = name
13
+ @roles = roles
14
+ @password = password
15
+
16
+ validate_roles
17
+ end
18
+
19
+ def payload
20
+ {
21
+ login: login,
22
+ name: name,
23
+ roles: roles,
24
+ password: password
25
+ }.compact
26
+ end
27
+
28
+ private
29
+
30
+ def validate_roles
31
+ raise ArgumentError, "roles should be an array" unless roles.is_a?(Array)
32
+ raise ArgumentError, "role should be one of #{ROLES.join('.')}" unless roles.all? { |role| ROLES.include? role }
33
+ end
34
+ end
35
+ end
36
+ end
data/lib/hachi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hachi
4
- VERSION = "0.2.3"
4
+ VERSION = "1.0.0"
5
5
  end
data/renovate.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": [
3
+ "config:base"
4
+ ]
5
+ }
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift("#{__dir__}/../lib")
4
+
5
+ require "hachi"
6
+
7
+ def api
8
+ @api ||= Hachi::API.new
9
+ end
10
+
11
+ description = ARGV[0].to_s
12
+ case_id = ARGV[1].to_s
13
+
14
+ alerts = api.alert.search(description: description)
15
+ alert_ids = alerts.map { |alert| alert.dig "id" }
16
+
17
+ api.alert.merge_into_case(alert_ids, case_id)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hachi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Manabu Niseki
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-08-11 00:00:00.000000000 Z
11
+ date: 2021-03-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '2.0'
19
+ version: '2.2'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '2.0'
26
+ version: '2.2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: coveralls
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -44,56 +44,56 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '12.3'
47
+ version: '13.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '12.3'
54
+ version: '13.0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rspec
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '3.8'
61
+ version: '3.10'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '3.8'
68
+ version: '3.10'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: vcr
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '5.0'
75
+ version: '6.0'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '5.0'
82
+ version: '6.0'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: webmock
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '3.6'
89
+ version: '3.12'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '3.6'
96
+ version: '3.12'
97
97
  description: A dead simple TheHive API wrapper.
98
98
  email:
99
99
  - manabu.niseki@gmail.com
@@ -101,9 +101,9 @@ executables: []
101
101
  extensions: []
102
102
  extra_rdoc_files: []
103
103
  files:
104
+ - ".github/workflows/test.yml"
104
105
  - ".gitignore"
105
106
  - ".rspec"
106
- - ".travis.yml"
107
107
  - Gemfile
108
108
  - LICENSE.txt
109
109
  - README.md
@@ -117,19 +117,23 @@ files:
117
117
  - lib/hachi/clients/artifact.rb
118
118
  - lib/hachi/clients/base.rb
119
119
  - lib/hachi/clients/case.rb
120
+ - lib/hachi/clients/user.rb
120
121
  - lib/hachi/models/alert.rb
121
122
  - lib/hachi/models/artifact.rb
122
123
  - lib/hachi/models/base.rb
123
124
  - lib/hachi/models/case.rb
125
+ - lib/hachi/models/user.rb
124
126
  - lib/hachi/version.rb
127
+ - renovate.json
125
128
  - samples/01_create_an_alert.rb
126
129
  - samples/02_search_artifacts.rb
127
130
  - samples/03_list_cases.rb
131
+ - samples/04_merge_alerts.rb
128
132
  homepage: https://github.com/ninoseki/hachi
129
133
  licenses:
130
134
  - MIT
131
135
  metadata: {}
132
- post_install_message:
136
+ post_install_message:
133
137
  rdoc_options: []
134
138
  require_paths:
135
139
  - lib
@@ -144,8 +148,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
144
148
  - !ruby/object:Gem::Version
145
149
  version: '0'
146
150
  requirements: []
147
- rubygems_version: 3.0.4
148
- signing_key:
151
+ rubygems_version: 3.2.3
152
+ signing_key:
149
153
  specification_version: 4
150
154
  summary: A dead simple TheHive API wrapper.
151
155
  test_files: []
data/.travis.yml DELETED
@@ -1,7 +0,0 @@
1
- ---
2
- sudo: false
3
- language: ruby
4
- cache: bundler
5
- rvm:
6
- - 2.6.1
7
- before_install: gem install bundler -v 2.0.1