aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAda Isopods <me@bathynomus.xyz>2025-05-15 07:48:24 -0600
committerAda Isopods <me@bathynomus.xyz>2026-10-21 23:04:24 -0600
commitbf0823fc02d4beb303ec7ca7192afa6c3af92ff4 (patch)
tree36d92cd9c9c4d4ed413adcc94f5c3e2e9648564e
parent880d4d6c90611661269b445d79dcf892535b1efa (diff)
woahg
Okay so I just - Implemented argparse - Universalized the IP filtering logic - Added verbosity settings, finally - Added something called "blank mode" where you can basically pick and choose what kinds of session you want to see - Moved the filtering logic I'd already implemented in low-verbosity mode to support blank mode to its own thing so it's universal among all the verbosity modes - Commented a bunch of stuff - Fixed a bug I ran into where if an extra-long "empty ping" is still open when the program is run it crashes. I just realized I should probably do that for all the session types actually I'm going to bed
-rw-r--r--README.md29
-rw-r--r--cowrieder.py195
2 files changed, 153 insertions, 71 deletions
diff --git a/README.md b/README.md
index 4162bd0..658c4d2 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,32 @@
-Script to help make sense of log output from popular SSH honeypot [Cowrie](https://github.com/cowrie/cowrie/). Only tested on my setup. YMMV
+Script to help make sense of log output from popular SSH honeypot [Cowrie](https://github.com/cowrie/cowrie/). I think I got carried away with the specialized logging settings. Only tested on my setup. Untested with Telnet. YMMV
# Usage
```
-python3 cowrieder.py cowrie.json
+cowrieder.py <args> filepath
```
-You can also filter it by IP address:
+Options:
```
-python3 cowrieder.py cowrie.json 127.0.0.1
-``` \ No newline at end of file
+-h, --help show this help message and exit
+-v, --verbose Make the output more verbose (print abridged logs for all logins where commands are run, not just the interesting ones)
+-vv, --verboser Make the output even more verbose (print timestamp, event id, and message (if applicable) for all events)
+-vvv, --verbosest Make the output even more verboserer (just group the input log by session id and dump it to pretty-printed JSON)
+-B, --blank On default and low verbosity, print nothing whatsoever. Override this with other arguments to filter for only the stuff you care about
+-P, --pings | With blank mode active, print "empty pings"
+-F, --fails | With blank mode active, print failed logins
+-L, --logins | With blank mode active, print "login pings"
+-C, --commands | With blank mode active, on default verbosity, print "command pings"
+-S, --sessions * With blank mode active, on default verbosity, print "interesting" sessions. With -v enabled, this and -C do the same thing.
+-a, --stats Print aggregated statistics
+-j, --json-stats Dump aggregated statistics as JSON
+-i, --filter-ip FILTER_IP
+ Filter data to a specific IP
+```
+
+# Terminology
+
+- "Empty ping": Client connected and made no attempt to log in
+- "Login ping": Client logged in successfully and did nothing
+- "Command ping": Client logged in successfully and only ran `uname`, `nproc`, and/or `echo`. In my experience "command pings" comprise most insances of a client successfully logging in and then bothering to run any commands \ No newline at end of file
diff --git a/cowrieder.py b/cowrieder.py
index 7194c20..419dd45 100644
--- a/cowrieder.py
+++ b/cowrieder.py
@@ -2,6 +2,7 @@ import json
import sys
import random
import os
+import argparse
def ccgen(seed=""): # Color code generator
if(not os.isatty(1)): # Do not output color code at all if output is not a TTY that can accept them
@@ -39,69 +40,139 @@ def ppip(addr, spacing = True): # Pretty-print an address with colour and, if it
return ccgen(addr) + addr + ccgen() + (" " * (15 - len(addr)))
return ccgen(addr) + addr + ccgen()
-if not 0 <= 1 < len(sys.argv): # TODO: integrate argparse
- print("Provide a path to a Cowrie JSON log file")
- exit(1)
-filepath = sys.argv[1]
+parser = argparse.ArgumentParser(
+ description="Script to parse JSON logs from popular SSH honeypot Cowrie and print them in a more readable format"
+)
+
+parser.add_argument('filepath')
+
+parser.add_argument('-v', '--verbose', action='store_true', help="Make the output more verbose (print abridged logs for all logins where commands are run, not just the interesting ones)")
+parser.add_argument('-vv', '--verboser', action='store_true', help="Make the output even more verbose (print timestamp, event id, and message (if applicable) for all events)")
+parser.add_argument('-vvv', '--verbosest', action='store_true', help="Make the output even more verboserer (just group the input log by session id and dump it to pretty-printed JSON)")
+
+parser.add_argument('-B', '--blank', action='store_true', help="Print nothing whatsoever. Override this with other arguments to filter for only the stuff you care about")
+parser.add_argument('-P', '--pings', action='store_true', help='| With blank mode active, print "empty pings"')
+parser.add_argument('-F', '--fails', action='store_true', help='| With blank mode active, print failed logins')
+parser.add_argument('-L', '--logins', action='store_true', help='| With blank mode active, print "login pings"')
+parser.add_argument('-C', '--commands', action='store_true', help='| With blank mode active, print "command pings"')
+parser.add_argument('-S', '--sessions', action='store_true', help='* With blank mode active, print the cool interesting sessions that aren\'t any of that other stuff.')
+
+parser.add_argument('-i', '--filter-ip', help='Filter data to a specific IP')
+
+#parser.add_argument('-a', '--stats', action='store_true', help='Print aggregated statistics')
+#parser.add_argument('-j', '--json-stats', action='store_true', help='Dump aggregated statistics as JSON')
+#print_stats = args.stats or args.json_stats
+
+args = parser.parse_args()
+
+filepath = args.filepath
+
+print_emptypings = True
+print_fails = True
+print_loginpings = True
+print_cmdpings = True
+print_sessions = True
+if(args.blank):
+ print_emptypings = args.pings
+ print_fails = args.fails
+ print_loginpings = args.logins
+ print_cmdpings = args.commands
+ print_sessions = args.sessions
-verbosity = 0
-print_log = True
-print_stats = True
entries = []
-sessions = {}
-session_ips = {}
-session_timestamps = {}
-session_durations = {}
-session_ran_command = {}
-session_successful_login = {}
+sessions = {} # Dict - session_id : list of events in session
+session_ips = {} # Dict - session_id : source IP
+session_timestamps = {} # Dict - session_id : timestamp of initial connection
+session_durations = {} # Dict - session_id : duration
+session_ran_command = [] # List of sessions where a command was run
+session_successful_login = {} # Dict - session_id : bool: was the login attempt successful
+ # Sessions where no login attempt was made will not be present
+cmdping_commands = ['uname', 'nproc', 'echo'] # List of commands that, if used exclusively, qualify a session as a "command ping"
+session_not_cmdpings = [] # List of sessions where a command not in cmdping_commands was run
+session_cmdping_commands = {} # Dict - session_id : list of "command ping" commands used in session
+
+
with open(filepath, "r") as logfile:
for line in logfile:
entry = json.loads(line)
- entries.append(entry)
- session_id = entry['session']
+
+ if(
+ (not args.filter_ip or entry["src_ip"] == args.filter_ip) # If it's not the IP we're filtering for don't even bother man
+ ):
+ entries.append(entry)
+ session_id = entry['session']
+
+ if(session_id not in sessions):
+ sessions.update({session_id: [entry]})
+ session_ips.update({session_id: entry['src_ip']})
+ else:
+ sessions[session_id].append(entry)
- if(session_id not in sessions):
- sessions.update({session_id: [entry]})
- session_ips.update({session_id: entry['src_ip']})
- else:
- sessions[session_id].append(entry)
+ if(entry["eventid"] == "cowrie.session.connect" and session_id not in session_timestamps): # Save the timestamp of the session's initial connection
+ session_timestamps.update({session_id: entry['timestamp']})
- if(entry["eventid"] == "cowrie.session.connect" and session_id not in session_timestamps): # Save the timestamp of the session's initial connection
- session_timestamps.update({session_id: entry['timestamp']})
+ if(entry["eventid"] == "cowrie.session.closed" and session_id not in session_durations): # Save the session's duration
+ session_durations.update({session_id: entry['duration']})
- if(entry["eventid"] == "cowrie.session.closed" and session_id not in session_durations): # Save the session's duration
- session_durations.update({session_id: entry['duration']})
+ if(entry["eventid"] == "cowrie.command.input"):
+ if(session_id not in session_ran_command): # Save whether any commands were run during the session
+ session_ran_command.append(session_id)
+
+ argv0 = entry["input"] # Isolate the name of the command
+ if(" " in argv0):
+ argv0 = entry["input"][:entry["input"].index(" ")]
+
+ if(argv0 in cmdping_commands): # Determine whether this event qualifies the session as not a command ping
+ if(session_id not in session_cmdping_commands):
+ session_cmdping_commands.update({session_id: [argv0]})
+ else:
+ session_cmdping_commands[session_id].append(argv0)
+ else:
+ if(session_id not in session_not_cmdpings):
+ session_not_cmdpings.append(session_id)
- if(entry["eventid"] == "cowrie.command.input" and session_id not in session_ran_command): # Save whether any commands were run during the session
- session_ran_command.update({session_id: True})
+ if(entry["eventid"] == "cowrie.login.success" and session_id not in session_successful_login): # Save if login was a success
+ session_successful_login.update({session_id: True})
- if(entry["eventid"] == "cowrie.login.success" and session_id not in session_successful_login): # Save if login was a success
- session_successful_login.update({session_id: True})
+ if(entry["eventid"] == "cowrie.login.failed" and session_id not in session_successful_login): # Save if login failed
+ session_successful_login.update({session_id: False})
- if(entry["eventid"] == "cowrie.login.failed" and session_id not in session_successful_login): # Save if login failed
- session_successful_login.update({session_id: False})
+ if(entry["eventid"] == "cowrie.login.success" and session_id in session_successful_login): # Save if login succeeded after failing
+ session_successful_login[session_id] = True
- if(entry["eventid"] == "cowrie.login.success" and session_id in session_successful_login): # Save if login succeeded after failing
- session_successful_login[session_id] = True
+# Blank Mode logic
-filter_ip = ""
-if 0 <= 2 < len(sys.argv):
- filter_ip = sys.argv[2]
+if(args.blank):
+ newsessions = sessions.copy()
-if(verbosity == 2): # High verbosity mode: print timestamp, event type and message (if applicable) for every event. I could probably do better but I'm lazy
for session_id in sessions:
- ip = session_ips[session_id]
- if(ip == filter_ip or not filter_ip):
- print("\n--- Session " + ccgen(session_id) + session_id + ccgen() + " from " + ppip(ip, False) +" ---")
- for entry in sessions[session_id]:
- if(entry['message']):
- eventid = entry['eventid']
- print(entry["timestamp"] + " > " + eventid + (" " * (29 - len(eventid))) + ": " + entry['message'])
- else:
- print(entry["timestamp"] + " > " + entry['eventid'])
-elif(verbosity == 3): # Maximum verbosity: literally just pretty print the now-sorted-by-session logs
+ if(
+ (not print_emptypings and session_id not in session_successful_login) or # Is empty ping and don't print those
+ (not print_fails and session_id in session_successful_login and not session_successful_login[session_id]) or # Is failed login and don't print those
+ (not print_loginpings and session_id in session_successful_login and session_successful_login[session_id] and session_id not in session_ran_command) or # is login ping and don't print those
+ (not print_cmdpings and session_id in session_ran_command and session_id not in session_not_cmdpings) or # is command ping and don't print those
+ (not print_sessions and session_id in session_ran_command and session_id in session_not_cmdpings) # is session and don't print those
+ ):
+ newsessions.pop(session_id)
+
+ sessions = newsessions
+
+# Printing logic coming up
+
+if(args.verboser): # High verbosity mode: print timestamp, event type and message (if applicable) for every event. I could probably do better but I'm lazy
+ for session_id in sessions:
+ ip = session_ips[session_id]
+ print("\n--- Session " + ccgen(session_id) + session_id + ccgen() + " from " + ppip(ip, False) +" ---")
+ for entry in sessions[session_id]:
+ if(entry['message']):
+ eventid = entry['eventid']
+ print(entry["timestamp"] + " > " + eventid + (" " * (29 - len(eventid))) + ": " + entry['message'])
+ else:
+ print(entry["timestamp"] + " > " + entry['eventid'])
+
+elif(args.verbosest): # Maximum verbosity: literally just pretty print the now-sorted-by-session logs
print(json.dumps(sessions, indent=4))
else: # Default (low) verbosity: just the good stuff
@@ -110,10 +181,13 @@ else: # Default (low) verbosity: just the good stuff
ip = session_ips[session_id]
timestamp = session_timestamps[session_id][:22].replace("T", " ") # clean up the timestamp text all pretty like
- if(session_id not in session_successful_login):
- print(timestamp + " > " + ppip(ip) + " > " + ccgen("PURPLE") + "Empty ping" + ccgen() + " lasting " + session_durations[session_id] + "s")
+ if(session_id not in session_successful_login): # Printing logic for empty pings
+ if(session_id in session_durations):
+ print(timestamp + " > " + ppip(ip) + " > " + ccgen("PURPLE") + "Empty ping" + ccgen() + " lasting " + session_durations[session_id] + "s")
+ else:
+ print(timestamp + " > " + ppip(ip) + " > " + ccgen("PURPLE") + "Empty ping" + ccgen() + " active as of processing")
- elif(not session_successful_login[session_id]):
+ elif(not session_successful_login[session_id]): # Printing logic for failed logins
cred_combos = []
for entry in sessions[session_id]:
if(entry["eventid"] == "cowrie.login.failed"):
@@ -123,27 +197,16 @@ else: # Default (low) verbosity: just the good stuff
cred_combos.append((entry["username"], pw))
print(timestamp + " > " + ppip(ip) + " > " + ccgen("RED") +"Failed login" + ccgen() + ": " + str(cred_combos))
- elif(session_id in session_ran_command):
- collapse_cmdping = False
- pingcmds = []
- if(verbosity != 1):
- for entry in sessions[session_id]:
- if(entry["eventid"] == "cowrie.command.input"):
- argv0 = entry["input"]
- if(" " in argv0):
- argv0 = entry["input"][:entry["input"].index(" ")]
- if(argv0 in ['uname', 'nproc', 'echo']):
- pingcmds.append(argv0)
- collapse_cmdping = True
-
- if(collapse_cmdping):
+ elif(session_id in session_ran_command): # Printing logic for sessions in which a command was run
+
+ if(not args.verbose and session_id not in session_not_cmdpings): # If -v is not enabled, collapse "command pings" to one-liners
cred_combos = []
for entry in sessions[session_id]:
if(entry["eventid"] == "cowrie.login.failed" or entry["eventid"] == "cowrie.login.success"):
cred_combos.append((entry["username"], entry["password"]))
- print(timestamp + " > " + ppip(ip) + " > " + ccgen("BLUE") + "Command ping" + ccgen() + ": " + str(cred_combos) + " > " + str(pingcmds))
+ print(timestamp + " > " + ppip(ip) + " > " + ccgen("BLUE") + "Command ping" + ccgen() + ": " + str(cred_combos) + " > " + str(session_cmdping_commands[session_id]))
- else:
+ else: # Printing logic for un-collapsed sessions
print(timestamp + " > " + ppip(ip) + " > " + ccgen("CYAN") + "Session" + ccgen() + ":")
for entry in sessions[session_id]:
match(entry["eventid"]):
@@ -165,7 +228,7 @@ else: # Default (low) verbosity: just the good stuff
print(" | Saved file " + df + " to " + entry["outfile"][:40])
print(" X Disconnected after " + session_durations[session_id] + "s")
- else:
+ else: # Printing logic for successful logins in which no commands were run
cred_combos = []
for entry in sessions[session_id]:
if(entry["eventid"] == "cowrie.login.failed" or entry["eventid"] == "cowrie.login.success"):