Skip to content

Commit d407c15

Browse files
feat(windows): ide support
Signed-off-by: Swarit Pandey <swarit@stepsecurity.io>
1 parent 7263c43 commit d407c15

6 files changed

Lines changed: 566 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
See [VERSIONING.md](VERSIONING.md) for why the version starts at 1.8.1.
99

10+
## [Unreleased]
11+
12+
### Added
13+
14+
- **JetBrains IDE detection** (macOS + Windows): IntelliJ IDEA (Ultimate & CE), PyCharm (Professional & CE), WebStorm, GoLand, PhpStorm, CLion, Rider, RubyMine, DataGrip, and Android Studio.
15+
- **Eclipse IDE detection** (macOS + Windows): Detects standard install paths including Eclipse Installer (`%USERPROFILE%\eclipse\*\eclipse`) and manual installs.
16+
- **Glob-based Windows path matching**: `detectWindows` now supports wildcard patterns in `WinPaths` for IDEs that embed version numbers in folder names (e.g., `C:\Program Files\JetBrains\GoLand 2025.1.3\`). Picks the newest installation when multiple versions are present.
17+
- **`product-info.json` version extraction**: Reads the JetBrains `product-info.json` file for accurate marketing version numbers (avoids registry build numbers).
18+
- **`.eclipseproduct` version extraction**: Reads Eclipse's `.eclipseproduct` properties file for version detection on Windows (Eclipse does not register in the Windows registry).
19+
1020
## [1.9.2] - 2026-04-15
1121

1222
### Fixed

SCAN_COVERAGE.md

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,28 @@ This document catalogs everything Dev Machine Guard detects. Contributions to ex
44

55
## IDEs & AI Desktop Apps
66

7-
| Application | Vendor | Detection Method | Version Extraction |
8-
|-----------------------|-----------|-----------------------------|---------------------------------|
9-
| Visual Studio Code | Microsoft | `/Applications/Visual Studio Code.app` | Binary `--version` |
10-
| Cursor | Cursor | `/Applications/Cursor.app` | Binary `--version` |
11-
| Windsurf | Codeium | `/Applications/Windsurf.app`| Binary `--version` |
12-
| Antigravity | Google | `/Applications/Antigravity.app` | Binary `--version` |
13-
| Zed | Zed | `/Applications/Zed.app` | `Info.plist` |
14-
| Claude Desktop | Anthropic | `/Applications/Claude.app` | `Info.plist` |
15-
| Microsoft Copilot | Microsoft | `/Applications/Copilot.app` | `Info.plist` |
7+
| Application | Vendor | macOS Detection | Windows Detection | Version Extraction |
8+
|-----------------------|-------------------|------------------------------------------|----------------------------------------------------------|-------------------------------------|
9+
| Visual Studio Code | Microsoft | `/Applications/Visual Studio Code.app` | `%PROGRAMFILES%\Microsoft VS Code` | Binary `--version` |
10+
| Cursor | Cursor | `/Applications/Cursor.app` | `%LOCALAPPDATA%\Programs\cursor` | Binary `--version` |
11+
| Windsurf | Codeium | `/Applications/Windsurf.app` | `%LOCALAPPDATA%\Programs\Windsurf` | Binary `--version` |
12+
| Antigravity | Google | `/Applications/Antigravity.app` | `%LOCALAPPDATA%\Programs\Antigravity` | Binary `--version` |
13+
| Zed | Zed | `/Applications/Zed.app` | `%LOCALAPPDATA%\Zed` | `Info.plist` |
14+
| Claude Desktop | Anthropic | `/Applications/Claude.app` | `%LOCALAPPDATA%\Programs\Claude` | `Info.plist` / Registry |
15+
| Microsoft Copilot | Microsoft | `/Applications/Copilot.app` | `%LOCALAPPDATA%\Programs\Copilot` | `Info.plist` / Registry |
16+
| IntelliJ IDEA Ultimate| JetBrains | `/Applications/IntelliJ IDEA.app` | `%PROGRAMFILES%\JetBrains\IntelliJ IDEA <ver>` | `product-info.json` / `Info.plist` |
17+
| IntelliJ IDEA CE | JetBrains | `/Applications/IntelliJ IDEA CE.app` | `%PROGRAMFILES%\JetBrains\IntelliJ IDEA Community Edition <ver>` | `product-info.json` / `Info.plist` |
18+
| PyCharm Professional | JetBrains | `/Applications/PyCharm.app` | `%PROGRAMFILES%\JetBrains\PyCharm <ver>` | `product-info.json` / `Info.plist` |
19+
| PyCharm CE | JetBrains | `/Applications/PyCharm CE.app` | `%PROGRAMFILES%\JetBrains\PyCharm Community Edition <ver>` | `product-info.json` / `Info.plist` |
20+
| WebStorm | JetBrains | `/Applications/WebStorm.app` | `%PROGRAMFILES%\JetBrains\WebStorm <ver>` | `product-info.json` / `Info.plist` |
21+
| GoLand | JetBrains | `/Applications/GoLand.app` | `%PROGRAMFILES%\JetBrains\GoLand <ver>` | `product-info.json` / `Info.plist` |
22+
| PhpStorm | JetBrains | `/Applications/PhpStorm.app` | `%PROGRAMFILES%\JetBrains\PhpStorm <ver>` | `product-info.json` / `Info.plist` |
23+
| CLion | JetBrains | `/Applications/CLion.app` | `%PROGRAMFILES%\JetBrains\CLion <ver>` | `product-info.json` / `Info.plist` |
24+
| Rider | JetBrains | `/Applications/Rider.app` | `%PROGRAMFILES%\JetBrains\JetBrains Rider <ver>` | `product-info.json` / `Info.plist` |
25+
| RubyMine | JetBrains | `/Applications/RubyMine.app` | `%PROGRAMFILES%\JetBrains\RubyMine <ver>` | `product-info.json` / `Info.plist` |
26+
| DataGrip | JetBrains | `/Applications/DataGrip.app` | `%PROGRAMFILES%\JetBrains\DataGrip <ver>` | `product-info.json` / `Info.plist` |
27+
| Android Studio | Google | `/Applications/Android Studio.app` | `%PROGRAMFILES%\Android\Android Studio` | `product-info.json` / `Info.plist` |
28+
| Eclipse IDE | Eclipse Foundation| `/Applications/Eclipse.app` | `%PROGRAMFILES%\eclipse`, `C:\eclipse`, `%USERPROFILE%\eclipse\*\eclipse` | `.eclipseproduct` / `Info.plist` |
1629

1730
## AI CLI Tools
1831

internal/detector/ide.go

Lines changed: 161 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package detector
22

33
import (
44
"context"
5+
"encoding/json"
56
"path/filepath"
7+
"sort"
68
"strings"
79
"time"
810

@@ -61,6 +63,78 @@ var ideDefinitions = []ideSpec{
6163
AppPath: "/Applications/Copilot.app",
6264
WinPaths: []string{`%LOCALAPPDATA%\Programs\Copilot`},
6365
},
66+
// JetBrains IDEs — version extracted via product-info.json (macOS + Windows)
67+
// or Info.plist fallback (macOS) or registry fallback (Windows).
68+
// Windows paths use glob patterns because folder names include the version
69+
// (e.g., "IntelliJ IDEA 2024.3.2").
70+
{
71+
AppName: "IntelliJ IDEA", IDEType: "intellij_idea_ultimate", Vendor: "JetBrains",
72+
AppPath: "/Applications/IntelliJ IDEA.app",
73+
WinPaths: []string{`%PROGRAMFILES%\JetBrains\IntelliJ IDEA 2*`},
74+
},
75+
{
76+
AppName: "IntelliJ IDEA Community Edition", IDEType: "intellij_idea_ce", Vendor: "JetBrains",
77+
AppPath: "/Applications/IntelliJ IDEA CE.app",
78+
WinPaths: []string{`%PROGRAMFILES%\JetBrains\IntelliJ IDEA Community Edition *`},
79+
},
80+
{
81+
AppName: "PyCharm", IDEType: "pycharm_professional", Vendor: "JetBrains",
82+
AppPath: "/Applications/PyCharm.app",
83+
WinPaths: []string{`%PROGRAMFILES%\JetBrains\PyCharm 2*`},
84+
},
85+
{
86+
AppName: "PyCharm Community Edition", IDEType: "pycharm_ce", Vendor: "JetBrains",
87+
AppPath: "/Applications/PyCharm CE.app",
88+
WinPaths: []string{`%PROGRAMFILES%\JetBrains\PyCharm Community Edition *`},
89+
},
90+
{
91+
AppName: "WebStorm", IDEType: "webstorm", Vendor: "JetBrains",
92+
AppPath: "/Applications/WebStorm.app",
93+
WinPaths: []string{`%PROGRAMFILES%\JetBrains\WebStorm *`},
94+
},
95+
{
96+
AppName: "GoLand", IDEType: "goland", Vendor: "JetBrains",
97+
AppPath: "/Applications/GoLand.app",
98+
WinPaths: []string{`%PROGRAMFILES%\JetBrains\GoLand *`},
99+
},
100+
{
101+
AppName: "PhpStorm", IDEType: "phpstorm", Vendor: "JetBrains",
102+
AppPath: "/Applications/PhpStorm.app",
103+
WinPaths: []string{`%PROGRAMFILES%\JetBrains\PhpStorm *`},
104+
},
105+
{
106+
AppName: "CLion", IDEType: "clion", Vendor: "JetBrains",
107+
AppPath: "/Applications/CLion.app",
108+
WinPaths: []string{`%PROGRAMFILES%\JetBrains\CLion *`},
109+
},
110+
{
111+
AppName: "JetBrains Rider", IDEType: "rider", Vendor: "JetBrains",
112+
AppPath: "/Applications/Rider.app",
113+
WinPaths: []string{`%PROGRAMFILES%\JetBrains\JetBrains Rider *`},
114+
},
115+
{
116+
AppName: "RubyMine", IDEType: "rubymine", Vendor: "JetBrains",
117+
AppPath: "/Applications/RubyMine.app",
118+
WinPaths: []string{`%PROGRAMFILES%\JetBrains\RubyMine *`},
119+
},
120+
{
121+
AppName: "DataGrip", IDEType: "datagrip", Vendor: "JetBrains",
122+
AppPath: "/Applications/DataGrip.app",
123+
WinPaths: []string{`%PROGRAMFILES%\JetBrains\DataGrip *`},
124+
},
125+
{
126+
AppName: "Android Studio", IDEType: "android_studio", Vendor: "Google",
127+
AppPath: "/Applications/Android Studio.app",
128+
WinPaths: []string{`%PROGRAMFILES%\Android\Android Studio`},
129+
},
130+
// Eclipse IDE — version extracted via .eclipseproduct (Windows)
131+
// or Info.plist fallback (macOS). Eclipse does not register in the
132+
// Windows registry and has no --version flag.
133+
{
134+
AppName: "Eclipse IDE", IDEType: "eclipse", Vendor: "Eclipse Foundation",
135+
AppPath: "/Applications/Eclipse.app",
136+
WinPaths: []string{`%PROGRAMFILES%\eclipse`, `C:\eclipse`, `%USERPROFILE%\eclipse\*\eclipse`},
137+
},
64138
}
65139

66140
// IDEDetector detects installed IDEs and AI desktop apps.
@@ -105,6 +179,11 @@ func (d *IDEDetector) detectDarwin(ctx context.Context, spec ideSpec) (model.IDE
105179
}
106180
}
107181

182+
// Fallback: product-info.json (JetBrains IDEs)
183+
if version == "unknown" {
184+
version = readProductInfoVersion(d.exec, filepath.Join(spec.AppPath, "Contents", "Resources", "product-info.json"))
185+
}
186+
108187
// Fallback: Info.plist
109188
if version == "unknown" {
110189
version = readPlistVersion(ctx, d.exec, filepath.Join(spec.AppPath, "Contents", "Info.plist"))
@@ -119,33 +198,77 @@ func (d *IDEDetector) detectDarwin(ctx context.Context, spec ideSpec) (model.IDE
119198
func (d *IDEDetector) detectWindows(ctx context.Context, spec ideSpec) (model.IDE, bool) {
120199
for _, winPath := range spec.WinPaths {
121200
resolved := resolveEnvPath(d.exec, winPath)
122-
if !d.exec.DirExists(resolved) {
201+
202+
installDir, ok := d.resolveInstallDir(resolved)
203+
if !ok {
123204
continue
124205
}
125206

126207
version := "unknown"
127208

128209
// Try version from binary
129210
if spec.WinBinary != "" && spec.VersionFlag != "" {
130-
binaryFull := filepath.Join(resolved, spec.WinBinary)
211+
binaryFull := filepath.Join(installDir, spec.WinBinary)
131212
if d.exec.FileExists(binaryFull) {
132213
version = runVersionCmd(ctx, d.exec, binaryFull, spec.VersionFlag)
133214
}
134215
}
135216

217+
// Fallback: product-info.json (JetBrains IDEs)
218+
if version == "unknown" {
219+
version = readProductInfoVersion(d.exec, filepath.Join(installDir, "product-info.json"))
220+
}
221+
222+
// Fallback: .eclipseproduct
223+
if version == "unknown" {
224+
version = readEclipseProductVersion(d.exec, filepath.Join(installDir, ".eclipseproduct"))
225+
}
226+
136227
// Fallback: registry
137228
if version == "unknown" {
138229
version = readRegistryVersion(ctx, d.exec, spec.AppName)
139230
}
140231

141232
return model.IDE{
142-
IDEType: spec.IDEType, Version: version, InstallPath: resolved,
233+
IDEType: spec.IDEType, Version: version, InstallPath: installDir,
143234
Vendor: spec.Vendor, IsInstalled: true,
144235
}, true
145236
}
146237
return model.IDE{}, false
147238
}
148239

240+
// resolveInstallDir resolves a Windows path to an install directory.
241+
// Supports glob patterns (e.g., "C:\Program Files\JetBrains\GoLand *")
242+
// for IDEs that embed version numbers in folder names.
243+
// When multiple matches exist, returns the last (newest by lexicographic order).
244+
func (d *IDEDetector) resolveInstallDir(resolved string) (string, bool) {
245+
if !strings.ContainsAny(resolved, "*?[") {
246+
if d.exec.DirExists(resolved) {
247+
return resolved, true
248+
}
249+
return "", false
250+
}
251+
252+
matches, err := d.exec.Glob(resolved)
253+
if err != nil || len(matches) == 0 {
254+
return "", false
255+
}
256+
257+
// Filter to directories only
258+
var dirs []string
259+
for _, m := range matches {
260+
if d.exec.DirExists(m) {
261+
dirs = append(dirs, m)
262+
}
263+
}
264+
if len(dirs) == 0 {
265+
return "", false
266+
}
267+
268+
sort.Strings(dirs)
269+
return dirs[len(dirs)-1], true
270+
}
271+
149272
// runVersionCmd runs a binary with a version flag and extracts the first line.
150273
func runVersionCmd(ctx context.Context, exec executor.Executor, binary, flag string) string {
151274
stdout, _, _, err := exec.RunWithTimeout(ctx, 10*time.Second, binary, flag)
@@ -177,6 +300,41 @@ func readPlistVersion(ctx context.Context, exec executor.Executor, plistPath str
177300
return "unknown"
178301
}
179302

303+
// readProductInfoVersion reads the "version" field from a JetBrains product-info.json file.
304+
// Returns "unknown" if the file does not exist or cannot be parsed.
305+
func readProductInfoVersion(exec executor.Executor, filePath string) string {
306+
data, err := exec.ReadFile(filePath)
307+
if err != nil {
308+
return "unknown"
309+
}
310+
var info struct {
311+
Version string `json:"version"`
312+
}
313+
if err := json.Unmarshal(data, &info); err != nil || info.Version == "" {
314+
return "unknown"
315+
}
316+
return info.Version
317+
}
318+
319+
// readEclipseProductVersion reads the "version" property from an .eclipseproduct file.
320+
// The file uses Java properties format (key=value per line).
321+
func readEclipseProductVersion(exec executor.Executor, filePath string) string {
322+
data, err := exec.ReadFile(filePath)
323+
if err != nil {
324+
return "unknown"
325+
}
326+
for _, line := range strings.Split(string(data), "\n") {
327+
line = strings.TrimSpace(line)
328+
if strings.HasPrefix(line, "version=") {
329+
v := strings.TrimPrefix(line, "version=")
330+
if v != "" {
331+
return v
332+
}
333+
}
334+
}
335+
return "unknown"
336+
}
337+
180338
// readRegistryVersion searches Windows Uninstall registry keys for DisplayVersion.
181339
func readRegistryVersion(ctx context.Context, exec executor.Executor, appName string) string {
182340
for _, root := range []string{

0 commit comments

Comments
 (0)