misp 0.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: db847f1e32b2e0a1064180b310b087ade5c9d608282bcbeebc5403f02b0cb71d
4
+ data.tar.gz: e1090389330d2de0bfd8b0c58cd0768f1a9bc58bb220d80c9d74aea6cb9ec738
5
+ SHA512:
6
+ metadata.gz: a4b3e71c139193309e919b6dc344f4fb200521b07a90fd3e45384d272549632af96eec9852416fdc8ddbfda31c9675f5ab107ae4cb1b2ff61b98dbeba0979457
7
+ data.tar.gz: 16b98a9dae45d273b70600565f8130d43d592e4097b517f49a69177c367a8c88a5841e4eb238a9629f9a45d1ca12ce7addfb5118c27f52d45a9c4a0ae3e56f61
@@ -0,0 +1,52 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ .env
15
+
16
+ ## Specific to RubyMotion:
17
+ .dat*
18
+ .repl_history
19
+ build/
20
+ *.bridgesupport
21
+ build-iPhoneOS/
22
+ build-iPhoneSimulator/
23
+
24
+ ## Specific to RubyMotion (use of CocoaPods):
25
+ #
26
+ # We recommend against adding the Pods directory to your .gitignore. However
27
+ # you should judge for yourself, the pros and cons are mentioned at:
28
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
29
+ #
30
+ # vendor/Pods/
31
+
32
+ ## Documentation cache and generated files:
33
+ /.yardoc/
34
+ /_yardoc/
35
+ /doc/
36
+ /rdoc/
37
+
38
+ ## Environment normalization:
39
+ /.bundle/
40
+ /vendor/bundle
41
+ /lib/bundler/man/
42
+
43
+ # for a library or gem, you might want to ignore these files since the code is
44
+ # intended to run in multiple environments; otherwise, check them in:
45
+ Gemfile.lock
46
+ .ruby-version
47
+ .ruby-gemset
48
+
49
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
50
+ .rvmrc
51
+
52
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.6
7
+ before_install: gem install bundler -v 2.0.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in misp-rb.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Manabu Niseki
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,118 @@
1
+ # misp-rb
2
+
3
+ [![Build Status](https://travis-ci.com/ninoseki/misp-rb.svg?branch=master)](https://travis-ci.com/ninoseki/misp-rb)
4
+ [![Coverage Status](https://coveralls.io/repos/github/ninoseki/misp-rb/badge.svg?branch=master)](https://coveralls.io/github/ninoseki/misp-rb?branch=master)
5
+ [![CodeFactor](https://www.codefactor.io/repository/github/ninoseki/misp-rb/badge)](https://www.codefactor.io/repository/github/ninoseki/misp-rb)
6
+
7
+ A dead simple MISP API wrapper for Ruby.
8
+
9
+ If you aren't a Rubyist, I highly recommend to use the official [PyMISP](https://github.com/MISP/PyMISP).
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ gem install misp
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ### Configuration
20
+
21
+ By default, it tries to load configurations from environmental variables:
22
+
23
+ - MISP API endpoint from ENV["MISP_API_ENDPOINT"]
24
+ - MISP API key from ENV["MISP_API_KEY"]
25
+
26
+ Also, you can configure them manually.
27
+
28
+ ```ruby
29
+ require "misp"
30
+
31
+ MISP.configure do |config|
32
+ config.api_endpoint = "https://misppriv.circl.lu"
33
+ config.api_key = "MISP_API_KEY"
34
+ end
35
+ ```
36
+
37
+ ### Create an event
38
+
39
+ ```ruby
40
+ event = MISP::Event.create(info: "my event")
41
+ ```
42
+
43
+ ### Retrive an event
44
+
45
+ ```ruby
46
+ event = MISP::Event.get(15)
47
+ ```
48
+
49
+ ### Update an event
50
+
51
+ ```ruby
52
+ event = MISP::Event.get(17)
53
+ event.info = "my new info field"
54
+ event.update
55
+ ```
56
+
57
+ ### Add an attribute
58
+
59
+ ```ruby
60
+ event = MISP::Event.get(17)
61
+ event.add_attribute(value: "8.8.8.8", type: "ip-dst")
62
+ # or
63
+ attribute = MISP::Attribute(value: "1.1.1.1", type: "ip-dst")
64
+ event.add_attribute attribute
65
+ event.update
66
+ ```
67
+
68
+ ### Tag an event
69
+
70
+ ```ruby
71
+ event = MISP::Event.get(17)
72
+ event.add_tag name: "my tag"
73
+ event.update
74
+ ```
75
+
76
+ ### Tag an attribute
77
+
78
+ ```ruby
79
+ attribute = MISP::Attribute.search(value: "8.8.8.8").first
80
+ attribute.add_tag(name: "my tag")
81
+ ```
82
+
83
+ ### Create an event with attributes and tags already applied
84
+
85
+ ```ruby
86
+ event = MISP::Event.new(
87
+ info: "my event",
88
+ Attribute: [
89
+ value: "8.8.8.8",
90
+ type: "ip-dst",
91
+ Tag: [
92
+ { name: "my attribute-level tag" }
93
+ ]
94
+ ],
95
+ Tag: [
96
+ { name: "my event-level tag" }
97
+ ]
98
+ )
99
+ event.create
100
+ # or
101
+ event = MISP::Event.new(info: "my event")
102
+
103
+ attribute = MISP::Attribute.new(value: "8.8.8.8", type: "ip-dst")
104
+ attribute.tags << MISP::Tag.new(name: "my attribute-level tag")
105
+
106
+ event.attributes << attribute
107
+ event.tags << MISP::Tag.new(name: "my event-level tag")
108
+
109
+ event.create
110
+ ```
111
+
112
+ ## Acknowledgement
113
+
114
+ The implementation design of this gem is highly influenced by [FloatingGhost/mispex](https://github.com/FloatingGhost/mispex).
115
+
116
+ ## License
117
+
118
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "misp/version"
4
+
5
+ require "misp/configuration"
6
+
7
+ require "misp/base"
8
+
9
+ require "misp/org"
10
+ require "misp/orgc"
11
+
12
+ require "misp/feed"
13
+ require "misp/server"
14
+
15
+ require "misp/sharing_group_org"
16
+ require "misp/sharing_group_server"
17
+ require "misp/sharing_group"
18
+
19
+ require "misp/galaxy_cluster"
20
+ require "misp/galaxy"
21
+
22
+ require "misp/tag"
23
+
24
+ require "misp/attribute"
25
+
26
+ require "misp/event"
27
+
28
+ module MISP
29
+ class Error < StandardError; end
30
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MISP
4
+ class Attribute < Base
5
+ attr_reader :id
6
+ attr_accessor :type
7
+ attr_accessor :category
8
+ attr_accessor :to_ids
9
+ attr_reader :uuid
10
+ attr_reader :event_id
11
+ attr_accessor :distribution
12
+ attr_accessor :timestamp
13
+ attr_accessor :comment
14
+ attr_accessor :sharing_group_id
15
+ attr_accessor :deleted
16
+ attr_accessor :disable_correlation
17
+ attr_accessor :value
18
+ attr_accessor :data
19
+
20
+ attr_accessor :sharing_groups
21
+ attr_accessor :shadow_attributes
22
+ attr_accessor :tags
23
+
24
+ def initialize(**attributes)
25
+ attributes = normalize_attributes(attributes)
26
+
27
+ @id = attributes.dig(:id)
28
+ @type = attributes.dig(:type)
29
+ @category = attributes.dig(:category)
30
+ @to_ids = attributes.dig(:to_ids)
31
+ @uuid = attributes.dig(:uuid)
32
+ @event_id = attributes.dig(:event_id)
33
+ @distribution = attributes.dig(:distribution)
34
+ @timestamp = attributes.dig(:timestamp)
35
+ @comment = attributes.dig(:comment)
36
+ @sharing_group_id = attributes.dig(:sharing_group_id)
37
+ @deleted = attributes.dig(:deleted)
38
+ @disable_correlation = attributes.dig(:disable_correlation)
39
+ @value = attributes.dig(:value)
40
+ @data = attributes.dig(:data)
41
+
42
+ @sharing_groups = build_plural_attribute(items: attributes.dig(:SharingGroup), klass: SharingGroup)
43
+ @shadow_attributes = build_plural_attribute(items: attributes.dig(:ShadowAttribute), klass: Attribute )
44
+ @tags = build_plural_attribute(items: attributes.dig(:Tag), klass: Tag)
45
+ end
46
+
47
+ def to_h
48
+ {
49
+ id: id,
50
+ type: type,
51
+ category: category,
52
+ to_ids: to_ids,
53
+ uuid: uuid,
54
+ event_id: event_id,
55
+ distribution: distribution,
56
+ timestamp: timestamp,
57
+ comment: comment,
58
+ sharing_group_id: sharing_group_id,
59
+ deleted: deleted,
60
+ disable_correlation: disable_correlation,
61
+ value: value,
62
+ data: data,
63
+ SharingGroup: sharing_groups.map(&:to_h),
64
+ ShadowAttribute: shadow_attributes.map(&:to_h),
65
+ Tag: tags.map(&:to_h)
66
+ }.compact
67
+ end
68
+
69
+ def get
70
+ _get("/attributes/#{id}") { |attribute| Attribute.new symbolize_keys(attribute) }
71
+ end
72
+
73
+ def self.get(id)
74
+ new(id: id).get
75
+ end
76
+
77
+ def delete
78
+ _post("/attributes/delete/#{id}") { |json| json }
79
+ end
80
+
81
+ def self.delete(id)
82
+ new(id: id).delete
83
+ end
84
+
85
+ def create(event_id)
86
+ _post("/attributes/add/#{event_id}", wrap(to_h)) { |attribute| Attribute.new symbolize_keys(attribute) }
87
+ end
88
+
89
+ def self.create(event_id, **attributes)
90
+ new(attributes).create(event_id)
91
+ end
92
+
93
+ def update(**attrs)
94
+ payload = to_h.merge(attrs)
95
+ payload[:timestamp] = nil
96
+ _post("/attributes/edit/#{id}", wrap(payload)) { |json| Attribute.new symbolize_keys(json.dig("response", "Attribute")) }
97
+ end
98
+
99
+ def search(**params)
100
+ base = {
101
+ returnFormat: "json",
102
+ limit: "100",
103
+ page: "0"
104
+ }
105
+
106
+ _post("/attributes/restSearch", base.merge(params)) do |json|
107
+ attributes = json.dig("response", "Attribute") || []
108
+ attributes.map { |attribute| Attribute.new symbolize_keys(attribute) }
109
+ end
110
+ end
111
+
112
+ def self.search(**params)
113
+ new.search params
114
+ end
115
+
116
+ def add_tag(tag)
117
+ tag = Tag.new(symbolize_keys(tag)) unless tag.is_a?(MISP::Tag)
118
+ payload = { uuid: uuid, tag: tag.name }
119
+ _post("/tags/attachTagToObject", payload) { |json| Tag.new symbolize_keys(json) }
120
+ end
121
+
122
+ def remove_tag(tag)
123
+ tag = Tag.new(symbolize_keys(tag)) unless tag.is_a?(MISP::Tag)
124
+ payload = { uuid: uuid, tag: tag.name }
125
+ _post("/tags/removeTagFromObject", payload) { |json| json }
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/https"
5
+ require "uri"
6
+
7
+ module MISP
8
+ class Base
9
+ def api_endpoint
10
+ @api_endpoint ||= URI(MISP.configuration.api_endpoint)
11
+ end
12
+
13
+ def api_key
14
+ @api_key ||= MISP.configuration.api_key
15
+ end
16
+
17
+ private
18
+
19
+ def build_attribute(item:, klass:)
20
+ return nil unless item
21
+
22
+ klass.new symbolize_keys(item)
23
+ end
24
+
25
+ def build_plural_attribute(items:, klass:)
26
+ (items || []).map do |item|
27
+ klass.new symbolize_keys(item)
28
+ end
29
+ end
30
+
31
+ def symbolize_keys(hash)
32
+ hash.map { |k, v| [k.to_sym, v] }.to_h
33
+ end
34
+
35
+ def class_name
36
+ self.class.to_s.split("::").last.to_s
37
+ end
38
+
39
+ def normalize_attributes(attributes)
40
+ klass = class_name.to_sym
41
+
42
+ attributes.key?(klass) ? symbolize_keys(attributes.dig(klass)) : attributes
43
+ end
44
+
45
+ def wrap(params)
46
+ klass = class_name.to_sym
47
+ return params if params.key?(klass)
48
+
49
+ [[klass, params]].to_h
50
+ end
51
+
52
+ def hostname
53
+ @hostname ||= api_endpoint.hostname
54
+ end
55
+
56
+ def port
57
+ @port ||= api_endpoint.port
58
+ end
59
+
60
+ def scheme
61
+ @scheme ||= api_endpoint.scheme
62
+ end
63
+
64
+ def base_url
65
+ "#{scheme}://#{hostname}:#{port}"
66
+ end
67
+
68
+ def url_for(path)
69
+ URI(base_url + path)
70
+ end
71
+
72
+ def https_options
73
+ return nil if scheme != "https"
74
+
75
+ if proxy = ENV["HTTPS_PROXY"] || ENV["https_proxy"]
76
+ uri = URI(proxy)
77
+ {
78
+ proxy_address: uri.hostname,
79
+ proxy_port: uri.port,
80
+ proxy_from_env: false,
81
+ use_ssl: true,
82
+ }
83
+ else
84
+ { use_ssl: true }
85
+ end
86
+ end
87
+
88
+ def http_options
89
+ if proxy = ENV["HTTP_PROXY"] || ENV["http_proxy"]
90
+ uri = URI(proxy)
91
+ {
92
+ proxy_address: uri.hostname,
93
+ proxy_port: uri.port,
94
+ proxy_from_env: false,
95
+ }
96
+ else
97
+ {}
98
+ end
99
+ end
100
+
101
+ def parse_body(body)
102
+ JSON.parse body.to_s
103
+ rescue JSON::ParserError => _e
104
+ body.to_s
105
+ end
106
+
107
+ def request(req)
108
+ Net::HTTP.start(hostname, port, https_options || http_options) do |http|
109
+ req.initialize_http_header default_headers
110
+
111
+ response = http.request(req)
112
+ json = parse_body(response.body)
113
+
114
+ code = response.code
115
+ unless code.start_with? "20"
116
+ raise Error, "Unsupported response code returned: #{code} (#{json})"
117
+ end
118
+
119
+ yield json
120
+ end
121
+ end
122
+
123
+ def default_headers
124
+ {
125
+ "Content-Type": "application/json",
126
+ "Accept": "application/json",
127
+ "Authorization": api_key
128
+ }
129
+ end
130
+
131
+ def _get(path, params = {}, &block)
132
+ url = url_for(path)
133
+ url.query = URI.encode_www_form(params) unless params.empty?
134
+
135
+ get = Net::HTTP::Get.new(url)
136
+ request(get, &block)
137
+ end
138
+
139
+ def _post(path, params = {}, &block)
140
+ url = url_for(path)
141
+
142
+ post = Net::HTTP::Post.new(url)
143
+ post.body = params.is_a?(Hash) ? params.to_json : params.to_s
144
+
145
+ request(post, &block)
146
+ end
147
+
148
+ def _delete(path, params = {}, &block)
149
+ url = url_for(path)
150
+ url.query = URI.encode_www_form(params) unless params.empty?
151
+
152
+ delete = Net::HTTP::Delete.new(url)
153
+ request(delete, &block)
154
+ end
155
+ end
156
+ end