...

Text file src/edge-infra.dev/pkg/edge/logging/fluentbit/siem.lua

Documentation: edge-infra.dev/pkg/edge/logging/fluentbit

     1--
     2-- json.lua
     3--
     4-- Copyright (c) 2020 rxi
     5--
     6-- Permission is hereby granted, free of charge, to any person obtaining a copy of
     7-- this software and associated documentation files (the "Software"), to deal in
     8-- the Software without restriction, including without limitation the rights to
     9-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
    10-- of the Software, and to permit persons to whom the Software is furnished to do
    11-- so, subject to the following conditions:
    12--
    13-- The above copyright notice and this permission notice shall be included in all
    14-- copies or substantial portions of the Software.
    15--
    16-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    17-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    18-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    19-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    20-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    21-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    22-- SOFTWARE.
    23--
    24
    25local json = { _version = "0.1.2" }
    26
    27-------------------------------------------------------------------------------
    28-- Decode
    29-------------------------------------------------------------------------------
    30
    31local parse
    32
    33local function create_set(...)
    34  local res = {}
    35  for i = 1, select("#", ...) do
    36    res[ select(i, ...) ] = true
    37  end
    38  return res
    39end
    40
    41local space_chars   = create_set(" ", "\t", "\r", "\n")
    42local delim_chars   = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
    43local escape_chars  = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
    44local literals      = create_set("true", "false", "null")
    45
    46local literal_map = {
    47  [ "true"  ] = true,
    48  [ "false" ] = false,
    49  [ "null"  ] = nil,
    50}
    51
    52
    53local function next_char(str, idx, set, negate)
    54  for i = idx, #str do
    55    if set[str:sub(i, i)] ~= negate then
    56      return i
    57    end
    58  end
    59  return #str + 1
    60end
    61
    62
    63local function decode_error(str, idx, msg)
    64  local line_count = 1
    65  local col_count = 1
    66  for i = 1, idx - 1 do
    67    col_count = col_count + 1
    68    if str:sub(i, i) == "\n" then
    69      line_count = line_count + 1
    70      col_count = 1
    71    end
    72  end
    73  error( string.format("%s at line %d col %d", msg, line_count, col_count) )
    74end
    75
    76
    77local function codepoint_to_utf8(n)
    78  -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
    79  local f = math.floor
    80  if n <= 0x7f then
    81    return string.char(n)
    82  elseif n <= 0x7ff then
    83    return string.char(f(n / 64) + 192, n % 64 + 128)
    84  elseif n <= 0xffff then
    85    return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
    86  elseif n <= 0x10ffff then
    87    return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
    88                       f(n % 4096 / 64) + 128, n % 64 + 128)
    89  end
    90  error( string.format("invalid unicode codepoint '%x'", n) )
    91end
    92
    93
    94local function parse_unicode_escape(s)
    95  local n1 = tonumber( s:sub(1, 4),  16 )
    96  local n2 = tonumber( s:sub(7, 10), 16 )
    97   -- Surrogate pair?
    98  if n2 then
    99    return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
   100  else
   101    return codepoint_to_utf8(n1)
   102  end
   103end
   104
   105
   106local function parse_string(str, i)
   107  local res = ""
   108  local j = i + 1
   109  local k = j
   110
   111  while j <= #str do
   112    local x = str:byte(j)
   113
   114    if x < 32 then
   115      decode_error(str, j, "control character in string")
   116
   117    elseif x == 92 then -- `\`: Escape
   118      res = res .. str:sub(k, j - 1)
   119      j = j + 1
   120      local c = str:sub(j, j)
   121      if c == "u" then
   122        local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
   123                 or str:match("^%x%x%x%x", j + 1)
   124                 or decode_error(str, j - 1, "invalid unicode escape in string")
   125        res = res .. parse_unicode_escape(hex)
   126        j = j + #hex
   127      else
   128        if not escape_chars[c] then
   129          decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
   130        end
   131        res = res .. escape_char_map_inv[c]
   132      end
   133      k = j + 1
   134
   135    elseif x == 34 then -- `"`: End of string
   136      res = res .. str:sub(k, j - 1)
   137      return res, j + 1
   138    end
   139
   140    j = j + 1
   141  end
   142
   143  decode_error(str, i, "expected closing quote for string")
   144end
   145
   146
   147local function parse_number(str, i)
   148  local x = next_char(str, i, delim_chars)
   149  local s = str:sub(i, x - 1)
   150  local n = tonumber(s)
   151  if not n then
   152    decode_error(str, i, "invalid number '" .. s .. "'")
   153  end
   154  return n, x
   155end
   156
   157
   158local function parse_literal(str, i)
   159  local x = next_char(str, i, delim_chars)
   160  local word = str:sub(i, x - 1)
   161  if not literals[word] then
   162    decode_error(str, i, "invalid literal '" .. word .. "'")
   163  end
   164  return literal_map[word], x
   165end
   166
   167
   168local function parse_array(str, i)
   169  local res = {}
   170  local n = 1
   171  i = i + 1
   172  while 1 do
   173    local x
   174    i = next_char(str, i, space_chars, true)
   175    -- Empty / end of array?
   176    if str:sub(i, i) == "]" then
   177      i = i + 1
   178      break
   179    end
   180    -- Read token
   181    x, i = parse(str, i)
   182    res[n] = x
   183    n = n + 1
   184    -- Next token
   185    i = next_char(str, i, space_chars, true)
   186    local chr = str:sub(i, i)
   187    i = i + 1
   188    if chr == "]" then break end
   189    if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
   190  end
   191  return res, i
   192end
   193
   194
   195local function parse_object(str, i)
   196  local res = {}
   197  i = i + 1
   198  while 1 do
   199    local key, val
   200    i = next_char(str, i, space_chars, true)
   201    -- Empty / end of object?
   202    if str:sub(i, i) == "}" then
   203      i = i + 1
   204      break
   205    end
   206    -- Read key
   207    if str:sub(i, i) ~= '"' then
   208      decode_error(str, i, "expected string for key")
   209    end
   210    key, i = parse(str, i)
   211    -- Read ':' delimiter
   212    i = next_char(str, i, space_chars, true)
   213    if str:sub(i, i) ~= ":" then
   214      decode_error(str, i, "expected ':' after key")
   215    end
   216    i = next_char(str, i + 1, space_chars, true)
   217    -- Read value
   218    val, i = parse(str, i)
   219    -- Set
   220    res[key] = val
   221    -- Next token
   222    i = next_char(str, i, space_chars, true)
   223    local chr = str:sub(i, i)
   224    i = i + 1
   225    if chr == "}" then break end
   226    if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
   227  end
   228  return res, i
   229end
   230
   231
   232local char_func_map = {
   233  [ '"' ] = parse_string,
   234  [ "0" ] = parse_number,
   235  [ "1" ] = parse_number,
   236  [ "2" ] = parse_number,
   237  [ "3" ] = parse_number,
   238  [ "4" ] = parse_number,
   239  [ "5" ] = parse_number,
   240  [ "6" ] = parse_number,
   241  [ "7" ] = parse_number,
   242  [ "8" ] = parse_number,
   243  [ "9" ] = parse_number,
   244  [ "-" ] = parse_number,
   245  [ "t" ] = parse_literal,
   246  [ "f" ] = parse_literal,
   247  [ "n" ] = parse_literal,
   248  [ "[" ] = parse_array,
   249  [ "{" ] = parse_object,
   250}
   251
   252
   253parse = function(str, idx)
   254  local chr = str:sub(idx, idx)
   255  local f = char_func_map[chr]
   256  if f then
   257    return f(str, idx)
   258  end
   259  decode_error(str, idx, "unexpected character '" .. chr .. "'")
   260end
   261
   262
   263function decode(str)
   264  if type(str) ~= "string" then
   265    error("expected argument of type string, got " .. type(str))
   266  end
   267  local res, idx = parse(str, next_char(str, 1, space_chars, true))
   268  idx = next_char(str, idx, space_chars, true)
   269  if idx <= #str then
   270    decode_error(str, idx, "trailing garbage")
   271  end
   272  return res
   273end
   274
   275-------------------------------------------------------------------------------
   276-- SIEM.LUA SCRIPT STARTS BELOW
   277-------------------------------------------------------------------------------
   278
   279local function parse_configmap(configmap_file)
   280  local siem_configs = {}
   281
   282  -- function to see if file exists
   283  local function file_exists(name)
   284      local f = io.open(name, "r")
   285      return f ~= nil and io.close(f)
   286  end
   287  if file_exists(configmap_file) then
   288      local file = io.open(configmap_file, "r")
   289      if file then
   290      local content = file:read("*a")
   291      if content ~= '' then
   292          siem_configs = decode(content)
   293      end 
   294      file:close()
   295      end
   296  else
   297      print("File does not exist: " .. configmap_file)
   298  end
   299
   300  return siem_configs
   301end
   302
   303local severity_hierarchy = {
   304  ["debug"] = 1,
   305  ["info"] = 2,
   306  ["notice"] = 3,
   307  ["warn"] = 4,
   308  ["warning"] = 4,
   309  ["error"] = 5,
   310  ["crit"] = 6,
   311  ["critical"] = 6,
   312  ["alert"] = 7,
   313  ["emergency"] = 8,
   314}
   315
   316local function severity_hierarchy_finder(severity_allowed, min_level)
   317  for severity, level in pairs(severity_hierarchy) do
   318    if level >= min_level then
   319      severity_allowed[severity] = true
   320    end
   321  end
   322  return severity_allowed
   323end
   324
   325local function siem_helper(entry, record)
   326  record["log_type"] = entry.log_type
   327  record["log_class"] = entry.log_class
   328  return true, record
   329end
   330
   331
   332local function pattern_match_helper(entry, record)
   333  local function match_and_update(field)
   334    local matched_pattern = string.match(record[field], entry.pattern)
   335    if matched_pattern ~= nil then 
   336      return siem_helper(entry, record)
   337    end
   338    return false, record
   339  end
   340
   341  if entry.pattern == "" then
   342    return siem_helper(entry, record)
   343  end
   344
   345  if record["message"] then
   346    return match_and_update("message")
   347  elseif record["msg"] then
   348    return match_and_update("msg")
   349  end
   350
   351  return false, record
   352end
   353
   354local function add_siem_record(tag, record, siem_configs, config_type)
   355  local tag_pattern = "^(k8s_container%.)([%a%-%d]+)_([%a%-%d]+)_([%a%-%d]+)"
   356  level = string.lower(record["severity"])
   357  local _, namespace_name, pod_name, container_name = tag:match(tag_pattern)
   358
   359  local record_updated = false
   360    -- loop through each configuration
   361  for index , entry in ipairs(siem_configs) do 
   362    repeat_breaker = false
   363    repeat
   364      -- create the severity_allowed hashmap
   365      local severity_allowed = {}
   366      local severity_allowed = severity_hierarchy_finder(severity_allowed, severity_hierarchy[string.lower(entry.severity)])
   367      if not severity_allowed[level] then 
   368        break
   369      end 
   370        -- Checks to see if the entry and tag data match
   371        local pod_pattern_match = entry.pod:gsub("-", "%%-")
   372        local container_pattern_match = entry.container:gsub("-", "%%-")
   373        local matched_pod = string.match(pod_name, pod_pattern_match)
   374        local matched_container = string.match(container_name, container_pattern_match)
   375
   376        -- check for static config
   377        if config_type == "edge" then 
   378          local namespace_pattern_match = entry.namespace:gsub("-", "%%-")  
   379          local matched_namespace = string.match(namespace_name, namespace_pattern_match)
   380          
   381          if not (matched_namespace and matched_container and matched_pod) then
   382            break
   383          else
   384              -- To this point we are 100% certain that the record was updated with new values
   385              -- record_updated should be true and we need to return an updated record back
   386              record_updated, record = pattern_match_helper(entry, record)
   387              return record_updated, record
   388          end
   389        end 
   390
   391         -- check for custom config
   392        if config_type == "workload" then 
   393          local workload_label
   394
   395          workload_label = record["kubernetes"]["namespace"]["labels"]["siem.edge.ncr.com/helm-edge-id"]
   396
   397          if entry.helm_edge_ID and workload_label and entry.pattern then
   398            if not (entry.helm_edge_ID == workload_label and matched_container and matched_pod and severity_allowed[level]) then
   399              break
   400          
   401            else
   402              -- To this point we are 100% certain that the record was updated with new values
   403              -- record_updated should be true and we need to return an updated record back
   404              record_updated, record = pattern_match_helper(entry, record)
   405              return record_updated, record
   406            end
   407          end
   408        end
   409      repeat_breaker = true
   410    until repeat_breaker 
   411  end
   412  -- Here we know that we do not have any relevant information
   413  -- to add SIEM values so return the record back and 
   414  -- record_updated should be false
   415  return record_updated, record
   416end
   417
   418
   419--[[
   420  process_logs is the starting function that fluent-bit calls
   421    - return codes : -1 record must be deleted
   422                      0 record not modified, keep the original
   423                      1 record was modified, replace timestamp and record
   424                      2 record was modified, replace record and keep timestamp
   425  ]]
   426  function process_logs(tag, timestamp, record)
   427    -- logs with log_class=replay are logs that have potentially already made it to the
   428    -- cloud and could already be stored in a siem bucket. Since we don't want to store replayed
   429    -- logs in the siem again. we need to return the log as-is instead of adding new siem classifications
   430    if record["log_class"] and record["log_class"] == "replay" then
   431      return 0, timestamp, record
   432    end
   433
   434    -- If there is no severity, kick out early
   435    if record["severity"] == nil then
   436      return 0, timestamp, record
   437    end
   438
   439
   440    local updated, new_record
   441    if (record["kubernetes"] ~= nil and record["kubernetes"]["namespace"] ~= nil and record["kubernetes"]["namespace"]["labels"]["siem.edge.ncr.com/helm-edge-id"] ~= nil) then
   442      local workload_siem_configmap_file = "/var/configs/workload-siem/configs"
   443      local workload_siem_configs = parse_configmap(workload_siem_configmap_file)
   444      updated, new_record = add_siem_record(tag, record, workload_siem_configs, "workload")
   445    else
   446      local edge_siem_configmap_file = "/var/configs/edge-siem/edge-siem"
   447      local edge_siem_configs = parse_configmap(edge_siem_configmap_file)
   448      updated, new_record = add_siem_record(tag, record, edge_siem_configs, "edge")
   449    end   
   450
   451    if updated then
   452      return 2, timestamp, new_record
   453    else
   454      return 0, timestamp, record
   455    end
   456  end
   457
   458return {
   459    parse_configmap = parse_configmap,
   460    add_siem_record = add_siem_record,
   461  }

View as plain text