Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ Or install it yourself as:
You can access all attributes on responses thanks to the method_missing function in Ruby. We only munged the attributes that don't correspond to snake_case. If you want to see a list of all available attributes for a serializable response object, simply do something like this:

```
client = Threatstack::Client.new(API_TOKEN, organization_id: ORG_ID)
[threatstack] main> ts.alerts.first.attrs
client = Threatstack::Client.new(ORG_ID, USER_ID, API_KEY)
[threatstack] main> ts.alerts("active").first.attrs
=> [:id,
:title,
:type,
Expand All @@ -48,9 +48,9 @@ client = Threatstack::Client.new(API_TOKEN, organization_id: ORG_ID)
### Alerts

```
client = Threatstack::Client.new(API_TOKEN, organization_id: ORG_ID)
client = Threatstack::Client.new(ORG_ID, USER_ID, API_KEY)
## All these are optional url params. See the Threatstack API Docs
alert = client.alerts(start: 3.days.ago, end: Time.now, count: 5).last
alert = client.alerts("active", start: 3.days.ago, end: Time.now, count: 5).last
=> #<Threatstack::Alert:0x007fde0b01cbd8
@raw=
{"created_at"=>1496850520000,
Expand All @@ -66,7 +66,7 @@ count = alert.count
You can also limit the response if that's important to you:

```
client.alerts(fields: ['title', 'alerts'])
client.alerts("active", fields: ['title', 'alerts'])
=> [#<Threatstack::Alert:0x007fd61348c768
@raw={"title"=>"CloudTrail Activity (IAM Policy Changes) : CreateAccessKey by ryan_canty", "severity"=>2}>]
```
Expand All @@ -80,7 +80,7 @@ client.alert('1234567890')
### Agents

```
client.agents
client.agents('online')
=> [#<Threatstack::Response:0x007fa262b0b2e0 @raw={...}> ]
client.agent('123123123')
=> #<Threatstack::Agent:0x007fa262b0b2e0 @raw={...}>
Expand Down
149 changes: 92 additions & 57 deletions lib/threatstack/client.rb
Original file line number Diff line number Diff line change
@@ -1,55 +1,59 @@
require 'open-uri'
require 'httparty'
require 'hawk'
require 'threatstack/response'
require 'threatstack/entities/agent'
require 'threatstack/entities/alert'
require 'threatstack/entities/ruleset'
require 'threatstack/entities/rule'


module Threatstack
class ThreatstackError < StandardError; end

class Client
THREATSTACK_API = 'https://api.threatstack.com'.freeze
attr_reader :token, :org_id, :api_version, :last_pagination_token
THREATSTACK_API = 'api.threatstack.com'.freeze
PORT = 443
attr_reader :org_id, :user_id, :api_key, :api_version, :last_pagination_token

def initialize(token, organization_id: nil, api_version: 'v2')
@api_version = api_version
@token = token
def initialize(organization_id, user_id, api_key, api_version: 'v2')
@org_id = organization_id
@user_id = user_id
@api_key = api_key
@api_version = api_version
if api_version == 'v1'
raise ThreatstackError, "This version of threatstack-ruby does not support Threatstack API v1"
end
end

### ALERTS ###

def agents(params = {})
### AGENTS ###
def agents(status, params = {})
if !valid_agent_status?(status)
raise ThreatstackError, "Must specify status with value of 'online' or 'offline'"
end
params[:status] = status
response = do_request(:get, 'agents', params)
Response.new(response['agents'], self, entity: :agent).agents
end

def agent(agent_id, params = {})
def agent(agent_id)
raise ThreatstackError, "Must specify agent id" unless agent_id
response = do_request(:get, "agents/#{agent_id}", params)
response = do_request(:get, "agents/#{agent_id}")
Agent.new(response, self)
end

### ALERTS ###
def alerts(params = {})
def alerts(status, params = {})
if !valid_alert_status?(status)
raise ThreatstackError, "Must specify status with value of 'active' or 'dismissed'"
end
params[:status] = status
response = do_request(:get, 'alerts', params)
Response.new(response['alerts'], self, entity: :alert).alerts
end

def dismissed_alerts(params = {})
response = do_request(:get, 'alerts/dismissed', params)
Response.new(response['alerts'], self, entity: :alert).alerts
end

def alert(alert_id, params = {})
raise ThreatstackError, "Must specify alert id" unless alert_id
response = do_request(:get, "alerts/#{alert_id}", params)
response = do_request(:get, "alerts/#{alert_id}")
Alert.new(response, self)
end

Expand All @@ -58,48 +62,31 @@ def severity_counts(params = {})
Response.new(response['severityCounts'], self, entity: :severity_count).list
end

def event(alert_id, event_id, params = {})
response = do_request(:get, "alerts/#{alert_id}/events/#{event_id}", params)
GenericObject.new(response['details'], self, entity: :event)
def events(alert_id)
response = do_request(:get, "alerts/#{alert_id}/events")
Response.new(response['events'], self, entity: :events).list
end

### CVEs ###

def vulnerabilities(params = {})
uri = "vulnerabilities"
uri += "/suppressed" if params[:suppressed]
response = do_request(:get, uri, params)
Response.new(response['cves'], self, entity: :cve).cves
end

def vulnerability(vuln_id, params = {})
raise ThreatstackError, "Must specify vulnerability id" unless vuln_id
response = do_request(:get, "vulnerabilities/#{vuln_id}", params)
Cve.new(response, self)
end

def package_vulnerabilities(package, params = {})
raise ThreatstackError, "Must specify package" unless package
uri = "vulnerabilities/package/#{package}"
uri += "/suppressed" if params[:suppressed]
response = do_request(:get, uri, params)
Response.new(response['packages'], self, entity: :package).list
end

def server_vulnerabilities(server, params = {})
raise ThreatstackError, "Must specify server" unless server
uri = "vulnerabilities/server/#{server}"
uri += "/suppressed" if params[:suppressed]
response = do_request(:get, uri, params)
response['cves']
end

def cves_by_agent(agent, params = {})
raise ThreatstackError, "Must specify agent" unless agent
uri = "vulnerabilities/agent/#{agent}"
uri += "/suppressed" if params[:suppressed]
response = do_request(:get, uri, params)
response['cves']
def affected_servers(cve)
raise ThreatstackError, "Must specify a cve" unless cve
uri = "vulnerabilities/#{cve}/servers"
response = do_request(:get, uri)
response['servers']
end

def vulnerability_suppressions(params = {})
Expand All @@ -114,12 +101,18 @@ def rulesets(params = {})
Response.new(response['rulesets'], self, entity: :ruleset).rulesets
end

def ruleset(ruleset_id, params = {})
def ruleset(ruleset_id)
raise ThreatstackError, "Must specify ruleset id" unless ruleset_id
response = do_request(:get, "rulesets/#{ruleset_id}", params)
response = do_request(:get, "rulesets/#{ruleset_id}")
Ruleset.new(response, self)
end

def agents_for_ruleset(ruleset_id)
raise ThreatstackError, "Must specify ruleset id" unless ruleset_id
response = do_request(:get, "rulesets/#{ruleset_id}")
Response.new(response['agents'], self, entity: :agent).agents
end

### Rules ###

def rules(ruleset_id, params = {})
Expand All @@ -133,36 +126,78 @@ def rule(ruleset_id, rule_id, params = {})
Rule.new(response, self)
end

### Servers ###
### EC2 Instances ###

def servers(monitored = true, params = {})
uri = "servers"
uri += "/non-monitored" unless monitored
def instances(monitored = nil)
uri = "aws/ec2"
params = monitored ? { isMonitored: monitored } : {}
response = do_request(:get, uri, params)
Response.new(response['servers'], self, entity: :server).list
end

private

def valid_agent_status?(status)
status && (status == 'online' || status == 'offline')
end

def valid_alert_status?(status)
status && (status == 'active' || status == 'dismissed')
end

def do_request(method, path, params = {})
headers = { "Authorization" => token, "Organization-Id" => org_id }
response = HTTParty.public_send(method, build_uri(path, params), headers: headers).parsed_response
if response.instance_of?(Hash) && response['status'] == 'error'
raise ThreatstackError, response['message']
convert_dates(params)
uri = build_uri(path, params, "https://#{THREATSTACK_API}:#{PORT}")
auth_info_uri = build_uri(path, params)
auth_token = calculate_auth_info(method, auth_info_uri)
headers = { "Authorization" => Hawk::Client.build_authorization_header(auth_token) }

response = HTTParty.public_send(method, uri, headers: headers)
if !response.success?
raise ThreatstackError, "Response returned with status #{response.code} with message #{response.message}."
end

response_auth_header = response.headers['Server-Authorization']

auth_token[:payload] = response.body
auth_token[:content_type] = "application/json"

auth_result = Hawk::Client.authenticate(response_auth_header, auth_token)
if auth_result['id'] != auth_token[:credentials]['id']
raise ThreatstackError, "Response was not authentic"
end

@last_pagination_token = response['token']
response
end

def build_uri(path, params = {})
params[:from] = params[:from].utc if params[:from]
params[:until] = params[:until].utc if params[:until]
def convert_dates(params)
params[:from] = params[:from].utc.iso8601 if params[:from]
params[:until] = params[:until].utc.iso8601 if params[:until]
params[:fields] = params[:fields].join(',') if params[:fields]&.is_a?(Array)
end

def build_uri(path, params = {}, root = '')
query = params.each_pair.map { |k, v| "#{k}=#{v}" }.join('&')
uri = "#{THREATSTACK_API}/#{api_version}/#{path}"
uri = "#{root}/#{api_version}/#{path}"
uri += "?#{URI::encode(query)}" if params.any?
uri
end

def calculate_auth_info(method, request_uri)
ts = Time.now.to_i
nonce = SecureRandom.hex(4)
credentials = { :id => @user_id, :key => @api_key, :algorithm => 'sha256' }
{
:credentials => credentials,
:method => method.to_s.upcase,
:request_uri => request_uri,
:host => THREATSTACK_API,
:ext => @org_id,
:port => PORT,
:nonce => nonce,
:ts => ts
}
end
end
end
10 changes: 1 addition & 9 deletions lib/threatstack/entities/agent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,10 @@ module Threatstack
class Agent
include Serializable
attributes :id, :instance_id, :status, :activated_at, :last_reported_at,
:version, :name, :description, :hostname, :tags, :agent_type
:version, :name, :description, :hostname, :tags, :agent_type, :kernel

def tags
raw['tags'].map{ |t| Tag.new(t) }
end

def ruleset_ids
raw['rulesets']
end

def rulesets
ruleset_ids.each { |id| client.ruleset(id) }
end
end
end
2 changes: 1 addition & 1 deletion lib/threatstack/entities/alert.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def ruleset
end

def events
event_ids&.map{ |event_id| client.event(id, event_id)}
client.events(id)
end
end
end
Loading