]> source.dussan.org Git - rspamd.git/commitdiff
[Feature] external_services - oletools (olefy) support
authorCarsten Rosenberg <c.rosenberg@heinlein-support.de>
Sat, 12 Jan 2019 23:28:38 +0000 (00:28 +0100)
committerCarsten Rosenberg <c.rosenberg@heinlein-support.de>
Sat, 12 Jan 2019 23:28:38 +0000 (00:28 +0100)
lualib/lua_scanners/init.lua
lualib/lua_scanners/oletools.lua [new file with mode: 0644]

index f769eb5a5d5a69cc958c05d384a71a1463a2ba3d..4bbd654d10f21c7d23d78f0ce33fac1b850a7745 100644 (file)
@@ -39,6 +39,7 @@ require_scanner('sophos')
 
 -- Other scanners
 require_scanner('dcc')
+require_scanner('oletools')
 
 exports.add_scanner = function(name, t, conf_func, check_func)
   assert(type(conf_func) == 'function' and type(check_func) == 'function',
@@ -59,4 +60,4 @@ exports.filter = function(t)
   end, exports))
 end
 
-return exports
\ No newline at end of file
+return exports
diff --git a/lualib/lua_scanners/oletools.lua b/lualib/lua_scanners/oletools.lua
new file mode 100644 (file)
index 0000000..16e0fdc
--- /dev/null
@@ -0,0 +1,276 @@
+--[[
+Copyright (c) 2018, Vsevolod Stakhov <vsevolod@highsecure.ru>
+Copyright (c) 2018, Carsten Rosenberg <c.rosenberg@heinlein-support.de>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+]]--
+
+--[[[
+-- @module dcc
+-- This module contains dcc access functions
+--]]
+
+local lua_util = require "lua_util"
+local tcp = require "rspamd_tcp"
+local upstream_list = require "rspamd_upstream_list"
+local rspamd_logger = require "rspamd_logger"
+local ucl = require "ucl"
+local common = require "lua_scanners/common"
+local fun = require "fun"
+
+local module_name = 'oletools'
+
+local function oletools_check(task, content, digest, rule)
+  local function oletools_check_uncached ()
+    local upstream = rule.upstreams:get_upstream_round_robin()
+    local addr = upstream:get_addr()
+    local retransmits = rule.retransmits
+
+    local function oletools_callback(err, data, conn)
+
+      local function oletools_requery()
+        -- set current upstream to fail because an error occurred
+        upstream:fail()
+
+        -- retry with another upstream until retransmits exceeds
+        if retransmits > 0 then
+
+          retransmits = retransmits - 1
+
+          lua_util.debugm(rule.module_name, task, '%s: Request Error: %s - retries left: %s',
+            rule.log_prefix, err, retransmits)
+
+          -- Select a different upstream!
+          upstream = rule.upstreams:get_upstream_round_robin()
+          addr = upstream:get_addr()
+
+          lua_util.debugm(rule.module_name, task, '%s: retry IP: %s:%s',
+            rule.log_prefix, addr, addr:get_port())
+
+          tcp.request({
+            task = task,
+            host = addr:to_string(),
+            port = addr:get_port(),
+            timeout = rule['timeout'],
+            shutdown = true,
+            data = content,
+            callback = oletools_callback,
+          })
+        else
+          rspamd_logger.errx(task, '%s: failed to scan, maximum retransmits '..
+            'exceed', rule.log_prefix)
+          task:insert_result(rule['symbol_fail'], 0.0, 'failed to scan and '..
+            'retransmits exceed')
+        end
+      end
+
+      if err then
+
+        oletools_requery()
+
+      else
+        -- Parse the response
+        if upstream then upstream:ok() end
+
+        data = tostring(data)
+        lua_util.debugm(rule.module_name, task, 'data: %s', tostring(data))
+
+        local ucl_parser = ucl.parser()
+        local ok, ucl_err = ucl_parser:parse_string(tostring(data))
+        if not ok then
+            rspamd_logger.errx(task, "%s: error parsing json response: %s",
+              rule.log_prefix, ucl_err)
+            return
+        end
+
+        local result = ucl_parser:get_object()
+
+        local oletools_rc = {
+          [0] = 'RETURN_OK',
+          [1] = 'RETURN_WARNINGS',
+          [2] = 'RETURN_WRONG_ARGS',
+          [3] = 'RETURN_FILE_NOT_FOUND',
+          [4] = 'RETURN_XGLOB_ERR',
+          [5] = 'RETURN_OPEN_ERROR',
+          [6] = 'RETURN_PARSE_ERROR',
+          [7] = 'RETURN_SEVERAL_ERRS',
+          [8] = 'RETURN_UNEXPECTED',
+          [9] = 'RETURN_ENCRYPTED',
+        }
+
+        --lua_util.debugm(rule.module_name, task, '%s: result: %s', rule.log_prefix, result)
+        lua_util.debugm(rule.module_name, task, '%s: filename: %s', rule.log_prefix, result[2]['file'])
+        lua_util.debugm(rule.module_name, task, '%s: type: %s', rule.log_prefix, result[2]['type'])
+
+        if result[1].error ~= nil then
+          rspamd_logger.errx(task, '%s: ERROR found: %s', rule.log_prefix,
+            result[1].error)
+          oletools_requery()
+        elseif result[3]['return_code'] == 9 then
+          rspamd_logger.warnx(task, '%s: File is encrypted.', rule.log_prefix)
+        elseif result[3]['return_code'] > 6 then
+          rspamd_logger.errx(task, '%s: Oletools Error Returned: %s',
+            rule.log_prefix, oletools_rc[result[3]['return_code']])
+        elseif result[3]['return_code'] > 1 then
+          rspamd_logger.errx(task, '%s: Oletools Error Returned: %s',
+            rule.log_prefix, oletools_rc[result[3]['return_code']])
+          oletools_requery()
+        elseif result[2]['analysis'] == 'null' and #result[2]['macros'] == 0 then
+          if rule.log_clean == true then
+            rspamd_logger.infox(task, '%s: Scanned Macro is OK', rule.log_prefix)
+          else
+            lua_util.debugm(rule.module_name, task, '%s: No Macro found', rule.log_prefix)
+          end
+        elseif #result[2]['macros'] > 0 then
+
+          for _,m in ipairs(result[2]['macros']) do
+            lua_util.debugm(rule.module_name, task, '%s: macros found - code: %s, ole_stream: %s, '..
+              'vba_filename: %s', rule.log_prefix, m.code, m.ole_stream, m.vba_filename)
+          end
+
+          local macro_autoexec = false
+          local macro_suspicious = false
+          local macro_keyword_table = {}
+
+          for _,a in ipairs(result[2]['analysis']) do
+            if a.type ~= 'AutoExec' or a.type ~= 'Suspicious' then
+              lua_util.debugm(rule.module_name, task, '%s: threat found - type: %s, keyword: %s, '..
+                'description: %s', rule.log_prefix, a.type, a.keyword, a.description)
+            end
+            if a.type == 'AutoExec' then
+              macro_autoexec = true
+              if rule.extended == true then
+                table.insert(macro_keyword_table, a.keyword)
+              end
+            elseif a.type == 'Suspicious'
+              and a.keyword ~= 'Base64 Strings'
+              and a.keyword ~= 'Hex Strings'
+            then
+              macro_suspicious = true
+              if rule.extended == true then
+                table.insert(macro_keyword_table, a.keyword)
+              end
+            end
+          end
+
+          if macro_autoexec then
+            table.insert(macro_keyword_table, 'AutoExec')
+          end
+          if macro_suspicious then
+            table.insert(macro_keyword_table, 'Suspicious')
+          end
+
+          lua_util.debugm(rule.module_name, task, '%s: extended: %s', rule.log_prefix, rule.extended)
+          if rule.extended == false and macro_autoexec and macro_suspicious then
+
+            lua_util.debugm(rule.module_name, task, '%s: found macro_autoexec and '..
+              'macro_suspicious', rule.log_prefix)
+            local threat = 'AutoExec+Suspicious'
+            common.yield_result(task, rule, threat, rule.default_score)
+            common.save_av_cache(task, digest, rule, threat, rule.default_score)
+
+          elseif rule.extended == true and #macro_keyword_table > 0 then
+
+            common.yield_result(task, rule, macro_keyword_table, rule.default_score)
+            common.save_av_cache(task, digest, rule, macro_keyword_table, rule.default_score)
+
+          elseif rule.log_clean == true then
+            rspamd_logger.infox(task, '%s: Scanned Macro is OK', rule.log_prefix)
+          end
+
+        else
+          rspamd_logger.warnx(task, '%s: unhandled response', rule.log_prefix)
+        end
+      end
+    end
+
+    tcp.request({
+      task = task,
+      host = addr:to_string(),
+      port = addr:get_port(),
+      timeout = rule['timeout'],
+      shutdown = true,
+      data = content,
+      callback = oletools_callback,
+    })
+
+  end
+  if common.need_av_check(task, content, rule) then
+    if common.check_av_cache(task, digest, rule, oletools_check_uncached) then
+      return
+    else
+      oletools_check_uncached()
+    end
+  end
+end
+
+local function oletools_config(opts)
+
+  local oletools_conf = {
+    module_name = module_name,
+    scan_mime_parts = false,
+    scan_text_mime = false,
+    scan_image_mime = false,
+    default_port = 5954,
+    timeout = 15.0,
+    log_clean = false,
+    retransmits = 2,
+    cache_expire = 7200, -- expire redis in 2h
+    message = '${SCANNER}: Oletools threat message found: "${VIRUS}"',
+    detection_category = "office macro",
+    default_score = 1,
+    action = false,
+    extended = false,
+  }
+
+  oletools_conf = lua_util.override_defaults(oletools_conf, opts)
+
+  if not oletools_conf.prefix then
+    oletools_conf.prefix = 'rs_av_' .. oletools_conf.name .. '_'
+  end
+
+  if not oletools_conf.log_prefix then
+    if oletools_conf.name:lower() == oletools_conf.type:lower() then
+      oletools_conf.log_prefix = oletools_conf.name
+    else
+      oletools_conf.log_prefix = oletools_conf.name .. ' (' .. oletools_conf.type .. ')'
+    end
+  end
+
+  if not oletools_conf.servers then
+    rspamd_logger.errx(rspamd_config, 'no servers defined')
+
+    return nil
+  end
+
+  oletools_conf.upstreams = upstream_list.create(rspamd_config,
+    oletools_conf.servers,
+    oletools_conf.default_port)
+
+  if oletools_conf.upstreams then
+    lua_util.add_debug_alias('antivirus', oletools_conf.module_name)
+    return oletools_conf
+  end
+
+  rspamd_logger.errx(rspamd_config, 'cannot parse servers %s',
+    oletools_conf.servers)
+  return nil
+end
+
+return {
+  type = {module_name,'office macro scanner', 'hash', 'scanner'},
+  description = 'oletools office macro scanner',
+  configure = oletools_config,
+  check = oletools_check,
+  name = module_name
+}