Mini Shell
"""
Beacon to fire events at failed login of users
.. versionadded:: 2015.5.0
Example Configuration
=====================
.. code-block:: yaml
# Fire events on all failed logins
beacons:
btmp: []
# Matching on user name, using a default time range
beacons:
btmp:
- users:
gareth:
- defaults:
time_range:
start: '8am'
end: '4pm'
# Matching on user name, overriding the default time range
beacons:
btmp:
- users:
gareth:
time_range:
start: '8am'
end: '4pm'
- defaults:
time_range:
start: '8am'
end: '4pm'
# Matching on group name, overriding the default time range
beacons:
btmp:
- groups:
users:
time_range:
start: '8am'
end: '4pm'
- defaults:
time_range:
start: '8am'
end: '4pm'
Use Case: Posting Failed Login Events to Slack
==============================================
This can be done using the following reactor SLS:
.. code-block:: jinja
report-wtmp:
runner.salt.cmd:
- args:
- fun: slack.post_message
- channel: mychannel # Slack channel
- from_name: someuser # Slack user
- message: "Failed login from `{{ data.get('user', '') or 'unknown user' }}` on `{{ data['id'] }}`"
Match the event like so in the master config file:
.. code-block:: yaml
reactor:
- 'salt/beacon/*/btmp/':
- salt://reactor/btmp.sls
.. note::
This approach uses the :py:mod:`slack execution module
<salt.modules.slack_notify>` directly on the master, and therefore requires
that the master has a slack API key in its configuration:
.. code-block:: yaml
slack:
api_key: xoxb-XXXXXXXXXXXX-XXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXX
See the :py:mod:`slack execution module <salt.modules.slack_notify>`
documentation for more information. While you can use an individual user's
API key to post to Slack, a bot user is likely better suited for this. The
:py:mod:`slack engine <salt.engines.slack>` documentation has information
on how to set up a bot user.
"""
import datetime
import logging
import os
import struct
import salt.utils.beacons
import salt.utils.files
import salt.utils.stringutils
__virtualname__ = "btmp"
BTMP = "/var/log/btmp"
FMT = b"hi32s4s32s256shhiii4i20x"
FIELDS = [
"type",
"PID",
"line",
"inittab",
"user",
"hostname",
"exit_status",
"session",
"time",
"addr",
]
SIZE = struct.calcsize(FMT)
LOC_KEY = "btmp.loc"
log = logging.getLogger(__name__)
try:
import dateutil.parser as dateutil_parser
_TIME_SUPPORTED = True
except ImportError:
_TIME_SUPPORTED = False
def __virtual__():
if os.path.isfile(BTMP):
return __virtualname__
err_msg = f"{BTMP} does not exist."
log.error("Unable to load %s beacon: %s", __virtualname__, err_msg)
return False, err_msg
def _validate_time_range(trange, status, msg):
"""
Check time range
"""
# If trange is empty, just return the current status & msg
if not trange:
return status, msg
if not isinstance(trange, dict):
status = False
msg = "The time_range parameter for btmp beacon must be a dictionary."
if not all(k in trange for k in ("start", "end")):
status = False
msg = (
"The time_range parameter for btmp beacon must contain start & end options."
)
return status, msg
def _gather_group_members(group, groups, users):
"""
Gather group members
"""
_group = __salt__["group.info"](group)
if not _group:
log.warning("Group %s does not exist, ignoring.", group)
return
for member in _group["members"]:
if member not in users:
users[member] = groups[group]
def _check_time_range(time_range, now):
"""
Check time range
"""
if _TIME_SUPPORTED:
_start = dateutil_parser.parse(time_range["start"])
_end = dateutil_parser.parse(time_range["end"])
return bool(_start <= now <= _end)
else:
log.error("Dateutil is required.")
return False
def _get_loc():
"""
return the active file location
"""
if LOC_KEY in __context__:
return __context__[LOC_KEY]
def validate(config):
"""
Validate the beacon configuration
"""
vstatus = True
vmsg = "Valid beacon configuration"
# Configuration for load beacon should be a list of dicts
if not isinstance(config, list):
vstatus = False
vmsg = "Configuration for btmp beacon must be a list."
else:
config = salt.utils.beacons.list_to_dict(config)
if "users" in config:
if not isinstance(config["users"], dict):
vstatus = False
vmsg = "User configuration for btmp beacon must be a dictionary."
else:
for user in config["users"]:
_time_range = config["users"][user].get("time_range", {})
vstatus, vmsg = _validate_time_range(_time_range, vstatus, vmsg)
if not vstatus:
return vstatus, vmsg
if "groups" in config:
if not isinstance(config["groups"], dict):
vstatus = False
vmsg = "Group configuration for btmp beacon must be a dictionary."
else:
for group in config["groups"]:
_time_range = config["groups"][group].get("time_range", {})
vstatus, vmsg = _validate_time_range(_time_range, vstatus, vmsg)
if not vstatus:
return vstatus, vmsg
if "defaults" in config:
if not isinstance(config["defaults"], dict):
vstatus = False
vmsg = "Defaults configuration for btmp beacon must be a dictionary."
else:
_time_range = config["defaults"].get("time_range", {})
vstatus, vmsg = _validate_time_range(_time_range, vstatus, vmsg)
if not vstatus:
return vstatus, vmsg
return vstatus, vmsg
def beacon(config):
"""
Read the last btmp file and return information on the failed logins
"""
ret = []
users = {}
groups = {}
defaults = None
for config_item in config:
if "users" in config_item:
users = config_item["users"]
if "groups" in config_item:
groups = config_item["groups"]
if "defaults" in config_item:
defaults = config_item["defaults"]
with salt.utils.files.fopen(BTMP, "rb") as fp_:
loc = __context__.get(LOC_KEY, 0)
if loc == 0:
fp_.seek(0, 2)
__context__[LOC_KEY] = fp_.tell()
return ret
else:
fp_.seek(loc)
while True:
now = datetime.datetime.now()
raw = fp_.read(SIZE)
if len(raw) != SIZE:
return ret
__context__[LOC_KEY] = fp_.tell()
pack = struct.unpack(FMT, raw)
event = {}
for ind, field in enumerate(FIELDS):
event[field] = pack[ind]
if isinstance(event[field], (str, bytes)):
if isinstance(event[field], bytes):
event[field] = salt.utils.stringutils.to_unicode(event[field])
event[field] = event[field].strip("\x00")
for group in groups:
_gather_group_members(group, groups, users)
if users:
if event["user"] in users:
_user = users[event["user"]]
if isinstance(_user, dict) and "time_range" in _user:
if _check_time_range(_user["time_range"], now):
ret.append(event)
else:
if defaults and "time_range" in defaults:
if _check_time_range(defaults["time_range"], now):
ret.append(event)
else:
ret.append(event)
else:
if defaults and "time_range" in defaults:
if _check_time_range(defaults["time_range"], now):
ret.append(event)
else:
ret.append(event)
return ret
Zerion Mini Shell 1.0