feat(llm-security): implement JetBrains discovery + Android Studio base dir

This commit is contained in:
Kjell Tore Guttormsen 2026-04-18 10:16:28 +02:00
commit 03d61d8bca
2 changed files with 208 additions and 6 deletions

View file

@ -52,6 +52,24 @@ export function getJetBrainsBaseDir() {
return existsSync(base) ? base : null;
}
/**
* Return the Android Studio base directory. Diverges from JetBrains on Linux:
* Android Studio uses ~/.config/Google (config path) rather than ~/.local/share.
* @returns {string|null}
*/
export function getAndroidStudioBaseDir() {
const home = homedir();
let base;
if (process.platform === 'darwin') {
base = join(home, 'Library', 'Application Support', 'Google');
} else if (process.platform === 'win32') {
base = join(process.env.APPDATA || join(home, 'AppData', 'Roaming'), 'Google');
} else {
base = join(home, '.config', 'Google');
}
return existsSync(base) ? base : null;
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
@ -257,15 +275,99 @@ export async function discoverVSCodeExtensions(options = {}) {
// JetBrains discovery — stub (v1.1)
// ---------------------------------------------------------------------------
// Matches JetBrains + Android Studio product directory names. Explicitly excludes
// Fleet (separate SDK, different plugin model) and Toolbox (launcher cache dir).
const JB_PRODUCT_DIR_RE = /^(IntelliJIdea|IdeaIC|PyCharm|PyCharmCE|WebStorm|GoLand|PhpStorm|RubyMine|CLion|DataGrip|Rider|RustRover|DataSpell|Aqua|AndroidStudio)([0-9]+\.[0-9]+(?:\.[0-9]+)?)?$/;
/**
* Discover JetBrains plugins. Stub returns empty for v6.3.0.
* Discover JetBrains + Android Studio plugins from installed IDE directories.
*
* **`rootsOverride` semantics for JetBrains:** Each entry is a BASE directory
* the equivalent of `getJetBrainsBaseDir()` output (the `JetBrains/` or
* `Google/` parent). The discovery walks `<base>/<ProductDir>/plugins/<plugin>/`.
* This differs from VS Code where `rootsOverride` entries point at the
* `extensions/` level directly. Rationale: JetBrains has many product dirs per
* base (IntelliJIdea, PyCharm, GoLand, AndroidStudio), so base-level injection
* is the natural analogue for tests.
*
* Fleet and Toolbox are always excluded.
*
* @param {object} [options]
* @param {string[]} [options.rootsOverride] - Base directories (not `plugins/` level).
* @param {boolean} [options.followSymlinks=false]
* @returns {Promise<{ extensions: ExtensionRecord[], warnings: string[], rootsScanned: string[] }>}
*/
export async function discoverJetBrainsExtensions(options = {}) {
return {
extensions: [],
warnings: ['IntelliJ plugin discovery deferred to v1.1 (see knowledge/ide-extension-threat-patterns.md)'],
rootsScanned: [],
};
const warnings = [];
const extensions = [];
const rootsScanned = [];
let baseDirs;
if (Array.isArray(options.rootsOverride) && options.rootsOverride.length > 0) {
baseDirs = options.rootsOverride;
} else {
baseDirs = [getJetBrainsBaseDir(), getAndroidStudioBaseDir()].filter(Boolean);
}
for (const base of baseDirs) {
if (!await pathExists(base)) continue;
rootsScanned.push(base);
let productEntries;
try {
productEntries = await readdir(base, { withFileTypes: true });
} catch (err) {
warnings.push(`failed to read ${base}: ${err.message}`);
continue;
}
for (const pe of productEntries) {
if (!pe.isDirectory() && !(pe.isSymbolicLink() && options.followSymlinks)) continue;
if (pe.name.startsWith('.')) continue;
if (!JB_PRODUCT_DIR_RE.test(pe.name)) continue; // excludes Fleet, Toolbox
const productDir = join(base, pe.name);
const pluginsDir = join(productDir, 'plugins');
if (!await pathExists(pluginsDir)) continue;
let pluginEntries;
try {
pluginEntries = await readdir(pluginsDir, { withFileTypes: true });
} catch (err) {
warnings.push(`failed to read ${pluginsDir}: ${err.message}`);
continue;
}
for (const plug of pluginEntries) {
if (plug.name.startsWith('.')) continue;
if (plug.name === 'disabled_plugins.txt') continue;
if (plug.isSymbolicLink() && !options.followSymlinks) continue;
if (!plug.isDirectory() && !plug.isSymbolicLink()) continue;
const pluginDir = join(pluginsDir, plug.name);
// Safety: skip anything rooted under Toolbox cache
if (pluginDir.includes(join('Caches', 'JetBrains', 'Toolbox'))) continue;
const id = plug.name.toLowerCase();
extensions.push({
id,
publisher: '',
name: plug.name,
version: '',
location: pluginDir,
type: 'jetbrains',
source: null,
isBuiltin: false,
installedTimestamp: null,
targetPlatform: null,
publisherDisplayName: null,
signed: false,
rootDir: base,
productDir: pe.name,
});
}
}
}
return { extensions, warnings, rootsScanned };
}