local stdnse = require "stdnse"
local tab = require "tab"
local table = require "table"

-- Defined in mysql-audit.nse
-- makes nse-check-globals complain, but that's ok
local test = test

TEMPLATE_NAME="CIS MySQL Benchmarks v1.0.2"

-- These accounts should be treated as admins and excluded from some of the results
ADMIN_ACCOUNTS={"root", "debian-sys-maint"}

-- Checks whether a resultset is empty or not
local function isEmpty(rows)
  if ( #rows > 0 ) then return false end
  return true
end

-- Extracts a column from a row and return all occurances as an array
local function col2tab(rs, cname)
  local tab = {}
  local cpos
  for i=1, #rs.cols do
    if ( rs.cols[i].name == cname ) then
      cpos = i
      break
    end
  end
  if ( not(cpos) ) then
    return
  end
  for _, row in ipairs(rs.rows) do table.insert(tab, row[cpos]) end
  return tab
end

local function createINstmt(tab)
  local tab2 = {}
  for i=1, #tab do tab2[i] = ("'%s'"):format(tab[i]) end
  return table.concat(tab2, ",")
end


-- This next section contains all the tests

-- Logging
test { id="3.1", desc="Skip symbolic links", sql="SHOW variables WHERE Variable_name = 'log_error' AND Value IS NOT NULL", check=function(rowstab)
  return { status = not(isEmpty(rowstab[1])) }
end
}

test { id="3.2", desc="Logs not on system partition", sql="SHOW variables WHERE Variable_name = 'log_bin' AND Value <> 'OFF'", check=function(rowstab)
  local log = col2tab(rowstab[1], 'Value')
  return { status = isEmpty(rowstab[1]), result = log, review = not(isEmpty(rowstab[1])) }
end
}

test { id="3.2", desc="Logs not on database partition", sql="SHOW variables WHERE Variable_name = 'log_bin' AND Value <> 'OFF'", check=function(rowstab)
  local log = col2tab(rowstab[1], 'Value')
  return { status = isEmpty(rowstab[1]), result = log, review = not(isEmpty(rowstab[1])) }
end
}


-- General
test { id="4.1", desc="Supported version of MySQL", sql="SHOW VARIABLES like 'version'", check=function(rowstab)
  local ver = col2tab(rowstab[1], 'Value')[1]
  return { status = true, review = true, result = ("Version: %s"):format(ver) }
end
}

test { id="4.4", desc="Remove test database", sql="SHOW DATABASES like 'test'", check=function(rowstab) return { status = isEmpty(rowstab[1]) } end  }

test { id="4.5", desc="Change admin account name", sql="SELECT user FROM mysql.user WHERE user='root';", check=function(rowstab) return { status = isEmpty(rowstab[1]) } end }

test { id="4.7", desc="Verify Secure Password Hashes", sql="SELECT DISTINCT user, password from mysql.user where length(password) < 41 AND length(password) > 0", check=function(rowstab)
  local users = col2tab(rowstab[1], 'user')
  users.name = ( #users > 0 ) and "The following users were found having weak password hashes"
  return { status = isEmpty(rowstab[1]), result = users }
end
}

test { id="4.9", desc="Wildcards in user hostname", sql="select user from mysql.user where host = '%'", check=function(rowstab)
  local users = col2tab(rowstab[1], 'user')
  users.name = ( #users > 0 ) and "The following users were found with wildcards in hostname"
  return { status = isEmpty(rowstab[1]), result = users }
end
}

test { id="4.10", desc="No blank passwords", sql="select distinct user, password from mysql.user where length(password) = 0 or password is null", check=function(rowstab)
  local users = col2tab(rowstab[1], 'user')
  users.name = ( #users > 0 ) and "The following users were found having blank/empty passwords"
  return { status = isEmpty(rowstab[1]), result = users }
end
}

test { id="4.11", desc="Anonymous account", sql="select distinct user from mysql.user where user =''", check=function(rowstab) return { status = isEmpty(rowstab[1]) } end }


-- MySQL Permissions
test { id="5.1", desc="Access to mysql database",
  sql = { "SELECT user, host FROM mysql.db WHERE db = 'mysql' and ((Select_priv = 'Y') or (Insert_priv = 'Y') " ..
    "or (Update_priv = 'Y') or (Delete_priv = 'Y') or (Create_priv = 'Y') or (Drop_priv = 'Y'))",
    "SELECT user, host FROM mysql.user WHERE (Select_priv = 'Y') or (Insert_priv = 'Y') or " ..
    "(Update_priv = 'Y') or (Delete_priv = 'Y') or (Create_priv = 'Y') or (Drop_priv = 'Y')" },
  check = function(rowstab)

    local result = tab.new(2)
    tab.addrow(result, "user", "host")

    local rs = rowstab[1]
    for _, row in ipairs(rs.rows) do
      tab.addrow( result, row[1], row[2] )
    end

    return { status = false, review = true, result = { tab.dump(result), name="Verify the following users that have access to the MySQL database" } }
  end
}

test { id="5.2", desc="Do not grant FILE privileges to non Admin users",
  sql=("SELECT user, host FROM mysql.user WHERE File_priv = 'Y' AND user NOT IN (%s)"):format(createINstmt(ADMIN_ACCOUNTS)),
  check=function(rowstab)
    local users = col2tab(rowstab[1], 'user')
    users.name = ( #users > 0 ) and "The following users were found having the FILE privilege"
    return { status = isEmpty(rowstab[1]), result = users, review = not(isEmpty(rowstab[1])) }
  end
}

test { id="5.3", desc="Do not grant PROCESS privileges to non Admin users",
  sql=("SELECT user, host FROM mysql.user WHERE Process_priv = 'Y' AND user NOT IN (%s)"):format(createINstmt(ADMIN_ACCOUNTS)),
  check=function(rowstab)
    local users = col2tab(rowstab[1], 'user')
    users.name = ( #users > 0 ) and "The following users were found having the PROCESS privilege"
    return { status = isEmpty(rowstab[1]), result = users, review = not(isEmpty(rowstab[1])) }
  end
}

test { id="5.4", desc="Do not grant SUPER privileges to non Admin users",
  sql=("SELECT user, host FROM mysql.user WHERE Super_priv = 'Y' AND user NOT IN (%s)"):format(createINstmt(ADMIN_ACCOUNTS)),
  check=function(rowstab)
    local users = col2tab(rowstab[1], 'user')
    users.name = ( #users > 0 ) and "The following users were found having the SUPER privilege"
    return { status = isEmpty(rowstab[1]), result = users, review = not(isEmpty(rowstab[1])) }
  end
}

test { id="5.5", desc="Do not grant SHUTDOWN privileges to non Admin users",
  sql=("SELECT user, host FROM mysql.user WHERE Shutdown_priv = 'Y' AND user NOT IN (%s)"):format(createINstmt(ADMIN_ACCOUNTS)),
  check=function(rowstab)
    local users = col2tab(rowstab[1], 'user')
    users.name = ( #users > 0 ) and "The following users were found having the SHUTDOWN privilege"
    return { status = isEmpty(rowstab[1]), result = users, review = not(isEmpty(rowstab[1])) }
  end
}

test { id="5.6", desc="Do not grant CREATE USER privileges to non Admin users",
  sql=("SELECT user, host FROM mysql.user WHERE Create_user_priv = 'Y' AND user NOT IN (%s)"):format(createINstmt(ADMIN_ACCOUNTS)),
  check=function(rowstab)
    local users = col2tab(rowstab[1], 'user')
    users.name = ( #users > 0 ) and "The following users were found having the CREATE USER privilege"
    return { status = isEmpty(rowstab[1]), result = users, review = not(isEmpty(rowstab[1])) }
  end
}

test { id="5.7", desc="Do not grant RELOAD privileges to non Admin users",
  sql=("SELECT user, host FROM mysql.user WHERE Reload_priv = 'Y' AND user NOT IN (%s)"):format(createINstmt(ADMIN_ACCOUNTS)),
  check=function(rowstab)
    local users = col2tab(rowstab[1], 'user')
    users.name = ( #users > 0 ) and "The following users were found having the RELOAD privilege"
    return { status = isEmpty(rowstab[1]), result = users, review = not(isEmpty(rowstab[1])) }
  end
}

test { id="5.8", desc="Do not grant GRANT privileges to non Admin users",
  sql=("SELECT user, host FROM mysql.user WHERE Grant_priv = 'Y' AND user NOT IN (%s)"):format(createINstmt(ADMIN_ACCOUNTS)),
  check=function(rowstab)
    local users = col2tab(rowstab[1], 'user')
    users.name = ( #users > 0 ) and "The following users were found having the GRANT privilege"
    return { status = isEmpty(rowstab[1]), result = users, review = not(isEmpty(rowstab[1])) }
  end
}


-- MySQL Configuraiton options
test { id="6.2", desc="Disable Load data local", sql="SHOW variables WHERE Variable_name = 'local_infile' AND Value='OFF'", check=function(rowstab)
  return { status = not(isEmpty(rowstab[1])) }
end
}

test { id="6.3", desc="Disable old password hashing", sql="SHOW variables WHERE Variable_name = 'old_passwords' AND Value='OFF'", check=function(rowstab)
  return { status = not(isEmpty(rowstab[1])) }
end
}

test { id="6.4", desc="Safe show database", sql="SHOW variables WHERE Variable_name = 'safe_show_database' AND Value='ON'", check=function(rowstab)
  return { status = not(isEmpty(rowstab[1])) }
end
}

test { id="6.5", desc="Secure auth", sql="SHOW variables WHERE Variable_name = 'secure_auth' AND Value='ON'", check=function(rowstab)
  return { status = not(isEmpty(rowstab[1])) }
end
}

test { id="6.6", desc="Grant tables", sql="SHOW variables WHERE Variable_name = 'skip_grant_tables' AND Value='OFF'", check=function(rowstab)
  return { status = not(isEmpty(rowstab[1])) }
end
}

test { id="6.7", desc="Skip merge", sql="SHOW variables WHERE Variable_name = 'have_merge_engine' AND Value='DISABLED'", check=function(rowstab)
  return { status = not(isEmpty(rowstab[1])) }
end
}

test { id="6.8", desc="Skip networking", sql="SHOW variables WHERE Variable_name = 'skip_networking' AND Value='ON'", check=function(rowstab)
  return { status = not(isEmpty(rowstab[1])) }
end
}

test { id="6.9", desc="Safe user create", sql="select @@global.sql_mode, @@session.sql_mode FROM dual WHERE @@session.sql_mode='NO_AUTO_CREATE_USER' AND @@global.sql_mode='NO_AUTO_CREATE_USER'", check=function(rowstab)
  return { status = not(isEmpty(rowstab[1])) }
end
}

test { id="6.10", desc="Skip symbolic links", sql="SHOW variables WHERE Variable_name = 'have_symlink' AND Value='DISABLED'", check=function(rowstab)
  return { status = not(isEmpty(rowstab[1])) }
end
}
