Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce Jbuilder for webhook #985

Merged
merged 1 commit into from
Jan 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
33 changes: 14 additions & 19 deletions docs/emitters/webhook.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ template: ...

### Template

`template` (`string`) is an [ERB](https://github.com/ruby/erb) template to customize the payload to sent. A template should generate a valid JSON.
`template` (`string`) is a [Jbuilder](https://github.com/rails/jbuilder) template string (or a path to a Jbuilder template file) to customize JSON payload to send.

You can use the following parameters inside an ERB template.
You can use the following attributes inside a JBuilder template.

- `rule`: a rule
- `artifacts`: a list of artifacts
- `rule`: a rule (= `Mihari::Rule`)
- `artifacts`: a list of artifacts (= `Array<Mihari::Models::Artifact>`)

## Examples

Expand All @@ -42,22 +42,17 @@ You can use the following parameters inside an ERB template.
url: https://threatfox-api.abuse.ch/api/v1/
headers:
api-key: YOUR_API_KEY
template: threatfox.erb
template: /path/to/threatfox.json.jbuilder
```

**threatfox.json.jbuilder**

```ruby
{
"query": "submit_ioc",
"threat_type": "payload_delivery",
"ioc_type": "ip:port",
"malware": "foobar",
"confidence_level": 100,
"anonymous": 0,
"iocs": [
<% @artifacts.select { |artifact| artifact.data_type == "ip" }.each_with_index do |artifact, idx| %>
"<%= artifact.data %>:80"
<%= ',' if idx < (@artifacts.length - 1) %>
<% end %>
]
}
json.query "submit_ioc"
json.threat_type "payload_delivery"
json.ioc_type "domain"
json.malware "foobar"
json.confidence_level 100
json.anonymous 0
json.iocs artifacts.map(&:data)
```
85 changes: 31 additions & 54 deletions lib/mihari/emitters/webhook.rb
Original file line number Diff line number Diff line change
@@ -1,49 +1,9 @@
# frozen_string_literal: true

require "erb"
require "tilt/jbuilder"

module Mihari
module Emitters
class ERBTemplate < ERB
class << self
def template
%{
{
"rule": {
"id": "<%= @rule.id %>",
"title": "<%= @rule.title %>",
"description": "<%= @rule.description %>"
},
"artifacts": [
<% @artifacts.each_with_index do |artifact, idx| %>
"<%= artifact.data %>"
<%= ',' if idx < (@artifacts.length - 1) %>
<% end %>
],
"tags": [
<% @rule.tags.each_with_index do |tag, idx| %>
"<%= tag.name %>"
<%= ',' if idx < (@rule.tags.length - 1) %>
<% end %>
]
}
}
end
end

def initialize(artifacts:, rule:, options: {})
@artifacts = artifacts
@rule = rule

@template = options.fetch(:template, self.class.template)
super(@template)
end

def result
super(binding)
end
end

class Webhook < Base
# @return [Addressable::URI, nil]
attr_reader :url
Expand All @@ -54,12 +14,21 @@ class Webhook < Base
# @return [String]
attr_reader :method

# @return [String, nil]
attr_reader :template

# @return [Array<Mihari::Models::Artifact>]
attr_accessor :artifacts

DEFAULT_TEMPLATE = %{
json.rule do
json.id rule.id
json.title rule.title
json.description rule.description
end

json.artifacts artifacts.map(&:data)

json.tags rule.tags.map(&:name)
}

#
# @param [Mihari::Rule] rule
# @param [Hash, nil] options
Expand All @@ -71,7 +40,7 @@ def initialize(rule:, options: nil, **params)
@url = Addressable::URI.parse(params[:url])
@headers = params[:headers] || {}
@method = params[:method] || "POST"
@template = params[:template]
@template = params[:template] || DEFAULT_TEMPLATE

@artifacts = []
end
Expand Down Expand Up @@ -108,21 +77,29 @@ def http
HTTP::Factory.build headers: headers, timeout: timeout
end

#
# @return [String]
#
def template_string
return File.read(@template) if Pathname(@template).exist?

@template
end

#
# @return [Tilt::JbuilderTemplate]
#
def template
Tilt::JbuilderTemplate.new { template_string }
end

#
# Render template
#
# @return [String]
#
def render
options = {}
options[:template] = File.read(template) unless template.nil?

erb_template = ERBTemplate.new(
artifacts: artifacts,
rule: rule,
options: options
)
erb_template.result
template.render(nil, rule: rule, artifacts: artifacts)
end

#
Expand Down
3 changes: 3 additions & 0 deletions mihari.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Gem::Specification.new do |spec|
spec.add_dependency "grape-swagger", "2.0.1"
spec.add_dependency "grape-swagger-entity", "0.5.2"
spec.add_dependency "http", "5.1.1"
spec.add_dependency "jbuilder", "2.11.5"
spec.add_dependency "jr-cli", "0.6.0"
spec.add_dependency "launchy", "2.5.2"
spec.add_dependency "memo_wise", "1.8.0"
Expand All @@ -109,6 +110,8 @@ Gem::Specification.new do |spec|
spec.add_dependency "sqlite3", "~> 1.7"
spec.add_dependency "thor", "1.3.0"
spec.add_dependency "thor-hollaback", "0.2.1"
spec.add_dependency "tilt", "2.3.0"
spec.add_dependency "tilt-jbuilder", "0.7.1"
spec.add_dependency "uuidtools", "2.2.0"
spec.add_dependency "whois", "5.1.1"
spec.add_dependency "whois-parser", "2.0.0"
Expand Down
37 changes: 28 additions & 9 deletions spec/emitters/webhook_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,10 @@
include_context "with fake HTTPBin"

let!(:url) { "#{server.base_url}/post" }
let!(:artifacts) do
[
Mihari::Models::Artifact.new(data: "1.1.1.1"),
Mihari::Models::Artifact.new(data: "github.com")
]
end

let_it_be(:rule) { Mihari::Rule.from_model FactoryBot.create(:rule) }
let_it_be(:artifact) { FactoryBot.create(:artifact) }
let_it_be(:artifacts) { [artifact] }
let_it_be(:rule) { Mihari::Rule.from_model artifact.rule }

describe "#configured?" do
context "without URL" do
Expand All @@ -32,14 +28,37 @@
end

describe "#call" do
subject(:emitter) { described_class.new(rule: rule, url: url, headers: { "Content-Type": "application/json" }) }
subject(:emitter) do
described_class.new(
rule: rule,
url: url,
headers: { "Content-Type": "application/json" }
)
end

it do
res = emitter.call artifacts
res = emitter.call [artifact]
json = JSON.parse(res)["json"]
expect(json["rule"]["id"]).to eq(rule.id)
expect(json["artifacts"]).to eq(artifacts.map(&:data))
expect(json["tags"]).to eq(rule.tags.map(&:name))
end

context "with a template file" do
subject(:emitter) do
described_class.new(
rule: rule,
url: url,
template: "spec/fixtures/templates/test.json.jbuilder",
headers: { "Content-Type": "application/json" }
)
end

it do
res = emitter.call [artifact]
json = JSON.parse(res)["json"]
expect(json["id"]).to eq(rule.id)
end
end
end
end
1 change: 1 addition & 0 deletions spec/fixtures/templates/test.json.jbuilder
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
json.id rule.id
7 changes: 7 additions & 0 deletions test.json.jbuilder
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
json.query "submit_ioc"
json.threat_type "payload_delivery"
json.ioc_type "domain"
json.malware "foobar"
json.confidence_level 100
json.anonymous 0
json.iocs artifacts.map(&:data)