-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.

--[[
#  Mozilla Security SSHD Login Monitor

Match SSH login events and generate email alerts.

By default, alerts always go to default_email.

If user_email is specified in the configuration, alerts will also be sent to this
address. User email should be a string containing a %s format specifier which is
replaced with the username of the account which logged in.

The acceptable_message_drift parameter indicates an age in seconds. If a new event
is received and the timestamp is older than current time - acceptable_message_drift,
or newer than the current time + acceptable_message_drift, then instead of handling
the alert normally the alert will be submitted to drift_email with an indication
that events are being consumed with abnormal timestamp fields.

If drift_email is not set, excessively new or old messages will just be ignored.

This analysis plugin makes assumptions events will be received in a timely manner
under normal circumstances.

## Sample Configuration
```lua
filename = "moz_security_sshd_login_monitor.lua"
message_matcher = "Type ~= 'bastion.file.sshd'% && Fields[sshd_authmsg] == 'Accepted'"
ticker_interval = 0
process_message_inject_limit = 1

default_email = "foxsec-dump+OutOfHours@mozilla.com" -- required
-- user_email = "manatee-%s@moz-svc-ops.pagerduty.com" -- optional user specific email address
-- drift_email = "captainkirk@mozilla.com" -- optional drift message notification
-- acceptable_message_drift = 600 -- optional, defaults to 600 seconds if not specified
```
--]]
--
require "string"
require "math"
require "os"

local default_email            = read_config("default_email") or error("default_email must be configured")
local user_email               = read_config("user_email")
local drift_email              = read_config("drift_email")
local acceptable_message_drift = read_config("acceptable_message_drift") or 600
local cephost                  = read_config("Hostname") or "unknown"

local msg = {
    Type = "alert",
    Payload = "",
    Severity = 1,
    Fields = {
        {name = "id"                , value = "sshd"},
        {name = "summary"           , value = ""},
        {name = "email.recipients"  , value = {}}
    }
}

function process_message()
    local ts      = read_message("Timestamp")
    local hn      = read_message("Hostname") or "unknown"
    local user    = read_message("Fields[user]") or "unknown"
    local ip      = read_message("Fields[ssh_remote_ipaddr]") or "unknown"
    local city    = read_message("Fields[ssh_remote_ipaddr_city]")
    local country = read_message("Fields[ssh_remote_ipaddr_country]")

    local tss = math.floor(ts / 1e9)

    -- first, adjust the message for the default case
    msg.Fields[2].value = string.format("%s bastion login %s from %s", user, hn, ip)
    msg.Fields[3].value[1] = string.format("<%s>", default_email)
    msg.Fields[3].value[2] = nil
    msg.Payload = string.format("Generated by %s, event timestamp %s\n",
        cephost, os.date("%Y-%m-%d %H:%M:%S", tss))

    -- if we also have city and country information, append that to the message subject
    if city and country then
        msg.Fields[2].value = msg.Fields[2].value .. string.format(" (%s, %s)", city, country)
    end

    -- if user_email is set, add a user specific notification address
    if user_email then
        msg.Fields[3].value[2] = string.format(string.format("<%s>", user_email), user)
    end

    -- finally, if the message has time drift modify the recipient address and add some additional
    -- information to the payload
    local delaysec = math.abs(os.time() - tss)
    if delaysec > acceptable_message_drift then
        if not drift_email then return -1, "dropping event with time drift" end
        msg.Fields[3].value[1] = string.format("<%s>", drift_email)
        msg.Fields[3].value[2] = nil
        msg.Payload = msg.Payload .. string.format("WARNING: message has unacceptable drift %d seconds\n", delaysec)
    end

    inject_message(msg)
    return 0
end


function timer_event()
    -- no op
end
