mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-23 03:24:42 -07:00
feat(extensions): implement cryptographic integrity verification for extension updates (#21772)
This commit is contained in:
@@ -103,6 +103,10 @@ const mockLogExtensionInstallEvent = vi.hoisted(() => vi.fn());
|
||||
const mockLogExtensionUninstall = vi.hoisted(() => vi.fn());
|
||||
const mockLogExtensionUpdateEvent = vi.hoisted(() => vi.fn());
|
||||
const mockLogExtensionDisable = vi.hoisted(() => vi.fn());
|
||||
const mockIntegrityManager = vi.hoisted(() => ({
|
||||
verify: vi.fn().mockResolvedValue('verified'),
|
||||
store: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
@@ -118,6 +122,9 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
ExtensionInstallEvent: vi.fn(),
|
||||
ExtensionUninstallEvent: vi.fn(),
|
||||
ExtensionDisableEvent: vi.fn(),
|
||||
ExtensionIntegrityManager: vi
|
||||
.fn()
|
||||
.mockImplementation(() => mockIntegrityManager),
|
||||
KeychainTokenStorage: vi.fn().mockImplementation(() => ({
|
||||
getSecret: vi.fn(),
|
||||
setSecret: vi.fn(),
|
||||
@@ -214,6 +221,7 @@ describe('extension tests', () => {
|
||||
requestConsent: mockRequestConsent,
|
||||
requestSetting: mockPromptForSettings,
|
||||
settings,
|
||||
integrityManager: mockIntegrityManager,
|
||||
});
|
||||
resetTrustedFoldersForTesting();
|
||||
});
|
||||
@@ -241,10 +249,8 @@ describe('extension tests', () => {
|
||||
expect(extensions[0].name).toBe('test-extension');
|
||||
});
|
||||
|
||||
it('should throw an error if a context file path is outside the extension directory', async () => {
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
it('should log a warning and remove the extension if a context file path is outside the extension directory', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'traversal-extension',
|
||||
@@ -654,10 +660,8 @@ name = "yolo-checker"
|
||||
expect(serverConfig.env!['MISSING_VAR_BRACES']).toBe('${ALSO_UNDEFINED}');
|
||||
});
|
||||
|
||||
it('should skip extensions with invalid JSON and log a warning', async () => {
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
it('should remove an extension with invalid JSON config and log a warning', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
// Good extension
|
||||
createExtension({
|
||||
@@ -678,17 +682,15 @@ name = "yolo-checker"
|
||||
expect(extensions[0].name).toBe('good-ext');
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}`,
|
||||
`Warning: Removing broken extension bad-ext: Failed to load extension config from ${badConfigPath}`,
|
||||
),
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should skip extensions with missing name and log a warning', async () => {
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
it('should remove an extension with missing "name" in config and log a warning', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
// Good extension
|
||||
createExtension({
|
||||
@@ -709,7 +711,7 @@ name = "yolo-checker"
|
||||
expect(extensions[0].name).toBe('good-ext');
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}: Invalid configuration in ${badConfigPath}: missing "name"`,
|
||||
`Warning: Removing broken extension bad-ext-no-name: Failed to load extension config from ${badConfigPath}: Invalid configuration in ${badConfigPath}: missing "name"`,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -735,10 +737,8 @@ name = "yolo-checker"
|
||||
expect(extensions[0].mcpServers?.['test-server'].trust).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw an error for invalid extension names', async () => {
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
it('should log a warning for invalid extension names during loading', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'bad_name',
|
||||
@@ -754,7 +754,7 @@ name = "yolo-checker"
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not load github extensions if blockGitExtensions is set', async () => {
|
||||
it('should not load github extensions and log a warning if blockGitExtensions is set', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
@@ -774,6 +774,7 @@ name = "yolo-checker"
|
||||
requestConsent: mockRequestConsent,
|
||||
requestSetting: mockPromptForSettings,
|
||||
settings: blockGitExtensionsSetting,
|
||||
integrityManager: mockIntegrityManager,
|
||||
});
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
const extension = extensions.find((e) => e.name === 'my-ext');
|
||||
@@ -807,6 +808,7 @@ name = "yolo-checker"
|
||||
requestConsent: mockRequestConsent,
|
||||
requestSetting: mockPromptForSettings,
|
||||
settings: extensionAllowlistSetting,
|
||||
integrityManager: mockIntegrityManager,
|
||||
});
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
|
||||
@@ -814,7 +816,7 @@ name = "yolo-checker"
|
||||
expect(extensions[0].name).toBe('my-ext');
|
||||
});
|
||||
|
||||
it('should not load disallowed extensions if the allowlist is set.', async () => {
|
||||
it('should not load disallowed extensions and log a warning if the allowlist is set.', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
@@ -835,6 +837,7 @@ name = "yolo-checker"
|
||||
requestConsent: mockRequestConsent,
|
||||
requestSetting: mockPromptForSettings,
|
||||
settings: extensionAllowlistSetting,
|
||||
integrityManager: mockIntegrityManager,
|
||||
});
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
const extension = extensions.find((e) => e.name === 'my-ext');
|
||||
@@ -862,6 +865,7 @@ name = "yolo-checker"
|
||||
requestConsent: mockRequestConsent,
|
||||
requestSetting: mockPromptForSettings,
|
||||
settings: loadedSettings,
|
||||
integrityManager: mockIntegrityManager,
|
||||
});
|
||||
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
@@ -885,6 +889,7 @@ name = "yolo-checker"
|
||||
requestConsent: mockRequestConsent,
|
||||
requestSetting: mockPromptForSettings,
|
||||
settings: loadedSettings,
|
||||
integrityManager: mockIntegrityManager,
|
||||
});
|
||||
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
@@ -909,6 +914,7 @@ name = "yolo-checker"
|
||||
requestConsent: mockRequestConsent,
|
||||
requestSetting: mockPromptForSettings,
|
||||
settings: loadedSettings,
|
||||
integrityManager: mockIntegrityManager,
|
||||
});
|
||||
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
@@ -1047,6 +1053,7 @@ name = "yolo-checker"
|
||||
requestConsent: mockRequestConsent,
|
||||
requestSetting: mockPromptForSettings,
|
||||
settings,
|
||||
integrityManager: mockIntegrityManager,
|
||||
});
|
||||
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
@@ -1082,6 +1089,7 @@ name = "yolo-checker"
|
||||
requestConsent: mockRequestConsent,
|
||||
requestSetting: mockPromptForSettings,
|
||||
settings,
|
||||
integrityManager: mockIntegrityManager,
|
||||
});
|
||||
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
@@ -1306,6 +1314,7 @@ name = "yolo-checker"
|
||||
requestConsent: mockRequestConsent,
|
||||
requestSetting: mockPromptForSettings,
|
||||
settings: blockGitExtensionsSetting,
|
||||
integrityManager: mockIntegrityManager,
|
||||
});
|
||||
await extensionManager.loadExtensions();
|
||||
await expect(
|
||||
@@ -1330,6 +1339,7 @@ name = "yolo-checker"
|
||||
requestConsent: mockRequestConsent,
|
||||
requestSetting: mockPromptForSettings,
|
||||
settings: allowedExtensionsSetting,
|
||||
integrityManager: mockIntegrityManager,
|
||||
});
|
||||
await extensionManager.loadExtensions();
|
||||
await expect(
|
||||
@@ -1677,6 +1687,7 @@ ${INSTALL_WARNING_MESSAGE}`,
|
||||
requestConsent: mockRequestConsent,
|
||||
requestSetting: null,
|
||||
settings: loadSettings(tempWorkspaceDir).merged,
|
||||
integrityManager: mockIntegrityManager,
|
||||
});
|
||||
|
||||
await extensionManager.loadExtensions();
|
||||
|
||||
Reference in New Issue
Block a user