From b8dbd9eb442e9ba820a82f437f791a7ea2129001 Mon Sep 17 00:00:00 2001 From: Kamna Kohli Date: Wed, 12 Mar 2014 12:44:40 -0700 Subject: [PATCH] Add in support for tags --- lib/nexpose.rb | 2 + lib/nexpose/ajax.rb | 31 +++- lib/nexpose/dag.rb | 2 +- lib/nexpose/filter.rb | 15 ++ lib/nexpose/group.rb | 12 +- lib/nexpose/site.rb | 13 ++ lib/nexpose/tag.rb | 345 ++++++++++++++++++++++++++++++++++++ lib/nexpose/tag/criteria.rb | 45 +++++ 8 files changed, 457 insertions(+), 8 deletions(-) create mode 100644 lib/nexpose/tag.rb create mode 100644 lib/nexpose/tag/criteria.rb diff --git a/lib/nexpose.rb b/lib/nexpose.rb index 59aa21cf..2fe7dcc0 100644 --- a/lib/nexpose.rb +++ b/lib/nexpose.rb @@ -79,6 +79,8 @@ require 'nexpose/scan_template' require 'nexpose/silo' require 'nexpose/site' +require 'nexpose/tag' +require 'nexpose/tag/criteria' require 'nexpose/ticket' require 'nexpose/user' require 'nexpose/vuln' diff --git a/lib/nexpose/ajax.rb b/lib/nexpose/ajax.rb index c4fc3f8c..8bd616f4 100644 --- a/lib/nexpose/ajax.rb +++ b/lib/nexpose/ajax.rb @@ -10,6 +10,12 @@ module Nexpose module AJAX module_function + module CONTENT_TYPE + XML = 'text/xml; charset=UTF-8' + JSON = 'application/json; charset-utf-8' + FORM = 'application/x-www-form-urlencoded; charset=UTF-8' + end + # GET call to a Nexpose controller. # # @param [Connection] nsc API connection to a Nexpose console. @@ -17,7 +23,7 @@ module AJAX # @param [String] content_type Content type to use when issuing the GET. # @return [String|REXML::Document|Hash] The response from the call. # - def get(nsc, uri, content_type = 'text/xml; charset=UTF-8') + def get(nsc, uri, content_type = CONTENT_TYPE::XML) get = Net::HTTP::Get.new(uri) get.set_content_type(content_type) _request(nsc, get) @@ -31,7 +37,7 @@ def get(nsc, uri, content_type = 'text/xml; charset=UTF-8') # @param [String] content_type Content type to use when issuing the PUT. # @return [String] The response from the call. # - def put(nsc, uri, payload = nil, content_type = 'text/xml; charset=UTF-8') + def put(nsc, uri, payload = nil, content_type = CONTENT_TYPE::XML) put = Net::HTTP::Put.new(uri) put.set_content_type(content_type) put.body = payload.to_s if payload @@ -46,13 +52,28 @@ def put(nsc, uri, payload = nil, content_type = 'text/xml; charset=UTF-8') # @param [String] content_type Content type to use when issuing the POST. # @return [String|REXML::Document|Hash] The response from the call. # - def post(nsc, uri, payload = nil, content_type = 'text/xml') + def post(nsc, uri, payload = nil, content_type = CONTENT_TYPE::XML) post = Net::HTTP::Post.new(uri) post.set_content_type(content_type) post.body = payload.to_s if payload _request(nsc, post) end + # PATCH call to a Nexpose controller. + # + # @param [Connection] nsc API connection to a Nexpose console. + # @param [String] uri Controller address relative to https://host:port + # @param [String|REXML::Document] payload XML document required by the call. + # @param [String] content_type Content type to use when issuing the PATCH. + # @return [String] The response from the call. + # + def patch(nsc, uri, payload = nil, content_type = CONTENT_TYPE::XML) + patch = Net::HTTP::Patch.new(uri) + patch.set_content_type(content_type) + patch.body = payload.to_s if payload + _request(nsc, patch) + end + # POST call to a Nexpose controller that uses a form-post model. # This is here to support legacy use of POST in old controllers. # @@ -63,7 +84,7 @@ def post(nsc, uri, payload = nil, content_type = 'text/xml') # @param [String] content_type Content type to use when issuing the POST. # @return [Hash] The parsed JSON response from the call. # - def form_post(nsc, uri, parameters, content_type = 'application/x-www-form-urlencoded; charset=UTF-8') + def form_post(nsc, uri, parameters, content_type = CONTENT_TYPE::FORM) post = Net::HTTP::Post.new(uri) post.set_content_type(content_type) post.set_form_data(parameters) @@ -75,7 +96,7 @@ def form_post(nsc, uri, parameters, content_type = 'application/x-www-form-urlen # @param [Connection] nsc API connection to a Nexpose console. # @param [String] uri Controller address relative to https://host:port # @param [String] content_type Content type to use when issuing the DELETE. - def delete(nsc, uri, content_type = 'text/xml') + def delete(nsc, uri, content_type = CONTENT_TYPE::XML) delete = Net::HTTP::Delete.new(uri) delete.set_content_type(content_type) _request(nsc, delete) diff --git a/lib/nexpose/dag.rb b/lib/nexpose/dag.rb index ccb1c6be..4ce5c56d 100644 --- a/lib/nexpose/dag.rb +++ b/lib/nexpose/dag.rb @@ -33,7 +33,7 @@ def save(nsc) @users.reject! { |id| admins.member? id } params = @id ? { 'entityid' => @id, 'mode' => 'edit' } : { 'entityid' => false, 'mode' => false } uri = AJAX.parametrize_uri('/data/assetGroup/saveAssetGroup', params) - data = JSON.parse(AJAX.post(nsc, uri, _to_entity_details, 'application/json; charset-utf-8')) + data = JSON.parse(AJAX.post(nsc, uri, _to_entity_details, AJAX::CONTENT_TYPE::JSON)) data['response'] == 'success.' end diff --git a/lib/nexpose/filter.rb b/lib/nexpose/filter.rb index 16e734dc..af96a2e8 100644 --- a/lib/nexpose/filter.rb +++ b/lib/nexpose/filter.rb @@ -134,6 +134,19 @@ module Field # Valid Operators: CONTAINS, NOT_CONTAINS SOFTWARE = 'SOFTWARE' + # Valid Operators: IS, IS_NOT, GREATER_THAN, LESS_THAN, IS_APPLIED, IS_NOT_APPLIED + # Valid Values: VERY_HIGH, HIGH, NORMAL, LOW, VERY_LOW + USER_ADDED_CRITICALITY_LEVEL = 'TAG_CRITICALITY' + + # Valid Operators: IS, IS_NOT, STARTS_WITH, ENDS_WITH, IS_APPLIED, IS_NOT_APPLIED, CONTAINS, NOT_CONTAINS + USER_ADDED_CUSTOM_TAG = 'TAG' + + # Valid Operators: IS, IS_NOT, STARTS_WITH, ENDS_WITH, IS_APPLIED, IS_NOT_APPLIED, CONTAINS, NOT_CONTAINS + USER_ADDED_TAG_LOCATION = 'TAG_LOCATION' + + # Valid Operators: IS, IS_NOT, STARTS_WITH, ENDS_WITH, IS_APPLIED, IS_NOT_APPLIED, CONTAINS, NOT_CONTAINS + USER_ADDED_TAG_OWNER = 'TAG_OWNER' + # Valid Operators: ARE # Valid Values: PRESENT, NOT_PRESENT VALIDATED_VULNERABILITIES = 'VULNERABILITY_VALIDATED_STATUS' @@ -171,6 +184,8 @@ module Operator IS_NOT_EMPTY = 'IS_NOT_EMPTY' INCLUDE = 'INCLUDE' DO_NOT_INCLUDE = 'DO_NOT_INCLUDE' + IS_APPLIED = 'IS_APPLIED' + IS_NOT_APPLIED = 'IS_NOT_APPLIED' end # Specialized values used by certain search fields diff --git a/lib/nexpose/group.rb b/lib/nexpose/group.rb index f9e37a6a..2e27ba5d 100644 --- a/lib/nexpose/group.rb +++ b/lib/nexpose/group.rb @@ -63,7 +63,7 @@ def delete(connection) class AssetGroup < AssetGroupSummary include Sanitize - attr_accessor :name, :description, :id + attr_accessor :name, :description, :id , :tags # Array[Device] of devices associated with this asset group. attr_accessor :assets @@ -73,6 +73,7 @@ class AssetGroup < AssetGroupSummary def initialize(name, desc, id = -1, risk = 0.0) @name, @description, @id, @risk_score = name, desc, id, risk @assets = [] + @tags = [] end def save(connection) @@ -98,6 +99,11 @@ def to_xml xml << %() end xml << '' + xml << '' + @tags.each do |tag| + xml << tag.as_xml.to_s + end + xml << '' xml << '' end @@ -133,7 +139,6 @@ def self.load(connection, id) def self.parse(xml) return nil unless xml - group = REXML::XPath.first(xml, 'AssetGroupConfigResponse/AssetGroup') asset_group = new(group.attributes['name'], group.attributes['description'], @@ -146,6 +151,9 @@ def self.parse(xml) dev.attributes['riskfactor'].to_f, dev.attributes['riskscore'].to_f) end + group.elements.each('Tags/Tag') do |tag| + asset_group.tags << TagSummary.parse_xml(tag) + end asset_group end end diff --git a/lib/nexpose/site.rb b/lib/nexpose/site.rb index 597898c7..b43bed40 100644 --- a/lib/nexpose/site.rb +++ b/lib/nexpose/site.rb @@ -124,6 +124,9 @@ class Site # Modifying their behavior through the API is not recommended. attr_accessor :is_dynamic + # [Array[TagSummary]] Collection of TagSummary + attr_accessor :tags + # Site constructor. Both arguments are optional. # # @param [String] name Unique name of the site. @@ -142,6 +145,7 @@ def initialize(name = nil, scan_template = 'full-audit') @alerts = [] @exclude = [] @users = [] + @tags = [] end # Returns true when the site is dynamic. @@ -303,6 +307,11 @@ def as_xml elem.add_element(sched) xml.add_element(elem) + unless tags.empty? + tag_xml = xml.add_element(REXML::Element.new('Tags')) + @tags.each { |tag| tag_xml.add_element(tag.as_xml) } + end + xml end @@ -360,6 +369,10 @@ def self.parse(rexml) site.alerts << Alert.parse(alert) end + s.elements.each('Tags/Tag') do |tag| + site.tags << TagSummary.parse_xml(tag) + end + return site end nil diff --git a/lib/nexpose/tag.rb b/lib/nexpose/tag.rb new file mode 100644 index 00000000..2fe93c42 --- /dev/null +++ b/lib/nexpose/tag.rb @@ -0,0 +1,345 @@ +module Nexpose + module_function + + class Connection + # Lists all tags + # + # @return [Array[TagSummary]] List of current tags. + # + def list_tags + tag_summary = [] + tags = JSON.parse(AJAX.get(self, '/api/2.0/tags')) + tags['resources'].each do |json| + tag_summary << TagSummary.parse(json) + end + tag_summary + end + alias_method :tags, :list_tags + + # Deletes a tag by ID + # + # @param [Fixnum] tag_id ID of tag to delete + # + def delete_tag(tag_id) + AJAX.delete(self, "/api/2.0/tags/#{tag_id}") + end + + # Lists all the tags on an asset + # + # @param [Fixnum] asset_id of the asset to list the applied tags for + # @return [Array[TagSummary]] list of tags on asset + # + def list_asset_tags(asset_id) + tag_summary = [] + asset_tag = JSON.parse(AJAX.get(self, "/api/2.0/assets/#{asset_id}/tags")) + asset_tag['resources'].select { |r| r['asset_ids'].find { |i| i == asset_id } }.each do |json| + tag_summary << TagSummary.parse(json) + end + tag_summary + end + alias_method :asset_tags, :list_asset_tags + + # Removes a tag from an asset + # + # @param [Fixnum] asset_id on which to remove tag + # @param [Fixnum] tag_id to remove from asset + # + def remove_tag_from_asset(asset_id, tag_id) + AJAX.delete(self, "/api/2.0/assets/#{asset_id}/tags/#{tag_id}") + end + + # Lists all the tags on a site + # + # @param [Fixnum] site_id id of the site to get the applied tags + # @return [Array[TagSummary]] list of tags on site + # + def list_site_tags(site_id) + tag_summary = [] + site_tag = JSON.parse(AJAX.get(self, "/api/2.0/sites/#{site_id}/tags")) + site_tag['resources'].each do |json| + tag_summary << TagSummary.parse(json) + end + tag_summary + end + + # Removes a tag from a site + # + # @param [Fixnum] site_id id of the site on which to remove the tag + # @param [Fixnum] tag_id id of the tag to remove + # + def remove_tag_from_site(site_id, tag_id) + AJAX.delete(self, "/api/2.0/sites/#{site_id}/tags/#{tag_id}") + end + + # Lists all the tags on an asset_group + # + # @param [Fixnum] asset_group_id id of the group on which tags are listed + # @return [Array[TagSummary]] list of tags on asset group + # + def list_asset_group_tags(asset_group_id) + tag_summary = [] + asset_group_tag = JSON.parse(AJAX.get(self, "/api/2.0/asset_groups/#{asset_group_id}/tags")) + asset_group_tag['resources'].each do |json| + tag_summary << TagSummary.parse(json) + end + tag_summary + end + alias_method :group_tags, :list_asset_group_tags + alias_method :asset_group_tags, :list_asset_group_tags + + # Removes a tag from an asset_group + # + # @param [Fixnum] asset_group_id id of group on which to remove tag + # @param [Fixnum] tag_id of the tag to remove from asset group + # + def remove_tag_from_asset_group(asset_group_id, tag_id) + AJAX.delete(self, "/api/2.0/asset_groups/#{asset_group_id}/tags/#{tag_id}") + end + alias_method :remove_tag_from_group, :remove_tag_from_asset_group + + # Returns the criticality value which takes precedent for an asset + # + # @param [Fixnum] asset_id id of asset on which criticality tag is selected + # @return [String] selected_criticality string of the relevant criticality; nil if not tagged + # + def selected_criticality_tag(asset_id) + selected_criticality = AJAX.get(self, "/data/asset/#{asset_id}/selected-criticality-tag") + selected_criticality.empty? ? nil : JSON.parse(selected_criticality)['name'] + end + end + + # Summary value object for tag information + # + class TagSummary + + # ID of tag + attr_accessor :id + + # Name of tag + attr_accessor :name + + # One of Tag::Type::Generic + attr_accessor :type + + def initialize(name, type, id) + @name, @type, @id = name, type, id + end + + def self.parse(json) + new(json['tag_name'], json['tag_type'], json['tag_id']) + end + + def self.parse_xml(xml) + new(xml.attributes['name'], xml.attributes['type'], xml.attributes['id'].to_i) + end + + # XML representation of the tag summary as required by Site and AssetGroup + # + # @return [ELEMENT] XML element + + def as_xml + xml = REXML::Element.new('Tag') + xml.add_attribute('id', @id) + xml.add_attribute('name', @name) + xml.add_attribute('type', @type) + xml + end + end + + # Tag object containing tag details + # + class Tag < TagSummary + module Type + # Criticality tag types + module Level + VERY_HIGH = 'Very High' + HIGH = 'High' + MEDIUM = 'Medium' + LOW = 'Low' + VERY_LOW = 'Very Low' + end + + # Tag types + module Generic + GENERAL = 'GENERAL' + OWNER = 'OWNER' + LOCATION = 'LOCATION' + CRITICALITY = 'CRITICALITY' + end + end + + DEFAULT_COLOR = '#F6F6F6' + + # Creation source + attr_accessor :source + + # HEX color code of tag + attr_accessor :color + + # Risk modifier + attr_accessor :risk_modifier + + # Array containing Site IDs to be associated with tag + attr_accessor :site_ids + + # Array containing Asset IDs to be associated with tag + attr_accessor :asset_ids + + # Array containing Asset IDs directly associated with the tag + attr_accessor :associated_asset_ids + + # Array containing Asset Group IDs to be associated with tag + attr_accessor :asset_group_ids + alias_method :group_ids, :asset_group_ids + alias_method :group_ids=, :asset_group_ids= + + # A TagCriteria + attr_accessor :search_criteria + + def initialize(name, type, id = -1) + @name, @type, @id = name, type, id + @source = 'nexpose-client' + @color = @type == Type::Generic::GENERAL ? DEFAULT_COLOR : nil + end + + # Creates and saves a tag to Nexpose console + # + # @param [Connection] connection Nexpose connection + # @return [Fixnum] ID of saved tag + # + def save(connection) + params = to_json + if @id == -1 + uri = AJAX.post(connection, '/api/2.0/tags', params, AJAX::CONTENT_TYPE::JSON) + @id = uri.split('/').last.to_i + else + AJAX.put(connection, "/api/2.0/tags/#{@id}", params, AJAX::CONTENT_TYPE::JSON) + end + @id + end + + # Retrieve detailed description of a single tag + # + # @param [Connection] connection Nexpose connection + # @param [Fixnum] ID of tag to retrieve + # @return [Tag] requested tag + # + def self.load(connection, tag_id) + json = JSON.parse(AJAX.get(connection, "/api/2.0/tags/#{tag_id}")) + Tag.parse(json) + end + + def to_json + json = { + 'tag_name' => @name, + 'tag_type' => @type, + 'tag_id' => @id, + 'attributes' => [ + { 'tag_attribute_name' => 'SOURCE', + 'tag_attribute_value' => @source } + ], + 'tag_config' => { 'site_ids' => @site_ids, + 'tag_associated_asset_ids' => @asset_ids, + 'asset_group_ids' => @asset_group_ids, + 'search_criteria' => @search_criteria ? @search_criteria.to_map : nil + } + } + if @type == Type::Generic::GENERAL + json['attributes'] << { 'tag_attribute_name' => 'COLOR', 'tag_attribute_value' => @color } + elsif @type == Type::Generic::CRITICALITY + json['attributes'] << { 'tag_attribute_name' => 'RISK_MODIFIER', 'tag_attribute_value' => 5.0 } + end + JSON.generate(json) + end + + # Delete this tag from Nexpose console + # + # @param [Connection] connection Nexpose connection + # + def delete(connection) + connection.delete_tag(@id) + end + + def self.parse(json) + color = json['attributes'].find { |attr| attr['tag_attribute_name'] == 'COLOR' } + color = color['tag_attribute_value'] if color + source = json['attributes'].find { |attr| attr['tag_attribute_name'] == 'SOURCE' } + source = source['tag_attribute_value'] if source + tag = Tag.new(json['tag_name'], json['tag_type'], json['tag_id']) + tag.color = color + tag.source = source + tag.asset_ids = json['asset_ids'] + if json['tag_config'] + tag.site_ids = json['tag_config']['site_ids'] + tag.associated_asset_ids = json['tag_config']['tag_associated_asset_ids'] + tag.asset_group_ids = json['tag_config']['asset_group_ids'] + criteria = json['tag_config']['search_criteria'] + tag.search_criteria = criteria ? Criteria.parse(criteria) : nil + end + modifier = json['attributes'].find { |attr| attr['tag_attribute_name'] == 'RISK_MODIFIER' } + if modifier + tag.risk_modifier = modifier['tag_attribute_value'].to_i + end + tag + end + + # Adds a tag to an asset + # + # @param [Connection] connection Nexpose connection + # @param [Fixnum] asset_id of the asset to be tagged + # @return [Fixnum] ID of applied tag + # + def add_to_asset(connection, asset_id) + params = to_json_for_add + uri = AJAX.post(connection, "/api/2.0/assets/#{asset_id}/tags", params, AJAX::CONTENT_TYPE::JSON) + @id = uri.split('/').last.to_i + end + + # Adds a tag to a site + # + # @param [Connection] connection Nexpose connection + # @param [Fixnum] site_id of the site to be tagged + # @return [Fixnum] ID of applied tag + # + def add_to_site(connection, site_id) + params = to_json_for_add + uri = AJAX.post(connection, "/api/2.0/sites/#{site_id}/tags", params, AJAX::CONTENT_TYPE::JSON) + @id = uri.split('/').last.to_i + end + + # Adds a tag to an asset group + # + # @param [Connection] connection Nexpose connection + # @param [Fixnum] group_id id of the asset group to be tagged + # @return [Fixnum] ID of applied tag + # + def add_to_group(connection, group_id) + params = to_json_for_add + uri = AJAX.post(connection, "/api/2.0/asset_groups/#{group_id}/tags", params, AJAX::CONTENT_TYPE::JSON) + @id = uri.split('/').last.to_i + end + alias_method :add_to_asset_group, :add_to_group + + private + + def to_json_for_add + if @id == -1 + json = { + 'tag_name' => @name, + 'tag_type' => @type, + 'attributes' => [ + { 'tag_attribute_name' => 'SOURCE', + 'tag_attribute_value' => @source } + ], + } + if @type == Tag::Type::Generic::GENERAL + json['attributes'] << { 'tag_attribute_name' => 'COLOR', 'tag_attribute_value' => @color } + end + params = JSON.generate(json) + else + params = JSON.generate('tag_id' => @id) + end + params + end + end +end diff --git a/lib/nexpose/tag/criteria.rb b/lib/nexpose/tag/criteria.rb new file mode 100644 index 00000000..b04bd266 --- /dev/null +++ b/lib/nexpose/tag/criteria.rb @@ -0,0 +1,45 @@ +module Nexpose + class Tag + + class Criterion < Nexpose::Criterion + + def to_map + { 'operator' => operator, + 'values' => Array(value), + 'field_name' => field + } + end + + def self.parse(json) + Criterion.new(json['field_name'], + json['operator'], + json['values']) + end + + end + + class Criteria < Nexpose::Criteria + + def initialize(criteria = [], match = 'AND') + super(criteria, match) + end + + def to_map + { 'criteria' => @criteria.map { |c| c.to_map }, + 'operator' => @match + } + end + + def self.parse(json) + ret = Criteria.new([], json['operator']) + json['criteria'].each do |c| + ret.criteria << Criterion.parse(c) + end + ret + end + + end + end +end + +