rapid7-insightidr 1.14.8: user_authentications counts the user's own logons

The keyword search matches the account name anywhere in a line, so it brought
other accounts' logons: on a real run the only "new" event reported was
another user's failed password. Only events whose account/user field is the
user (DOMAIN\\user and user@domain normalized) are summarized now, with
keyword_matches and user_matches both reported. A first authentication is
recognized as Rapid7 flags it, a boolean new_authentication field, as well as
the word. The event limit cannot be set below 10000 (the agent passed 1000).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RhL2gy6Sx6Y3FEsyBE3mZ
This commit is contained in:
2026-10-12 07:53:57 +00:00
parent 3bfcebdf21
commit fb560cc937
3 changed files with 54 additions and 11 deletions
@@ -40,7 +40,9 @@ instructions: |
assets", "Domain Credentials": call user_authentications(user=<the assets", "Domain Credentials": call user_authentications(user=<the
account>, anchor_time=<the alert's first seen time, copied as given>) account>, anchor_time=<the alert's first seen time, copied as given>)
ONCE. It does the search, the window and the split itself. Report its ONCE. It does the search, the window and the split itself. Report its
"window" word for word, the totals from "all", and from "window" word for word, user_matches (the user's own events —
keyword_matches includes other accounts' lines, never report those as
the user's), the totals from "all", and from
"new_authentications" the destination assets by name with their counts: "new_authentications" the destination assets by name with their counts:
those are the new assets. Do not redo this search with query_log_set. those are the new assets. Do not redo this search with query_log_set.
Other alerts with evidence_api false, or a question this does not Other alerts with evidence_api false, or a question this does not
File diff suppressed because one or more lines are too long
@@ -255,6 +255,42 @@ def _resolve_name(name):
NEW_MARKERS = ("new_authentication",) NEW_MARKERS = ("new_authentication",)
_TRUE = ("true", "1", "yes")
def _norm_account(v):
"""jdoe, DOMAIN\\jdoe, jdoe@domain.tld -> jdoe."""
v = str(v or "").strip().lower()
if "\\" in v:
v = v.rsplit("\\", 1)[1]
if "@" in v:
v = v.split("@", 1)[0]
return v
def _is_users(fields, user):
"""The event is this user's: an account or user field holds the account.
The keyword search matches the text anywhere in the line, so it also brings
other people's logons — on a real run the only "new" event it found was
another account's. Only the account fields decide."""
want = _norm_account(user)
for k, v in fields.items():
kl = k.lower()
if ("account" in kl or "user" in kl) and "sid" not in kl and _norm_account(v) == want:
return True
return False
def _is_new(fields):
"""Rapid7 marks a first-time authentication either with a boolean field
(new_authentication: true) or with that word as a value."""
for k, v in fields.items():
if "new_authentication" in k.lower() and str(v).strip().lower() in _TRUE:
return True
if str(v).strip().lower() in NEW_MARKERS:
return True
return False
def run(): def run():
@@ -278,24 +314,29 @@ def run():
name = str(inp.get("log_set_name") or "Asset Authentication").strip() name = str(inp.get("log_set_name") or "Asset Authentication").strip()
log_set_id = _resolve_name(name) log_set_id = _resolve_name(name)
try: try:
limit = int(inp.get("limit") or 2000) limit = max(int(inp.get("limit") or 10000), 10000)
except (TypeError, ValueError): except (TypeError, ValueError):
limit = 2000 limit = 10000
query = 'where("%s")' % user query = 'where("%s")' % user
first = request("GET", "log_search/query/logsets/" + urllib.parse.quote(log_set_id, safe=""), first = request("GET", "log_search/query/logsets/" + urllib.parse.quote(log_set_id, safe=""),
params={"query": query, "from": start_time, "to": end_time}) params={"query": query, "from": start_time, "to": end_time})
events = _identify(_collect(first, limit), "", None) matched = _identify(_collect(first, limit), "", None)
new = [e for e in events events = [e for e in matched if _is_users(_fields(e), user)]
if any(str(v).lower() in NEW_MARKERS for v in _fields(e).values())] new = [e for e in events if _is_new(_fields(e))]
print(json.dumps({ print(json.dumps({
"user": user, "user": user,
"log_set": name, "log_set": name,
"query": query, "query": query,
"keyword_matches": len(matched),
"user_matches": len(events),
"truncated": len(matched) >= limit,
"window": "%s to %s" % (_iso(start_time), _iso(end_time)), "window": "%s to %s" % (_iso(start_time), _iso(end_time)),
"all": _summarize(events, limit), "all": _summarize(events, limit),
"new_authentications": _summarize(new, limit), "new_authentications": _summarize(new, limit),
"note": ("new_authentications holds only the events Rapid7 marked new_authentication: their " "note": ("all and new_authentications hold only this user's events (an account field equals "
"destination assets are the new assets a behaviour alert counts."), "the user): keyword_matches counts every line the text appeared in, other accounts "
"included. new_authentications holds the events Rapid7 marked as a first "
"authentication: their destination assets are the new assets a behaviour alert counts."),
})) }))