feat(llm-security): implement JetBrains discovery + Android Studio base dir
This commit is contained in:
parent
5afb9b1f33
commit
03d61d8bca
2 changed files with 208 additions and 6 deletions
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue