diff options
author | Vsevolod Stakhov <vsevolod@highsecure.ru> | 2021-11-04 20:22:36 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-11-04 20:22:36 +0000 |
commit | bcf3daa54b31c23665030e50f6fa73d155895496 (patch) | |
tree | dd7407290ecc0d343fc599c006ae19a96bd011fd /lualib | |
parent | b248bd715a92e93cfbd97bb80c12cf1375f9c014 (diff) | |
parent | 78ca6debc2a472f375e1c3e3e9c1266c9d42187f (diff) | |
download | rspamd-bcf3daa54b31c23665030e50f6fa73d155895496.tar.gz rspamd-bcf3daa54b31c23665030e50f6fa73d155895496.zip |
Merge pull request #3960 from HeinleinSupport/lua_scanners/icap
lua_scanners/icap
Diffstat (limited to 'lualib')
-rw-r--r-- | lualib/lua_scanners/icap.lua | 440 |
1 files changed, 345 insertions, 95 deletions
diff --git a/lualib/lua_scanners/icap.lua b/lualib/lua_scanners/icap.lua index 86def8edd..d2a418fb1 100644 --- a/lualib/lua_scanners/icap.lua +++ b/lualib/lua_scanners/icap.lua @@ -15,17 +15,69 @@ See the License for the specific language governing permissions and limitations under the License. ]]-- ---[[[ --- @module icap --- This module contains icap access functions. --- Currently tested with --- - Symantec --- - Sophos Savdi --- - ClamAV/c-icap --- - Kaspersky Web Traffic Security --- - Trend Micro IWSVA --- - F-Secure Internet Gatekeeper Strings ---]] +--[[ +@module icap +This module contains icap access functions. +Currently tested with + - C-ICAP Squidclamav / echo + - F-Secure Internet Gatekeeper + - Kaspersky Web Traffic Security + - Kaspersky Scan Engine 2.0 + - McAfee Web Gateway 11 + - Sophos Savdi + - Symantec (Rspamd <3.2, >=3.2 untested) + - Trend Micro IWSVA 6.0 + - Trend Micro Web Gateway + +@TODO + - Preview / Continue + - Reqmod URL's + - Content-Type / Filename +]] -- + +--[[ +Configuration Notes: + +C-ICAP Squidclamav + scheme = "squidclamav"; + +ESET Gateway Security / Antivirus for Linux example: + scheme = "scan"; + +F-Secure Internet Gatekeeper example: + scheme = "respmod"; + x_client_header = true; + x_rcpt_header = true; + x_from_header = true; + +Kaspersky Web Traffic Security example: + scheme = "av/respmod"; + x_client_header = true; + +Kaspersky Web Traffic Security (as configured in kavicapd.xml): + scheme = "resp"; + x_client_header = true; + +McAfee Web Gateway 11 (Headers must be activated with personal extra Rules) + scheme = "respmod"; + x_client_header = true; + +Sophos SAVDI example: + # scheme as configured in savdi.conf (name option in service section) + scheme = "respmod"; + +Symantec example: + scheme = "avscan"; + +Trend Micro IWSVA example (X-Virus-ID/X-Infection-Found headers must be activated): + scheme = "avscan"; + x_client_header = true; + +Trend Micro Web Gateway example (X-Virus-ID/X-Infection-Found headers must be activated): + scheme = "interscan"; + x_client_header = true; +]] -- + local lua_util = require "lua_util" local tcp = require "rspamd_tcp" @@ -48,6 +100,8 @@ local function icap_config(opts) scan_image_mime = false, scheme = "scan", default_port = 1344, + ssl = false, + no_ssl_verify = false, timeout = 10.0, log_clean = false, retransmits = 2, @@ -58,6 +112,14 @@ local function icap_config(opts) action = false, dynamic_scan = false, user_agent = "Rspamd", + x_client_header = false, + x_rcpt_header = false, + x_from_header = false, + req_headers_enabled = true, + req_fake_url = "http://127.0.0.1/mail", + http_headers_enabled = true, + use_http_result_header = true, + use_http_3xx_as_threat = false, } icap_conf = lua_util.override_defaults(icap_conf, opts) @@ -103,7 +165,9 @@ local function icap_check(task, content, digest, rule, maybe_part) local upstream = rule.upstreams:get_upstream_round_robin() local addr = upstream:get_addr() local retransmits = rule.retransmits - local respond_headers = {} + local http_headers = {} + local req_headers = {} + local tcp_options = {} -- Build extended User Agent if rule.user_agent == "extended" then @@ -116,16 +180,24 @@ local function icap_check(task, content, digest, rule, maybe_part) -- Build the icap queries local options_request = { - string.format("OPTIONS icap://%s/%s ICAP/1.0\r\n", addr:to_string(true), rule.scheme), + string.format("OPTIONS icap://%s/%s ICAP/1.0\r\n", addr:to_string(), rule.scheme), string.format('Host: %s\r\n', addr:to_string()), string.format("User-Agent: %s\r\n", rule.user_agent), + "Connection: keep-alive\r\n", "Encapsulated: null-body=0\r\n\r\n", } if rule.user_agent == "none" then table.remove(options_request, 3) end - local size = string.format("%x", tonumber(#content)) + local respond_headers = { + -- Add main RESPMOD header before any other + string.format('RESPMOD icap://%s/%s ICAP/1.0\r\n', addr:to_string(), rule.scheme), + string.format('Host: %s\r\n', addr:to_string()), + } + + local size = tonumber(#content) + local chunked_size = string.format("%x", size) local function icap_callback(err, conn) @@ -149,16 +221,13 @@ local function icap_check(task, content, digest, rule, maybe_part) lua_util.debugm(rule.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, - stop_pattern = '\r\n', - data = options_request, - read = false, - callback = icap_callback, - }) + tcp_options.host = addr:to_string() + tcp_options.port = addr:get_port() + tcp_options.callback = icap_callback + tcp_options.data = options_request + + tcp.request(tcp_options) + else rspamd_logger.errx(task, '%s: failed to scan, maximum retransmits '.. 'exceed - error: %s', rule.log_prefix, err_m or '') @@ -167,11 +236,76 @@ local function icap_check(task, content, digest, rule, maybe_part) end end + local function get_req_headers() + + local req_hlen = 2 + table.insert(req_headers, string.format('GET %s HTTP/1.0\r\n', rule.req_fake_url)) + table.insert(req_headers, string.format('Date: %s\r\n', rspamd_util.time_to_string(rspamd_util.get_time()))) + --table.insert(http_headers, string.format('Content-Type: %s\r\n', 'text/html')) + if rule.user_agent ~= "none" then + table.insert(req_headers, string.format("User-Agent: %s\r\n", rule.user_agent)) + end + + for _, h in ipairs(req_headers) do + req_hlen = req_hlen + tonumber(#h) + end + + return req_hlen, req_headers + + end + + local function get_http_headers() + local http_hlen = 2 + table.insert(http_headers, 'HTTP/1.0 200 OK\r\n') + table.insert(http_headers, string.format('Date: %s\r\n', rspamd_util.time_to_string(rspamd_util.get_time()))) + table.insert(http_headers, string.format('Server: %s\r\n', 'Apache/2.4')) + if rule.user_agent ~= "none" then + table.insert(http_headers, string.format("User-Agent: %s\r\n", rule.user_agent)) + end + --table.insert(http_headers, string.format('Content-Type: %s\r\n', 'text/html')) + table.insert(http_headers, string.format('Content-Length: %s\r\n', size)) + + for _, h in ipairs(http_headers) do + http_hlen = http_hlen + tonumber(#h) + end + + return http_hlen, http_headers + + end + local function get_respond_query() - table.insert(respond_headers, 1, string.format( - 'RESPMOD icap://%s:%s/%s ICAP/1.0\r\n', addr:to_string(), addr:get_port(), rule.scheme)) + local req_hlen = 0 + local resp_req_headers + local http_hlen = 0 + local resp_http_headers + + -- Append all extra headers + if rule.user_agent ~= "none" then + table.insert(respond_headers, string.format("User-Agent: %s\r\n", rule.user_agent)) + end + + if rule.req_headers_enabled then + req_hlen, resp_req_headers = get_req_headers() + end + if rule.http_headers_enabled then + http_hlen, resp_http_headers = get_http_headers() + end + + if rule.req_headers_enabled and rule.http_headers_enabled then + local res_body_hlen = req_hlen + http_hlen + table.insert(respond_headers, string.format('Encapsulated: req-hdr=0, res-hdr=%s, res-body=%s\r\n', req_hlen, res_body_hlen)) + elseif rule.http_headers_enabled then + table.insert(respond_headers, string.format('Encapsulated: res-hdr=0, res-body=%s\r\n', http_hlen)) + else + table.insert(respond_headers, 'Encapsulated: res-body=0\r\n') + end + + table.insert(respond_headers, '\r\n') + for _,h in ipairs(resp_req_headers) do table.insert(respond_headers, h) end table.insert(respond_headers, '\r\n') - table.insert(respond_headers, size .. '\r\n') + for _,h in ipairs(resp_http_headers) do table.insert(respond_headers, h) end + table.insert(respond_headers, '\r\n') + table.insert(respond_headers, chunked_size .. '\r\n') table.insert(respond_headers, content) table.insert(respond_headers, '\r\n0\r\n\r\n') return respond_headers @@ -183,16 +317,17 @@ local function icap_check(task, content, digest, rule, maybe_part) end end - local function icap_result_header_table(result) + local function result_header_table(result) local icap_headers = {} for s in result:gmatch("[^\r\n]+") do if string.find(s, '^ICAP') then - icap_headers['icap'] = s - end - if string.find(s, '[%a%d-+]-:') then + icap_headers['icap'] = tostring(s) + elseif string.find(s, '^HTTP') then + icap_headers['http'] = tostring(s) + elseif string.find(s, '[%a%d-+]-:') then local _,_,key,value = tostring(s):find("([%a%d-+]-):%s?(.+)") if key ~= nil then - icap_headers[key] = value + icap_headers[key:lower()] = tostring(value) end end end @@ -201,7 +336,7 @@ local function icap_check(task, content, digest, rule, maybe_part) return icap_headers end - local function icap_parse_result(icap_headers) + local function icap_parse_result(headers) local threat_string = {} @@ -209,41 +344,65 @@ local function icap_check(task, content, digest, rule, maybe_part) @ToDo: handle type in response Generic Strings: - X-Infection-Found: Type=0; Resolution=2; Threat=Troj/DocDl-OYC; - X-Infection-Found: Type=0; Resolution=2; Threat=W97M.Downloader; + icap: X-Infection-Found: Type=0; Resolution=2; Threat=Troj/DocDl-OYC; + icap: X-Infection-Found: Type=0; Resolution=2; Threat=W97M.Downloader; Symantec String: - X-Infection-Found: Type=2; Resolution=2; Threat=Container size violation - X-Infection-Found: Type=2; Resolution=2; Threat=Encrypted container violation; + icap: X-Infection-Found: Type=2; Resolution=2; Threat=Container size violation + icap: X-Infection-Found: Type=2; Resolution=2; Threat=Encrypted container violation; Sophos Strings: - X-Virus-ID: Troj/DocDl-OYC + icap: X-Virus-ID: Troj/DocDl-OYC + http: X-Blocked: Virus found during virus scan + http: X-Blocked-By: Sophos Anti-Virus Kaspersky Web Traffic Security Strings: - X-Virus-ID: HEUR:Backdoor.Java.QRat.gen - X-Response-Info: blocked - X-Virus-ID: no threats - X-Response-Info: blocked - X-Response-Info: passed - - Trend Micro IWSVA Strings: - X-Virus-ID: Trojan.W97M.POWLOAD.SMTHF1 - X-Infection-Found: Type=0; Resolution=2; Threat=Trojan.W97M.POWLOAD.SMTHF1; + icap: X-Virus-ID: HEUR:Backdoor.Java.QRat.gen + icap: X-Response-Info: blocked + icap: X-Virus-ID: no threats + icap: X-Response-Info: blocked + icap: X-Response-Info: passed + http: HTTP/1.1 403 Forbidden + + Kaspersky Scan Engine 2.0 (ICAP mode) + icap: X-Virus-ID: EICAR-Test-File + http: HTTP/1.0 403 Forbidden + + Trend Micro Strings: + icap: X-Virus-ID: Trojan.W97M.POWLOAD.SMTHF1 + icap: X-Infection-Found: Type=0; Resolution=2; Threat=Trojan.W97M.POWLOAD.SMTHF1; + http: HTTP/1.1 403 Forbidden (TMWS Blocked) + http: HTTP/1.1 403 Forbidden F-Secure Internet Gatekeeper Strings: - X-FSecure-Scan-Result: infected - X-FSecure-Infection-Name: "Malware.W97M/Agent.32584203" - X-FSecure-Infected-Filename: "virus.doc" + icap: X-FSecure-Scan-Result: infected + icap: X-FSecure-Infection-Name: "Malware.W97M/Agent.32584203" + icap: X-FSecure-Infected-Filename: "virus.doc" ESET File Security for Linux 7.0 - X-Infection-Found: Type=0; Resolution=0; Threat=VBA/TrojanDownloader.Agent.JOA; - X-Virus-ID: Trojaner - X-Response-Info: Blocked + icap: X-Infection-Found: Type=0; Resolution=0; Threat=VBA/TrojanDownloader.Agent.JOA; + icap: X-Virus-ID: Trojaner + icap: X-Response-Info: Blocked + + McAfee Web Gateway 11 (Headers must be activated with personal extra Rules) + icap: X-Virus-ID: EICAR test file + icap: X-Media-Type: text/plain + icap: X-Block-Result: 80 + icap: X-Block-Reason: Malware found + icap: X-Block-Reason: Archive not supported + icap: X-Block-Reason: Media Type (Block List) + http: HTTP/1.0 403 VirusFound + + C-ICAP Squidclamav + icap/http: X-Infection-Found: Type=0; Resolution=2; Threat={HEX}EICAR.TEST.3.UNOFFICIAL; + icap/http: X-Virus-ID: {HEX}EICAR.TEST.3.UNOFFICIAL + http: HTTP/1.0 307 Temporary Redirect ]] -- - if icap_headers['X-Infection-Found'] then + -- Generic ICAP Headers + if headers['x-infection-found'] then local _,_,icap_type,_,icap_threat = - icap_headers['X-Infection-Found']:find("Type=(.-); Resolution=(.-); Threat=(.-);$") + headers['x-infection-found']:find("Type=(.-); Resolution=(.-); Threat=(.-);$") if not icap_type or icap_type == 2 then -- error returned @@ -251,35 +410,37 @@ local function icap_check(task, content, digest, rule, maybe_part) '%s: icap error X-Infection-Found: %s', rule.log_prefix, icap_threat) common.yield_result(task, rule, icap_threat, 0, 'fail', maybe_part) + return true else lua_util.debugm(rule.name, task, '%s: icap X-Infection-Found: %s', rule.log_prefix, icap_threat) table.insert(threat_string, icap_threat) end - elseif icap_headers['X-Virus-ID'] and icap_headers['X-Virus-ID'] ~= "no threats" then + elseif headers['x-virus-id'] and headers['x-virus-id'] ~= "no threats" then lua_util.debugm(rule.name, task, - '%s: icap X-Virus-ID: %s', rule.log_prefix, icap_headers['X-Virus-ID']) + '%s: icap X-Virus-ID: %s', rule.log_prefix, headers['x-virus-id']) - if string.find(icap_headers['X-Virus-ID'], ', ') then - local vnames = lua_util.str_split(string.gsub(icap_headers['X-Virus-ID'], "%s", ""), ',') or {} + if string.find(headers['x-virus-id'], ', ') then + local vnames = lua_util.str_split(string.gsub(headers['x-virus-id'], "%s", ""), ',') or {} for _,v in ipairs(vnames) do table.insert(threat_string, v) end else - table.insert(threat_string, icap_headers['X-Virus-ID']) + table.insert(threat_string, headers['x-virus-id']) end - elseif icap_headers['X-FSecure-Scan-Result'] and icap_headers['X-FSecure-Scan-Result'] ~= "clean" then + -- FSecure X-Headers + elseif headers['x-fsecure-scan-result'] and headers['x-fsecure-scan-result'] ~= "clean" then local infected_filename = "" local infection_name = "-unknown-" - if icap_headers['X-FSecure-Infected-Filename'] then - infected_filename = string.gsub(icap_headers['X-FSecure-Infected-Filename'], '[%s"]', '') + if headers['x-fsecure-infected-filename'] then + infected_filename = string.gsub(headers['x-fsecure-infected-filename'], '[%s"]', '') end - if icap_headers['X-FSecure-Infection-Name'] then - infection_name = string.gsub(icap_headers['X-FSecure-Infection-Name'], '[%s"]', '') + if headers['x-fsecure-infection-name'] then + infection_name = string.gsub(headers['x-fsecure-infection-name'], '[%s"]', '') end lua_util.debugm(rule.name, task, @@ -295,13 +456,54 @@ local function icap_check(task, content, digest, rule, maybe_part) else table.insert(threat_string, infection_name) end + -- McAfee Web Gateway manual extra headers + elseif headers['x-mwg-block-reason'] and headers['x-mwg-block-reason'] ~= "" then + table.insert(threat_string, headers['x-mwg-block-reason']) + -- Sophos SAVDI special http headers + elseif headers['x-blocked'] and headers['x-blocked'] ~= "" then + table.insert(threat_string, headers['x-blocked']) + -- last try HTTP [4]xx return + elseif headers.http and string.find(headers.http, '^HTTP%/[12]%.. [4]%d%d') then + local message = string.format("pseudo-virus (blocked): %s", string.gsub(headers.http, 'HTTP%/[12]%.. ', '')) + table.insert(threat_string, message) + elseif rule.use_http_3xx_as_threat and headers.http and string.find(headers.http, '^HTTP%/[12]%.. [3]%d%d') then + local message = string.format("pseudo-virus (redirect): %s", string.gsub(headers.http, 'HTTP%/[12]%.. ', '')) + table.insert(threat_string, message) end + if #threat_string > 0 then common.yield_result(task, rule, threat_string, rule.default_score, nil, maybe_part) common.save_cache(task, digest, rule, threat_string, rule.default_score, maybe_part) + return true else - common.save_cache(task, digest, rule, 'OK', 0, maybe_part) - common.log_clean(task, rule) + return false + end + end + + local function icap_r_respond_http_cb(err_m, data, connection) + if err_m or connection == nil then + icap_requery(err_m, "icap_r_respond_http_cb") + else + local result = tostring(data) + + local icap_http_headers = result_header_table(result) or {} + -- Find HTTP/[12].x [234]xx response + if icap_http_headers.http and string.find(icap_http_headers.http, 'HTTP%/[12]%.. [234]%d%d') then + local icap_http_header_result = icap_parse_result(icap_http_headers) + if icap_http_header_result then + -- Threat found - close connection + connection:close() + else + common.save_cache(task, digest, rule, 'OK', 0, maybe_part) + common.log_clean(task, rule) + end + else + rspamd_logger.errx(task, '%s: unhandled response |%s|', + rule.log_prefix, string.gsub(result, "\r\n", ", ")) + common.yield_result(task, rule, + 'unhandled icap response: ' .. icap_http_headers.icap or "-", + 0.0, 'fail', maybe_part) + end end end @@ -310,12 +512,28 @@ local function icap_check(task, content, digest, rule, maybe_part) icap_requery(err_m, "icap_r_respond_cb") else local result = tostring(data) - conn:close() - local icap_headers = icap_result_header_table(result) or {} + local icap_headers = result_header_table(result) or {} -- Find ICAP/1.x 2xx response if icap_headers.icap and string.find(icap_headers.icap, 'ICAP%/1%.. 2%d%d') then - icap_parse_result(icap_headers) + local icap_header_result = icap_parse_result(icap_headers) + if icap_header_result then + -- Threat found - close connection + connection:close() + elseif not icap_header_result + and rule.use_http_result_header + and icap_headers.encapsulated + and not string.find(icap_headers.encapsulated, 'null%-body=0') + then + -- Try to read encapsulated HTTP Headers + lua_util.debugm(rule.name, task, '%s: no ICAP virus header found - try HTTP headers', + rule.log_prefix) + connection:add_read(icap_r_respond_http_cb, '\r\n\r\n') + else + connection:close() + common.save_cache(task, digest, rule, 'OK', 0, maybe_part) + common.log_clean(task, rule) + end elseif icap_headers.icap and string.find(icap_headers.icap, 'ICAP%/1%.. [45]%d%d') then -- Find ICAP/1.x 5/4xx response --[[ @@ -323,6 +541,12 @@ local function icap_check(task, content, digest, rule, maybe_part) ICAP/1.0 539 Aborted - No AV scanning license SquidClamAV/C-ICAP: ICAP/1.0 500 Server error + Eset: + ICAP/1.0 405 Forbidden + TrendMicro: + ICAP/1.0 400 Bad request + McAfee: + ICAP/1.0 418 Bad composition ]]-- rspamd_logger.errx(task, '%s: ICAP ERROR: %s', rule.log_prefix, icap_headers.icap) common.yield_result(task, rule, icap_headers.icap, 0.0, @@ -350,32 +574,50 @@ local function icap_check(task, content, digest, rule, maybe_part) if err_m or connection == nil then icap_requery(err_m, "icap_r_options_cb") else - local icap_headers = icap_result_header_table(tostring(data)) + local icap_headers = result_header_table(tostring(data)) if icap_headers.icap and string.find(icap_headers.icap, 'ICAP%/1%.. 2%d%d') then - if icap_headers['Methods'] and string.find(icap_headers['Methods'], 'RESPMOD') then - if icap_headers['Allow'] and string.find(icap_headers['Allow'], '204') then + if icap_headers['methods'] and string.find(icap_headers['methods'], 'RESPMOD') then + -- Allow "204 No Content" responses + -- https://datatracker.ietf.org/doc/html/rfc3507#section-4.6 + if icap_headers['allow'] and string.find(icap_headers['allow'], '204') then add_respond_header('Allow', '204') end - if icap_headers['Service'] and string.find(icap_headers['Service'], 'IWSVA 6.5') then - add_respond_header('Encapsulated', 'res-hdr=0 res-body=0') - else - add_respond_header('Encapsulated', 'res-body=0') - end - if icap_headers['Server'] and string.find(icap_headers['Server'], 'F-Secure ICAP Server') then - local from = task:get_from('mime') - local rcpt_to = task:get_principal_recipient() + + if rule.x_client_header then local client = task:get_from_ip() if client then add_respond_header('X-Client-IP', client:to_string()) end - add_respond_header('X-Mail-From', from[1].addr) - add_respond_header('X-Rcpt-To', rcpt_to) end - conn:add_write(icap_w_respond_cb, get_respond_query()) + -- F-Secure extra headers + if icap_headers['server'] and string.find(icap_headers['server'], 'f-secure icap server') then + + if rule.x_rcpt_header then + local rcpt_to = task:get_principal_recipient() + if rcpt_to then add_respond_header('X-Rcpt-To', rcpt_to) end + end + + if rule.x_from_header then + local mail_from = task:get_principal_recipient() + if mail_from and mail_from[1] then add_respond_header('X-Rcpt-To', mail_from[1].addr) end + end + + end + + if icap_headers.connection and icap_headers.connection:lower() == 'close' then + lua_util.debugm(rule.name, task, '%s: OPTIONS request Connection: %s - using new connection', + rule.log_prefix, icap_headers.connection) + connection:close() + tcp_options.callback = icap_w_respond_cb + tcp_options.data = get_respond_query() + tcp.request(tcp_options) + else + connection:add_write(icap_w_respond_cb, get_respond_query()) + end else rspamd_logger.errx(task, '%s: RESPMOD method not advertised: Methods: %s', - rule.log_prefix, icap_headers['Methods']) + rule.log_prefix, icap_headers['methods']) common.yield_result(task, rule, 'NO RESPMOD', 0.0, 'fail', maybe_part) end @@ -397,16 +639,24 @@ local function icap_check(task, content, digest, rule, maybe_part) end end - tcp.request({ - task = task, - host = addr:to_string(), - port = addr:get_port(), - timeout = rule.timeout, - stop_pattern = '\r\n', - data = options_request, - read = false, - callback = icap_callback, - }) + tcp_options.task = task + tcp_options.stop_pattern = '\r\n' + tcp_options.read = false + tcp_options.timeout = rule.timeout + tcp_options.callback = icap_callback + tcp_options.data = options_request + + if rule.ssl then + tcp_options.ssl = true + if rule.no_ssl_verify then + tcp_options.no_ssl_verify = true + end + end + + tcp_options.host = addr:to_string() + tcp_options.port = addr:get_port() + + tcp.request(tcp_options) end if common.condition_check_and_continue(task, content, rule, digest, |