Diaries

Published: 2026-10-08

Reconstructing AI Agent Activity: Two New Scripts for Forensic Review

We just did a major update to FOR577 and added a lot of new material on day 5 about investigating AI usage in incident response. In the new material we dicsuss 8 of the most popular AI coding assistants and agents including Claude Code, Codex, Gemini CLI, Cursor, Copilot, Warp, Windsurf, and Qwen Code. I've been using Claude Code and a little bit of Codex, but I also have recently been playing with OpenCode and am setting up Hermes. I decided to figure out where the chat history was in OpenCode and where any Hermes evidence might be located.

I’ve added two new Python scripts to my GitHub repository for examining the records left behind by these two tools: opencode-chat-replay.py[1] and hermes_forensic_extract.py[2]. I absolutely had OpenCode write the opencode script and Hermes wrote the hermes script (which explains some of the differences between them). 

Both focus on reconstruction, but they approach it differently. The opencode script turns stored session data into readable chat transcripts or structured exports. The Hermes script extracts a broader collection of evidence, including conversation records, model usage, API request dumps, and application logs.

These are forensic review tools, not tools for running or replaying an agent’s actions (the title of the opencode script not withstanding, opencode named it). The goal is to make recorded activity accessible for investigation: what the user asked, what the assistant returned, what tool activity was captured, and what surrounding context remains available.

Both scripts require Python 3.10 or later and use only the Python standard library. Both tools allow use of --start and --end (in YYYY-mm-dd [HH[:MM[:SS]]] format) to narrow the time range of the extraction, and -f/--file to point to a specific sqlite db (which is probably insufficient for the hermes script, so may be removed in the future), or -d/--dir to point to the home directory (though the --hermes-home switch probably does this better for the hermes script) where the evidence can be found, e.g. from a mounted image or a tarball collected from a system under investigation.

opencode Chat Replay: From SQLite to a Readable Conversation

opencode-chat-replay.py reconstructs session transcripts from opencode’s SQLite database, located by default at:

~/.local/share/opencode/opencode.db

The script supports both the separate message and part tables and the newer consolidated session_message storage. When consolidated storage is populated for a session, it uses that instead of the separate message and part records.

That distinction matters because the transcript is more than a list of text messages. Stored turns can contain text, assistant reasoning, tool calls, completion information, errors, and token or cost accounting.

Start with the session inventory

Running the script without a selector lists sessions. You can also request that explicitly:

python opencode-chat-replay.py --list

The listing includes session IDs, slugs, message and part counts, creation times in UTC, and titles.

From there, select the most recently created session, a specific session ID, or a slug:

# Most recently created session

./opencode-chat-replay.py --latest

# Exact session ID or unique ID prefix

./opencode-chat-replay.py --session ses_f074ee11

# Slug selection

./opencode-chat-replay.py --slug swift-star

Ambiguous session ID prefixes abort rather than silently selecting a session. Slug selection first looks for an exact match, then uses a case-insensitive substring match.

Readable reports or structured exports

Markdown is the default output format for this script (again, a choice made by the tool, I will probably change it to json later). Each transcript starts with session metadata, including the session ID, slug, working directory, version, timestamps, cost, tokens, and turn count.

The conversation follows in numbered role sections. Reasoning and tool calls appear in collapsible <details> blocks, keeping the main transcript readable while retaining access to the supporting material.

For a compact review copy:  

python opencode-chat-replay.py --latest --no-reasoning --no-tools --out report.md

For structured analysis:

python opencode-chat-replay.py --slug swift-star --format json --out chat.json

JSON output contains a session object and an array of turns. One of the design goals for this script was to be able to reproduce the output of opencode export <session> on an investigative system where opencode was not installed. JSONL output places one turn on each line, with session metadata embedded in each record:

python opencode-chat-replay.py --all --format jsonl --out-dir ./exports/transcripts

That exports every session to a separate file named using the session date and slug.

Tool input and output rendering is capped at 2,000 characters by default. Truncation is explicitly annotated rather than silently dropping the remainder. To remove the cap:

python opencode-chat-replay.py \ --latest \ --max-tool-output 0 \ --out full-transcript.md

The --include-children option also attaches child-session metadata when examining sessions that have associated sub-sessions.

Working with the exported data

The structured formats make targeted review straightforward. For example, extract user prompts from a JSON transcript:

jq -r ' .turns[] | select(.role == "user") | .parts[] | select(.type == "text") | .text ' chat.json

Or identify turns with recorded errors in JSONL:

jq -c ' select(.turn.error != null) | {id: .turn.id, role: .turn.role, error: .turn.error} ' chat.jsonl

Source handling

Before querying SQLite, both scripts copy the sqlite database and any present -wal and -shm sidecars into a temporary directory (this works around a potential issue in opening the db read-only, that still might result in a write to the database due to partially staged results when we don't want to potentially tamper with the evidece). It opens that snapshot with SQLite’s read-only URI mode and removes the temporary directory on exit.

It does not write to or delete files in the original opencode data location. A custom database path also allows examination of a copied database:

python opencode-chat-replay.py --db /mnt/evidence/opencode.db --list

Original epoch-millisecond timestamps remain in JSON and JSONL output; Markdown renders timestamps in ISO-8601 UTC.

The script never opens auth.json. That does not make the resulting transcript nonsensitive: recorded tool inputs and outputs may still contain secrets.

Hermes Forensic Extractor: Conversations Plus Supporting Artifacts

hermes_forensic_extract.py takes a broader extraction approach.

Its documented sources include:

  • ~/.hermes/state.db for sessions, messages, and model usage.

  • ~/.hermes/sessions/request_dump_*.json for full LLM API request and response payloads.

  • ~/.hermes/logs/*.log for agent, error, gateway, and GUI activity.

The output is newline-delimited JSON, with one JSON object per line.

Each record identifies its source category through _extraction_type, such as session, message, model_usage, request_dump, or log_entry. Records also carry extraction and source-file metadata, original fields, parsed JSON variants, and timestamp representations.

Binary fields are represented as base64 with an accompanying binary flag.

Extract broadly, then narrow the review

The basic extraction command writes the available data to a JSONL file:

python hermes_forensic_extract.py -o evidence.jsonl

You can narrow extraction using a session ID substring:

python hermes_forensic_extract.py --session 20261005_140841_b266b9 -o session_evidence.jsonl

Additional filters include an exact source match, a Unix timestamp range, and a maximum record count per source. Source toggles let you include or exclude particular categories.

For example, extract only request dumps or only application logs:

python hermes_forensic_extract.py --only-dumps -o api_dumps.jsonl

python hermes_forensic_extract.py --only-logs -o app_logs.jsonl

Request dumps provide a different view from stored conversation messages: they include API payload material such as headers, bodies, model information, tools, and messages sent to the provider.

The log extraction adds application context, with parsed fields for component, timestamp, level, session ID, logger, and message.

Profiles are separate evidence locations

Hermes profiles have their own databases, session directories, logs, and configuration under:

~/.hermes/profiles/<name>/

The extractor can target a specific profile directly:

python hermes_forensic_extract.py --hermes-home ~/.hermes/profiles/suspect_profile -o profile_evidence.jsonl

This makes the selected Hermes home explicit rather than treating the default directory as the only location of interest.

Review with jq

Extract user message content:

jq ' select(._extraction_type == "message" and .role == "user") | .content ' evidence.jsonl

Review logged errors:

jq ' select(._extraction_type == "log_entry" and .level == "ERROR") ' evidence.jsonl

Summarize the extracted record categories:

jq -s ' group_by(._extraction_type) | map({type: .[0]._extraction_type, count: length}) ' evidence.jsonl

The extractor opens state.db in read-only mode and does not write to Hermes data directories. Its _extracted_at and _source_file fields document when records were extracted and where they came from.

Unlike the opencode script’s documented temporary-snapshot process, the Hermes README describes read-only database access without documenting an equivalent database-and-sidecar snapshot step.

Two Tools, Two Views of Agent Activity

The distinction between these scripts is deliberate:

Script Primary purpose Output
opencode-chat-replay.py Reconstruct opencode conversations from stored session data Markdown, JSON, JSONL
hermes_forensic_extract.py Extract Hermes conversations and supporting application artifacts NDJSON/JSONL

For opencode, the emphasis is moving from database records to an understandable conversation, with structured exports available for deeper analysis.

For Hermes, the emphasis is collecting multiple artifact types into a common, filterable format while retaining source and extraction metadata.

Neither should be read as proof that every action or every interaction was recorded. They expose the artifacts available in their documented sources. The useful investigative question is not just what appears in the export, but which source supports it—and what context is still outside that export.

References:

1. https://github.com/clausing/scripts/blob/master/opencode-chat-replay.py

2. https://github.com/clausing/scripts/blob/master/hermes_forensic_extract.py

---------------
Jim Clausing, GIAC GSE #26
jclausing --at-- isc [dot] sans (dot) edu

0 Comments

Published: 2026-10-07

Scans for Atlassian vulnerablity (CVE-2026-21589)

On October 5th, Atlassian published patches for multiple products to fix an "Arbitrary File Access" vulnerability [CVE-2026-21589]. An attacker can read arbitrary files in the web application's directory, potentially exposing sensitive information such as configuration files.

This directory traversal vulnerability is a little bit different from the textbook case. Atlassian products replace slashes with the pattern "::". To avoid this issue, but may, in some cases, undo this escape to access files.  Watchtowr has a great write-up with all the details and proof-of-concept URLs demonstrating the vulnerability [Watchtowr].

Starting yesterday, we saw some exploit attempts hitting our honeypot, using the exploit URLs mentioned in the Watchtowr blog. 

/download/resources/jira.webresources:color-picker-popup/images/..::..::..::..::..::WEB-INF::web.xml
/s/1.0/_/download/resources/com.atlassian.bitbucket.server.bitbucket-webpack-INTERNAL:avatar/avatar/..::..::..::..::..::WEB-INF::urlrewrite.xml
/s/1/_/download/resources/com.atlassian.confluence.plugins.dashboard-actions/images/..::..::..::..::..::..::..::..::WEB-INF::web.xml

One condition for successful exploitation is that the file the user attempts to access exists. The exploit uses "WEB-INF/web.xml" as it is a required file for Tomcat applications, and can be used similarly to "/etc/passwd". The "/etc/passwd" file will not work in this case. Access is restricted to the web application's directory. The "::" pattern used in the exploit will be translated to "/" on the server, leading to the directory traversal.

Based on the timing and the targets hit, I believe these scans are all triggered by the same threat actor. Oddly enough, all the source IPs are associated with Digital Ocean. The source IPs I see from our honeypots:

134.199.229.190
134.199.230.82
137.184.112.247
137.184.33.84
143.198.103.58
143.198.132.93
146.190.169.1
146.190.172.250
159.223.199.218
164.92.68.152
209.38.147.216
24.199.101.184
64.23.172.129 

--
Johannes B. Ullrich, Ph.D. , Dean of Research, SANS.edu
Twitter|

1 Comments

Published: 2026-10-06

More RMM Tools In the Wild

It seems that a trend started… I continue my journey discovering more RMM ("Remote Management & Monitoring") tools abused by threat actors! A few days ago, I wrote a diary[1] about ScreenConnect used in the wild. Today, I found another one.

Same scenario, it started with a phishing email that delivers a fake PDF invoice to the victim:

When the PDF is opened, it just redirect to a malicious VBS file. Indeed, the PDF contains an “OpenAction” and “URI” keywords, that sounds weird! 

remnux@remnux:~/files/samples$ pdf-parser.py Transaction\ Receipt\ .pdf -o 3
obj 3 0
Type: /Page
Referencing: 1 0 R, 2 0 R, 4 0 R

  <<
    /Type /Page
    /Parent 1 0 R
    /Resources 2 0 R
    /MediaBox [0 0 595.2799999999999727 841.8899999999999864]
    /Annots
      <<
        /Type /Annot
        /Subtype /Link
        /Rect [0. 841.8899999999999864 595.2799999999999727 71.3010032362460606]
        /Border [0 0 0]
        /A
          <<
            /S /URI
            /URI (hxxps://up-theta-rose.vercel[.]app/adobe_new_update.vbs)
          >>
      >>
    ] /Contents 4 0 R
  >>

The URL will be visited thanks to the OpenAction. This is a common trick to avoid writing URLs in email bodies that can be easily detected.

The VBS file is pretty simple and even not obfuscated. It will display another PDF as a decoy: a non-blurred version of the initial attachment.

In parallel, a MSI archive will be downloaded and installed:

hxxps://up-theta-rose.vercel[.]app/action1.msi

The MSI file contains 4 files that are not reported as malicious by VT:

$ sha256sum *
eaff35d250c9b04f51c971e70082740dbfeee5dd846829d541f588ad43378727  a1_7z_dll_file
996b01e15f85e165899630721a141b178a9c372b6e878012180ec9e9d4e7bd06  a1_sas_dll_file
1b19115d5ebdc216e0ab3adf2c643648cfc70a385f4caf0217c679f9f3b20342  action1_remote_exe
941695d20d82dd5d62f74b0111feb23720637202f6c797df2a02e2cb6cb6e8e3  main_service_exe

These files belongs to the RMM tool developed by Action1[2] and are signed with an "Action1 Corporation" certificate that expired in May 2026. 

The tool installs itself as a service for persistence ("A1Agent" - "Action1 Agent"), executing C:\Windows\Action1\action1_agent.exe.

The registy key "HKLM\Software\Action1\Agent" contains the values: CustomerId, Certificate, PrivateKey, MSI & INSTALLDIR.

The CustomerID is: 49b18106-681d-456a-b098-092e2818c09a and is connecting to the Action1 infrastructure via server[.]na-2.action1[.]com.

We are facing here the same behaviour: the threat actor abuse the cloud infrastructure of the company developing the RMM tool, probably using a free/test account.

[Update 15:11 CET]

A second sample reached my mailbox, this take mimicking a DHL document:

The URL in the PDF is similar, it contains a URL (hxxps://update-two-tau[.]vercel[.]app/adobe-ne) pointing to a ZIP archive with an HTA script. It delivers the same MSI file.

[1] https://isc.sans.edu/diary/ScreenConnect+Client+Abused+by+Attackers/33388
[2] https://www.action1.com/remote-access/

Xavier Mertens (@xme)
Senior ISC Handler | SANS Principal Instructor | Freelance Consultant
Xameco | PGP Key

0 Comments

Published: 2026-10-04

TTY Logs and the Data it Captures

For an experiment, I created a script [1] that parses and send the TTY logs collected from actors or bots activity that run various commands after they successfully login the DShield sensor. Those TTY logs are sent daily at the end of each day to the DShield SIEM [2] to be correlated with all the data. 

The following ES|QL query provides a summary of all contab commands matching a TTYLog hash performed by different actors while logged in the sensor over a 90 day period. 

TTYLogs Correlation

FROM cowrie* 
| WHERE transaction.id == "f904275333aeac48d7df6cf53fe5fb9212c7d132a7d37253d2ab9321ba2690d8"
| WHERE event.hash IS NOT NULL
| KEEP transaction.id, event.hash
| STATS Total=COUNT(event.hash) BY event.hash, transaction.id
| SORT Total DESC


This transaction ID captured 5 similar crontab commands that are translated from its hash equivalent into this list executed by more than 3130 different actors (IPs):

TTYLogs Sources

transaction.id: f904275333aeac48d7df6cf53fe5fb9212c7d132a7d37253d2ab9321ba2690d8 over a 90 day period

Other example of Event Hash decoded and sent to DShield SIEM for analysis

Top 10 Indicators

      IP                      ASN
102.88.137.80        29465
42.96.20.16            131423
182.253.221.210    38482
46.188.119.26         8334
159.223.97.218       14061
185.158.22.150       210022
193.233.48.169       207713
209.99.190.200       402253
45.64.74.51              55933
202.152.148.27        23951

[1] https://github.com/bruneaug/DShield-Sensor/blob/main/sensor_scripts/daily_tty.sh
[2] https://github.com/bruneaug/DShield-SIEM
[3] https://www.elastic.co/docs/reference/query-languages/esql

-----------
Guy Bruneau IPSS Inc.
My GitHub Page
Twitter: GuyBruneau
gbruneau at isc dot sans dot edu

0 Comments

Published: 2026-10-04

User Agent Strings Curiosities

Sometimes I have to smile, or my interest is triggered, when I review new User Agent Strings in the honeypot logs.

Like when I see an "authorized" scan:

Or when I'm owned for the umpteenth time:

I regularly see URLs or email addresses for when you want to know more, or get in touch, with the persons behind a scanner:

(around the end of this list, you'll see the Belarus email address we wrote about recently)

Many variants of masscan:

Even a KGB variant.

As you can guess, "scan" is a popular word to include in your UAS:

And some wordplays are thrown in:

And they do not shy away from discrediting:

Sometime complete lists of User Agent Strings are used: the scanner will select a new UAS for each request. They don't always sanitize these list, as you can see with these weird "User Agent Strings":

These lines actually appear in this repository of User Agent Strings, to separate them in groups:


And because of a lack of quality control, these separator lines also get used as UAS in a request.

Of course, there are also attempts to exploit the parsing of a User Agent String. Shellshock may be more than 10 years old, I still see it in User Agent Strings:

And sometimes I think: "Huh, are they scanning for this too?". Like the last one:

Scanning for servers that stream GPS correction data via the NTRIP protocol (a NTRIP header was also included in this request).

 

 

 

 

Didier Stevens
Senior handler
blog.DidierStevens.com

0 Comments

Published: 2026-10-03

YARA-X 1.21.0 Release

YARA-X's 1.21.0 release brings 5 improvements and 4 bugfixes.

One improvement is allowing stdin for CLI option --scan-list.

This allows one to generate a list of folders to scan, and pass it via a pipe. Like this example (Windows) to scan all folders with "sample" in their name:

dir /s /b /a:d c:\*samples* | yr.exe scan --scan-list - rules.yara

Didier Stevens
Senior handler
blog.DidierStevens.com

0 Comments

Published: 2026-10-01

ScreenConnect Client (Ab)used by Attackers

Threat Actors do not always use top-notch techniques or very complex malware to perform their attacks. Sometimes, they just abuse of existing applications...

I received a very simple phishing email:

From: contact@mejuri[.]com
To: <redacted>
Subject: EFT Wire Transfer

Paid Invoice Receipt

Dear Customer,
Payment of $5745.65 was Received.
Please click here to view your Order Information in PDF
If this charge wasn't authorized by you, contact our customer service to cancel and
receive an immediate refund.

Digitally Yours,
Customer Support: +1(332)638474823

“Click here” is a link pointing to:

hxxps://thelittlecupandsaucer[.]com[.]au/ScreenConnect.ClientSetup.exe

This email passed all the basic security controls. The link points to a real PE file. Today this attack vector will be blocked by browsers because downloaded an executable is suspicious!

The PE file was unknown on VT so I did a quick analysis of it. It’s a legit application: a ScreenConnect[1] client preconfigured to call-back a test account operated by the Attacker. Here is the configuration extracted from the PE file:

 

Parameter

Value

Relay (h)

instance-v2e3e2-relay.screenconnect.com

Port (p)

443

Instance ID

v2e3e2 (ConnectWise-hosted cloud)

Instance key (k)

RSA-2048 public key, blob SHA256 16b1cec1…9b00ead7

The PE is signed by ConnectWise, LLC (DigiCert G4 Code Signing CA1). The Authenticode digest matches the signed digest exactly. There's no overlay and nothing appended to or injected into the certificate table, so the signed-but-tampered config trick isn't used here.

Such tools are a gold mine for attackers because they are easy to deploy and trusted by most used! The list of “RMM” (Remote Monitoring and Management) tools is huge. Here is a brief list of the well-known ones;

  • ScreenConnect
  • AnyDesk
  • TeamViewer
  • LogMeIn
  • Bomgar (BeyondTrust Remote Support)
  • Zoho Assist
  • Remote utilities like rutserv.exe
  • NetSupport Manager
  • SimpleHelp

If you want a better overview, check LOLRMM project [2] that maintains a list similar to the LOLBAS project!

[1] https://www.screenconnect.com
[2] https://lolrmm.io

Xavier Mertens (@xme)
Senior ISC Handler | SANS Principal Instructor | Freelance Consultant
Xameco | PGP Key

2 Comments