EDR telemetry tells you what happened on the endpoint: PowerShell ran, an MSI installed, an executable made a network connection. It often can’t tell you how the user got there. The browser’s history database usually can.

Use it to answer questions like:

  • What site did the user visit right before the alert?
  • Was the file downloaded through the browser, and from which URL?
  • Was there a redirect or a fake verification page before execution?
  • Did a legitimate search lead to a malicious result?
  • Does the browser activity support a phishing, ClickFix, scareware, or malicious-download story, or not?

The whole workflow:

Detection -> identify user -> collect browser DB -> reconstruct visits and downloads
          -> correlate with EDR -> determine initial access -> remediate -> document

1. Find the browser and profile that were actually used

Don’t assume Chrome and Default. Users run Edge, Brave, or Firefox, and Chromium browsers keep one database per profile (Default, Profile 1, Profile 2…). The most recently written History file is usually the one that matters.

From a remote response shell:

Get-ChildItem -Force -ErrorAction SilentlyContinue -Path @(
    'C:\Users\*\AppData\Local\Google\Chrome\User Data\*\History',
    'C:\Users\*\AppData\Local\Microsoft\Edge\User Data\*\History',
    'C:\Users\*\AppData\Local\BraveSoftware\Brave-Browser\User Data\*\History',
    'C:\Users\*\AppData\Roaming\Mozilla\Firefox\Profiles\*\places.sqlite'
) | Sort-Object LastWriteTimeUtc -Descending |
    Select-Object LastWriteTimeUtc, Length, FullName |
    Format-Table -AutoSize -Wrap

A History file written a minute before the alert is a strong lead. Check which browsers are running too:

Get-Process chrome, msedge, brave, firefox -ErrorAction SilentlyContinue |
    Select-Object Name, Id, StartTime | Format-Table -AutoSize

For PowerShell patterns that keep this output readable, see PowerShell for Responders.

2. Collect a working copy

The browser holds the database open while it runs, so copy it first and collect the copy. Include the -journal file if one exists: it can hold changes that haven’t been written into the main database yet.

$src  = 'C:\Users\<user>\AppData\Local\Google\Chrome\User Data\Default\History'
$dest = 'C:\Windows\Temp\<user>_Chrome_History.db'
Copy-Item -LiteralPath $src -Destination $dest
Copy-Item -LiteralPath "$src-journal" -Destination "$dest-journal" -ErrorAction SilentlyContinue
Get-FileHash $dest -Algorithm SHA256

If the copy fails because the file is locked, a volume shadow copy read usually works:

esentutl.exe /y "C:\Users\<user>\AppData\Local\Google\Chrome\User Data\Default\History" /vss /d "C:\Windows\Temp\<user>_Chrome_History.db"

Then pull the copy through your EDR’s file retrieval, for example get in a CrowdStrike RTR session or Fetch File in SentinelOne. Record the hash, and remove the temp copy from the endpoint once it’s collected.

On your side, keep the collected file untouched and analyze a second copy.

3. Open it in DB Browser for SQLite

Open the working copy with File > Open Database Read-Only, then use the Execute SQL tab. The Chromium tables that matter:

Table What it holds
urls Each unique URL: title, visit count, last visit
visits Every individual visit, how it happened (transition), and which visit led to it (from_visit)
downloads Downloaded files: path, size, state, referrer, tab URL
downloads_url_chains Every URL in a download’s redirect chain
keyword_search_terms Search terms typed into the address bar

Chromium stores time as microseconds since 1601-01-01 UTC. Every query below converts it the same way:

datetime(visit_time / 1000000 - 11644473600, 'unixepoch')

Don’t review the whole database. Work the window around the alert.

4. Build the browser timeline

Visits in order, with how each one happened and where it came from:

SELECT
    datetime(v.visit_time / 1000000 - 11644473600, 'unixepoch') AS visit_utc,
    u.url,
    u.title,
    CASE v.transition & 0xFF
        WHEN 0 THEN 'link'      WHEN 1 THEN 'typed'
        WHEN 2 THEN 'bookmark'  WHEN 5 THEN 'generated (search)'
        WHEN 7 THEN 'form submit'  WHEN 8 THEN 'reload'
        ELSE v.transition & 0xFF
    END AS how,
    prev.url AS came_from
FROM visits v
JOIN urls u ON v.url = u.id
LEFT JOIN visits pv ON v.from_visit = pv.id
LEFT JOIN urls prev ON pv.url = prev.id
WHERE v.visit_time BETWEEN
      (strftime('%s', '2026-01-01 17:45:00') + 11644473600) * 1000000
  AND (strftime('%s', '2026-01-01 18:05:00') + 11644473600) * 1000000
ORDER BY v.visit_time;

Set the two timestamps (UTC) to a window around the detection, typically 10 to 15 minutes before and a few minutes after. The came_from column rebuilds the chain the user clicked through.

What you’re trying to establish:

Search or email -> landing page -> redirect -> download or lure -> execution

What the user searched for

SELECT
    datetime(u.last_visit_time / 1000000 - 11644473600, 'unixepoch') AS last_visit_utc,
    k.term,
    u.url
FROM keyword_search_terms k
JOIN urls u ON k.url_id = u.id
ORDER BY u.last_visit_time DESC
LIMIT 50;

Searches are often the first link in the chain: a search for a legitimate tool or service, then a malicious result or ad.

5. Review downloads

For an executable, MSI, ZIP, script, or document payload:

SELECT
    datetime(d.start_time / 1000000 - 11644473600, 'unixepoch') AS start_utc,
    d.target_path,
    d.received_bytes,
    d.total_bytes,
    d.state,
    d.referrer,
    d.tab_url,
    c.chain_index,
    c.url AS chain_url
FROM downloads d
LEFT JOIN downloads_url_chains c ON d.id = c.id
ORDER BY d.start_time DESC, c.chain_index;
  • chain_url rows are the redirect chain; the highest chain_index is the URL that actually served the file.
  • referrer and tab_url usually name the page the user was on, which is often the lure itself.
  • state 1 means the download completed.

This identifies the real delivery domain even when the payload itself carries no useful attribution.

6. Pivot on domains and lure language

Search the urls table for anything tied to the case:

SELECT datetime(last_visit_time / 1000000 - 11644473600, 'unixepoch') AS last_visit_utc,
       url, title, visit_count
FROM urls
WHERE url   LIKE '%verify%'  OR url   LIKE '%captcha%'
   OR title LIKE '%verif%'   OR title LIKE '%human%'
   OR title LIKE '%download%' OR title LIKE '%document%'
ORDER BY last_visit_time DESC;

Good terms to try: known IOCs, the payload file name, “verify”, “captcha”, “human verification”, “download”, “document”, and whatever brand the lure impersonates (fake AI tools and document portals are common).

When you get a hit, read several visits before and after it. One suspicious URL on its own isn’t proof of the infection chain.

Two patterns from real cases

ClickFix behind a fake AI page

The EDR showed malicious PowerShell and not much else. The browser history filled in the rest:

Time Browser EDR
Shortly before Searches for a popular AI chatbot
T-1 min Visit to a free-hosted site page titled as that chatbot, with a “verification” step
T+0 Malicious PowerShell launched from the user’s session

The page was hosted on a legitimate site-builder platform, so the domain on its own looked trustworthy. The sequence is what made the case: a search, a page impersonating the tool the user searched for, and PowerShell about a minute later.

A “document” page delivering an executable

Time Browser
T+0 A page titled “Downloading Document”
Then Redirect to a randomized URL on the same domain
T+6 s, T+14 s, T+28 s Three downloads of an executable named like a verification form

downloads_url_chains identified the delivery domain, a randomized subdomain on a lookalike domain. Just as important, the history didn’t show what sent the user to that first page, so the write-up didn’t claim one. Stating only what the chain supports is what makes the timeline defensible.

7. Correlate with endpoint evidence

Browser history on its own shows what the user looked at, not what executed. Line it up with:

  • process creation and script activity
  • file creation (the downloaded payload) and hashes
  • MSI or installer execution
  • DNS requests and network connections
  • detection timestamps
  • persistence
  • email telemetry and user logon sessions

A strong combined timeline reads like this:

T+0      landing page ("Downloading Document")
T+6s     executable downloaded
T+28s    repeat download
T+1m     file launched
T+2m     child process and outbound connection

That sequence is much stronger evidence than “a suspicious domain and an executable were both on the system.”

8. Let the findings drive response

Browser evidence helps decide whether to:

  • block the delivery domain
  • remove downloaded payloads and kill related processes
  • contain the endpoint
  • reset credentials and revoke sessions
  • hunt other endpoints for the same domain or hash
  • search email telemetry for the originating lure
  • close the browser activity as benign or inconclusive when no execution followed

Firefox

Firefox uses places.sqlite, with times in microseconds since 1970:

SELECT
    datetime(h.visit_date / 1000000, 'unixepoch') AS visit_utc,
    p.url,
    p.title
FROM moz_historyvisits h
JOIN moz_places p ON h.place_id = p.id
ORDER BY h.visit_date DESC
LIMIT 200;

Downloads are stored as page annotations (moz_annos) rather than a dedicated table, so the destination path sits in an annotation on the download URL.

The principle

Use the browser database to answer one question: what did the user do immediately before the endpoint activity we detected?

The EDR explains what happened on the endpoint. The browser explains how the user got there.