Detecting Malicious VS Code Extensions: Useful Artifacts
Looking for evidence of extension installation and execution
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.
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.
{
"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
}
},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.
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 activatedEvery 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.
Show code (29 lines)Hide code
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 1000For 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.
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 1000Finding 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
Staging index
1 doc per row
Snapshot index
1 doc per extension
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
-
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 ↩
-
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 ↩