Back to blog
Dirk

Detecting Malicious VS Code Extensions: Useful Artifacts

Looking for evidence of extension installation and execution

LabsVS CodeSupply Chain

Malicious VS Code extensions pose a real threat to enterprises and individuals. This post aims to provide defenders with some insight into the telemetry and artifacts they might find useful to hunt for known bad extensions. It will not go into specific techniques used by these extensions, as they vary in nature and are already covered in earlier (academic) work1. The artifacts and rules in this post are scoped to Windows only.

Finding Installed Extensions

To check what extensions are installed, I prefer to use OSQuery. This already has a table dedicated to VS Code extensions.

installed-extensions.sqlView on GitHub
SELECT
  u.username,
  u.uuid AS user_sid,
  u.directory AS profile_path,
  lower(v.name) AS extension_id,
  v.version,
  v.publisher,
  v.path,
  CASE
    WHEN v.installed_at > 0
    THEN datetime(v.installed_at / 1000, 'unixepoch')
  END AS installed_at_utc,
  v.prerelease,
  v.vscode_edition
FROM users AS u
CROSS JOIN vscode_extensions AS v USING (uid)
WHERE v.name != ''
ORDER BY u.username, v.name;

Alternatively, VS Code will also have a file with the installed extensions at %userprofile%\.vscode\extensions\extensions.json (which is probably used by OSQuery in the background). An example of what this file looks like is given below.

extensions.json · lines 2–26View on GitHub
⋮ 1 line above
    {
        "identifier": {
            "id": "ritwickdey.liveserver",
            "uuid": "46cc5bbd-b098-4568-9b87-f91e07d26f2d"
        },
        "version": "5.7.10",
        "location": {
            "$mid": 1,
            "path": "/C:/Users/vscode-lab/.vscode/extensions/ritwickdey.liveserver-5.7.10",
            "scheme": "file"
        },
        "relativeLocation": "ritwickdey.liveserver-5.7.10",
        "metadata": {
            "installedTimestamp": 1790413313238,
            "source": "gallery",
            "id": "46cc5bbd-b098-4568-9b87-f91e07d26f2d",
            "publisherId": "17fd9a78-e430-4a78-add2-ade4a8830352",
            "publisherDisplayName": "Ritwick Dey",
            "targetPlatform": "undefined",
            "updated": false,
            "private": false,
            "isPreReleaseVersion": false,
            "hasPreReleaseVersion": false
        }
    },
⋮ 128 lines below

Finding Evidence of Execution

Most EDRs will already collect relevant process telemetry. In particular, processes with code.exe might be of use here. However, I was not able to easily get sufficient information from these events, such as which exact extension executed.

Luckily, VS Code provides us with a log file at %appdata%\Code\logs\<timestamp>\window<#>\exthost\exthost.log. An example of how this file is structured is given below.

exthost.logView on GitHub
2026-07-28 15:52:09.489 [info] Extension host with pid 11216 started
2026-07-28 15:52:17.289 [info] ExtensionService#_doActivateExtension vscode.github-authentication, startup: false, activationEvent: 'onAuthenticationRequest:github'
2026-07-28 15:52:17.559 [info] ExtensionService#_doActivateExtension vscode.emmet, startup: false, activationEvent: 'onLanguage'
2026-07-28 15:52:17.736 [info] ExtensionService#_doActivateExtension saoudrizwan.claude-dev, startup: false, activationEvent: 'onLanguage'
2026-07-28 15:52:45.092 [info] ExtensionService#_doActivateExtension Anthropic.claude-code, startup: false, activationEvent: 'onWebviewPanel:claudeVSCodePanel'
2026-07-28 15:52:53.150 [info] ExtensionService#_doActivateExtension eamodio.gitlens, startup: false, activationEvent: 'onView:gitlens.views.commitDetails'
2026-07-28 15:53:06.792 [info] ExtensionService#_doActivateExtension vscode.git-base, startup: true, activationEvent: '*', root cause: vscode.git
2026-07-28 15:53:06.807 [info] ExtensionService#_doActivateExtension WakaTime.vscode-wakatime, startup: true, activationEvent: '*'
2026-07-28 15:53:08.261 [info] ExtensionService#_doActivateExtension vscode.git, startup: true, activationEvent: '*'
2026-07-28 15:53:08.624 [info] ExtensionService#_doActivateExtension vscode.github, startup: true, activationEvent: '*'
2026-07-28 15:53:08.836 [info] ExtensionService#_doActivateExtension vscode.debug-auto-launch, startup: false, activationEvent: 'onStartupFinished'
2026-07-28 15:53:09.041 [info] ExtensionService#_doActivateExtension vscode.merge-conflict, startup: false, activationEvent: 'onStartupFinished'
2026-07-28 15:53:09.473 [info] ExtensionService#_doActivateExtension esbenp.prettier-vscode, startup: false, activationEvent: 'onStartupFinished'
2026-07-28 15:53:09.512 [info] ExtensionService#_doActivateExtension ritwickdey.LiveServer, startup: false, activationEvent: 'onStartupFinished'
2026-07-28 15:53:21.445 [info] Eager extensions activated

Every time the VS Code extension host starts, it will create a log file with which extensions launched, along with the reason they started (activationEvent). The startup field indicates whether VS Code classified that particular activation request as a startup activation. Note that VS Code does not classify onStartupFinished activity as such a startup request, even though it will start along with VS Code regardless of user action.

Onboarding to Elastic

For quick parsing, we can use ESQL. This allows us to pull the file in without an ingest pipeline and parse it on the fly, which might be preferred in an Incident Response scenario.

parse-exthost.esqlView on GitHub
Show code (29 lines)
  FROM logs-filestream*
  | WHERE data_stream.namespace == "windows_vscode"
      AND message LIKE "*ExtensionService#_doActivateExtension*"
  | GROK message """%{TIMESTAMP_ISO8601:vscode_time_text} \[%{LOGLEVEL:log_level}\] %{GREEDYDATA:vscode_message}"""
  | GROK vscode_message """ExtensionService#_doActivateExtension %{DATA:extension_id}, startup: %{WORD:startup_text}, activationEvent: '%{DATA:activation_event}'(?:, root cause: %{GREEDYDATA:root_cause})?"""
  | WHERE extension_id IS NOT NULL
  | EVAL `@timestamp` = DATE_PARSE("yyyy-MM-dd HH:mm:ss.SSS", vscode_time_text),
         `event.kind` = "event",
         `event.category` = "package",
         `event.type` = "start",
         `event.action` = "extension-activated",
         `event.module` = "vscode",
         `event.provider` = "vscode",
         `event.original` = message,
         `log.level` = log_level,
         `package.name` = extension_id,
         `vscode.extension.startup` = TO_BOOLEAN(startup_text),
         `vscode.extension.activation_event` = activation_event,
         `vscode.extension.root_cause` = root_cause
  | KEEP @timestamp,
         event.kind, event.category, event.type, event.action,
         event.module, event.provider, event.dataset, event.ingested, event.original,
         host.name, log.level, log.file.path,
         package.name,
         vscode.extension.startup,
         vscode.extension.activation_event,
         vscode.extension.root_cause
  | SORT @timestamp DESC
  | LIMIT 1000

For continuous monitoring, an ingest pipeline is preferred. An example pipeline is available on GitHub.

In Fleet, you can create a simple filestream integration to ingest C:/Users/*/AppData/Roaming/Code/logs/*/window*/exthost/exthost.log. Under Ingest Pipeline, specify the pipeline ID. Under Advanced options, be sure to add the locale processor for the timestamp to be changed into UTC.

Now, you can easily query which extensions have been activated with the below query.

launched-extensions.esqlView on GitHub
  FROM logs-filestream.generic-windows_vscode
  | WHERE event.action == "extension-activated"
  | KEEP @timestamp, event.ingested, host.name,
      package.name,
      vscode.extension.activation_event,
      vscode.extension.startup,
      message
  | SORT @timestamp DESC
  | LIMIT 1000

Finding Malicious Extensions

A list of all extensions that have been removed from the VS Marketplace is published here, along with the reason for removal. Once Microsoft has removed an extension and flagged it as malicious, it will be uninstalled from the system. Therefore, some of the telemetry sources discussed previously (such as extensions.json) will become less useful if not ingested prior to an extension's removal.

Ingesting Removed Extensions

To ingest and parse the removed extension list, we will prepare two indexes in Elastic: one to stage the list (and use an ingest pipeline to parse it), and another one to actually hold a document per unique extension in the list. The latter will be used in subsequent detection logic to cross-reference telemetry from our hosts.

The ingest pipeline used for parsing can be found here. Then there's also the staging index template and the index template for the snapshot. Note that you may want to implement an index lifecycle management policy on these indexes to avoid old indexes from building up. When setting up ILM, turn off rollover (on by default), as these indexes don't have a rollover alias.

To tie these things together, I created a workflow. This fetches the list from GitHub, puts everything in the staging index, then creates a document for each of the extensions in the snapshot index. Further, an alias vscode-removed-current is used to always point to the most recent index. A high-level overview of the workflow is given below.

Removal list

RemovedPackages.md

fetch & parse
Elastic

Staging index

1 doc per row

group by extension

Snapshot index

1 doc per extension

repoint alias

Alias

vscode-removed-current

After running this workflow, we can create a data view for this alias and inspect it in Discover:

Alerting

Now that we have all the ingredients we need, we can combine them to create an alert. The goal is to correlate our vscode-removed-current index against what we have installed, or what ran in our environment.

First, we just need to normalize the OSQuery results. The results are already saved in Elastic, but we just need to make sure the field name vscode.extension.id matches. Then, we will create a scheduled pack so that the query will run every 15 minutes.

The normalization is done using this ingest pipeline. And a pack can be created in the OSQuery Manager. Using this integration, we can easily have Elastic Agents report periodically with OSQuery data. An example of such a pack is provided on GitHub.

Next, we can create two indicator match rules: one for if a removed extension is seen installed, and one for if a removed extension is executed. Note that these rules will only fire for extensions removed for security-related reasons.

To test it, we install and activate an extension from our VSMEx dataset2. This gives us two alerts:

Conclusion

Using a few specific files, we can fill the detection gap left behind by standard process monitoring. Using the extensions.json file, we learn what VS Code extensions are installed on a system. The exthost.log file gives insight into what extensions actually executed.

By ingesting these files and cross-referencing Microsoft's removed extension list, we are able to alert on known malicious extensions being installed or executed on a system.

Footnotes

  1. Kotaiba Alachkar, Dirk Gaastra, Karlo Zanki, Marc Ohm, Eduardo Barbaro, and Yury Zhauniarovich. 2026. Dissecting Malicious VS Code Extensions: Characterization and Classification. In Proceedings of the 23rd International Conference on Security and Cryptography (SECRYPT 2026). doi.org/10.5220/0015063000004103 ↩

  2. Kotaiba Alachkar, Dirk Gaastra, Olga Gadyatskaya, Eduardo Barbaro, Michel van Eeten, and Yury Zhauniarovich. 2026. VSMEx: A Collection Tool and a Dataset of Malicious VS Code Extensions. In Proceedings of the Sixteenth ACM Conference on Data and Application Security and Privacy (CODASPY '26). doi.org/10.1145/3800506.3803487 ↩