From 4ff11a7eca5002b954ed3e36e773e80e5a9a30a7 Mon Sep 17 00:00:00 2001 From: Jan Dubois Date: Fri, 31 Jan 2025 14:53:59 -0800 Subject: [PATCH] Configure trivy to scan local images instead of downloading them again Run as root so trivy has access to the containerd socket. Don't output the trivy JSON to the browser; this is extremely slow when the output is several megabytes. Also don't output the trivy JSON to images.log, as it makes the file unwieldy. Co-authored-by: Mark Yen Signed-off-by: Jan Dubois Signed-off-by: Mark Yen --- pkg/rancher-desktop/assets/lima-config.yaml | 5 + .../backend/images/imageProcessor.ts | 98 ++++++++++++------- .../backend/images/mobyImageProcessor.ts | 22 ++--- .../backend/images/nerdctlImageProcessor.ts | 26 +++-- pkg/rancher-desktop/backend/wsl.ts | 18 +++- pkg/rancher-desktop/components/Images.vue | 2 +- pkg/rancher-desktop/main/imageEvents.ts | 14 ++- .../pages/images/scans/_image-name.vue | 3 +- pkg/rancher-desktop/router.js | 2 +- pkg/rancher-desktop/typings/electron-ipc.d.ts | 2 +- 10 files changed, 128 insertions(+), 64 deletions(-) diff --git a/pkg/rancher-desktop/assets/lima-config.yaml b/pkg/rancher-desktop/assets/lima-config.yaml index 7c4e904abc0..0620ce826d1 100644 --- a/pkg/rancher-desktop/assets/lima-config.yaml +++ b/pkg/rancher-desktop/assets/lima-config.yaml @@ -176,6 +176,11 @@ provision: mount bpffs -t bpf /sys/fs/bpf mount --make-shared /sys/fs/bpf mount --make-shared /sys/fs/cgroup +- # we run trivy as root now; remove any cached databases installed into the user directory by previous version + # trivy.db is 600M and trivy-java.db is 1.1G + mode: user + script: | + rm -rf "${HOME}/.cache/trivy" portForwards: - guestPortRange: [1, 65535] guestIPMustBeZero: true diff --git a/pkg/rancher-desktop/backend/images/imageProcessor.ts b/pkg/rancher-desktop/backend/images/imageProcessor.ts index 8af82adceb8..a6c0126e0ee 100644 --- a/pkg/rancher-desktop/backend/images/imageProcessor.ts +++ b/pkg/rancher-desktop/backend/images/imageProcessor.ts @@ -33,6 +33,25 @@ export interface imageType { digest: string; } +/** + * Options for `processChildOutput`. + */ +type ProcessChildOutputOptions = { + /** The name of the executable; defaults to `processorName`. */ + commandName?: string; + /** The sub-command being executed; typically the first argument. */ + subcommandName: string; + /** What notifications to send. */ + notifications?: { + /** Send stdout as it comes in via `image-process-output`. */ + stdout?: boolean; + /** Send stderr as it comes in via `image-process-output`. */ + stderr?: boolean; + /** Send stdout after the command succeeds to the window via `ok:images-process-output`. */ + ok?: boolean; + } +}; + /** * ImageProcessors take requests, from the UI or caused by state transitions * (such as a K8s engine hitting the STARTED state), and invokes the appropriate @@ -41,7 +60,7 @@ export interface imageType { * Each concrete ImageProcessor is a singleton, with a 1:1 correspondence between * the current container engine the user has selected, and its ImageProcessor. * - * Currently some events are handled directly by the concrete ImageProcessor subclasses, + * Currently, some events are handled directly by the concrete ImageProcessor subclasses, * and some are handled by the ImageEventHandler singleton, which calls methods on * the current ImageProcessor. Because these events are sent to all imageProcessors, but * only one should actually act on them, we use the concept of the `active` processor @@ -154,27 +173,36 @@ export abstract class ImageProcessor extends EventEmitter { /** * Wrapper around the trivy command to scan the specified image. - * @param taggedImageName + * @param taggedImageName The name of the image, e.g. `registry.opensuse.org/opensuse/leap:15.6`. + * @param namespace The namespace to scan. */ - async scanImage(taggedImageName: string): Promise { - return await this.runTrivyCommand([ - '--quiet', - 'image', - '--format', - 'json', - taggedImageName, - ]); - } + abstract scanImage(taggedImageName: string, namespace: string): Promise; /** - * Run trivy with the given arguments; the first argument is generally a - * subcommand to execute. + * Scan an image using trivy. + * @param taggedImageName The image to scan, e.g. `registry.opensuse.org/opensuse/leap:15.6`. + * @param env Extra environment variables to set, e.g. `CONTAINERD_NAMESPACE`. */ - async runTrivyCommand(args: string[], sendNotifications = true): Promise { - const subcommandName = args[0]; - const child = this.executor?.spawn('trivy', ...args); + async runTrivyScan(taggedImageName: string, env?: Record) { + const imageSrc = { + docker: 'docker', + nerdctl: 'containerd', + }[this.processorName] ?? this.processorName; + const args = ['trivy', '--quiet', 'image', '--image-src', imageSrc, '--format', 'json', taggedImageName]; + + if (env) { + args.unshift('/usr/bin/env', ...Object.entries(env).map(([k, v]) => `${ k }=${ v }`)); + } - return await this.processChildOutput(child, subcommandName, sendNotifications, args); + return await this.processChildOutput( + this.executor.spawn({ root: true }, ...args), + { + commandName: 'trivy', + subcommandName: 'image', + // Do not set stdout to avoid dumping JSON that nobody ever reads. + notifications: { stderr: true, ok: true }, + }, + ); } /** @@ -248,19 +276,23 @@ export abstract class ImageProcessor extends EventEmitter { * Takes the `childProcess` returned by a command like `child_process.spawn` and processes the * output streams and exit code and signal. * - * @param child - * @param subcommandName - used for error messages only - * @param sendNotifications - * @param args - used to support running `trivy` with this method. + * @param child The child process to monitor. + * @param options Additional options. */ - async processChildOutput(child: ChildProcess, subcommandName: string, sendNotifications: boolean, args?: string[]): Promise { + async processChildOutput(child: ChildProcess, options: ProcessChildOutputOptions): Promise { + const { subcommandName } = options; const result = { stdout: '', stderr: '' }; + const commandName = options.commandName ?? this.processorName; + const command = `${ commandName } ${ subcommandName }`; + const sendNotifications = options.notifications ?? { + stdout: true, stderr: true, ok: true, + }; return await new Promise((resolve, reject) => { child.stdout?.on('data', (data: Buffer) => { const dataString = data.toString(); - if (sendNotifications) { + if (sendNotifications.stdout) { this.emit('images-process-output', dataString, false); } result.stdout += dataString; @@ -268,7 +300,7 @@ export abstract class ImageProcessor extends EventEmitter { child.stderr?.on('data', (data: Buffer) => { let dataString = data.toString(); - if (this.processorName === 'nerdctl' && subcommandName === 'images') { + if (commandName === 'nerdctl' && subcommandName === 'images') { /** * `nerdctl images` issues some dubious error messages * (see https://github.com/containerd/nerdctl/issues/353 , logged 2021-09-10) @@ -282,7 +314,7 @@ export abstract class ImageProcessor extends EventEmitter { } } result.stderr += dataString; - if (sendNotifications) { + if (sendNotifications.stderr) { this.emit('images-process-output', dataString, true); } }); @@ -293,23 +325,21 @@ export abstract class ImageProcessor extends EventEmitter { if (this.lastErrorMessage !== timeLessMessage) { this.lastErrorMessage = timeLessMessage; this.sameErrorMessageCount = 1; - const argsString = args ? ` ${ args.join(' ') }` : ''; - console.log(`> ${ this.processorName } ${ subcommandName }${ argsString }:\r\n${ result.stderr.replace(/(?!<\r)\n/g, '\r\n') }`); + console.log(`> ${ command }:\r\n${ result.stderr.replace(/(?!<\r)\n/g, '\r\n') }`); } else { const m = /(Error: .*)/.exec(this.lastErrorMessage); this.sameErrorMessageCount += 1; - console.log(`${ this.processorName } ${ subcommandName }: ${ m ? m[1] : 'same error message' } #${ this.sameErrorMessageCount }\r`); + console.log(`${ command }: ${ m ? m[1] : 'same error message' } #${ this.sameErrorMessageCount }\r`); } + } else if (commandName === 'trivy') { + console.log(`> ${ command }: returned ${ result.stdout.length } bytes on stdout`); } else { - const formatBreak = result.stdout ? '\n' : ''; - const argsString = args ? ` ${ args.join(' ') }` : ''; - - console.log(`> ${ this.processorName } ${ subcommandName }${ argsString }:${ formatBreak }${ result.stdout.replace(/(?!<\r)\n/g, '\r\n') }`); + console.log(`> ${ command }:\n${ result.stdout.replace(/(?!<\r)\n/g, '\r\n') }`); } if (code === 0) { - if (sendNotifications) { + if (sendNotifications.ok) { window.send('ok:images-process-output', result.stdout); } resolve({ ...result, code }); @@ -339,7 +369,7 @@ export abstract class ImageProcessor extends EventEmitter { * Called normally when the UI requests the current list of namespaces * for the current imageProcessor. * - * Containerd starts with two namespaces: "k8s.io" and "default". + * containerd starts with two namespaces: "k8s.io" and "default". * There's no way to add other namespaces in the UI, * but they can easily be added from the command-line. * diff --git a/pkg/rancher-desktop/backend/images/mobyImageProcessor.ts b/pkg/rancher-desktop/backend/images/mobyImageProcessor.ts index 08409b887d9..7e6a45e7d2b 100644 --- a/pkg/rancher-desktop/backend/images/mobyImageProcessor.ts +++ b/pkg/rancher-desktop/backend/images/mobyImageProcessor.ts @@ -25,7 +25,7 @@ export default class MobyImageProcessor extends imageProcessor.ImageProcessor { } protected get processorName() { - return 'moby'; + return 'docker'; } protected async runImagesCommand(args: string[], sendNotifications = true): Promise { @@ -35,7 +35,14 @@ export default class MobyImageProcessor extends imageProcessor.ImageProcessor { args.unshift('--context', 'rancher-desktop'); } - return await this.processChildOutput(spawn(executable('docker'), args), subcommandName, sendNotifications); + return await this.processChildOutput( + spawn(executable('docker'), args), + { + subcommandName, + notifications: { + stdout: sendNotifications, stderr: sendNotifications, ok: sendNotifications, + }, + }); } async buildImage(dirPart: string, filePart: string, taggedImageName: string): Promise { @@ -69,15 +76,8 @@ export default class MobyImageProcessor extends imageProcessor.ImageProcessor { false); } - async scanImage(taggedImageName: string): Promise { - return await this.runTrivyCommand( - [ - '--quiet', - 'image', - '--format', - 'json', - taggedImageName, - ]); + scanImage(taggedImageName: string, namespace: string): Promise { + return this.runTrivyScan(taggedImageName); } relayNamespaces(): Promise { diff --git a/pkg/rancher-desktop/backend/images/nerdctlImageProcessor.ts b/pkg/rancher-desktop/backend/images/nerdctlImageProcessor.ts index ab751ffb259..6ab37dc07e6 100644 --- a/pkg/rancher-desktop/backend/images/nerdctlImageProcessor.ts +++ b/pkg/rancher-desktop/backend/images/nerdctlImageProcessor.ts @@ -32,7 +32,14 @@ export default class NerdctlImageProcessor extends imageProcessor.ImageProcessor const subcommandName = args[0]; const namespacedArgs = ['--namespace', this.currentNamespace].concat(args); - return await this.processChildOutput(spawn(executable('nerdctl'), namespacedArgs), subcommandName, sendNotifications); + return await this.processChildOutput( + spawn(executable('nerdctl'), namespacedArgs), + { + subcommandName, + notifications: { + stdout: sendNotifications, stderr: sendNotifications, ok: sendNotifications, + }, + }); } async buildImage(dirPart: string, filePart: string, taggedImageName: string): Promise { @@ -67,15 +74,14 @@ export default class NerdctlImageProcessor extends imageProcessor.ImageProcessor false); } - async scanImage(taggedImageName: string): Promise { - return await this.runTrivyCommand( - [ - '--quiet', - 'image', - '--format', - 'json', - taggedImageName, - ]); + scanImage(taggedImageName: string, namespace: string): Promise { + return this.runTrivyScan( + taggedImageName, + { + CONTAINERD_ADDRESS: '/run/k3s/containerd/containerd.sock', + CONTAINERD_NAMESPACE: namespace, + }, + ); } async getNamespaces(): Promise> { diff --git a/pkg/rancher-desktop/backend/wsl.ts b/pkg/rancher-desktop/backend/wsl.ts index 9d2bce4cd3e..bac42370836 100644 --- a/pkg/rancher-desktop/backend/wsl.ts +++ b/pkg/rancher-desktop/backend/wsl.ts @@ -13,7 +13,16 @@ import semver from 'semver'; import tar from 'tar-stream'; import { - BackendError, BackendEvents, BackendProgress, BackendSettings, execOptions, FailureDetails, RestartReasons, State, VMBackend, VMExecutor, + BackendError, + BackendEvents, + BackendProgress, + BackendSettings, + execOptions, + FailureDetails, + RestartReasons, + State, + VMBackend, + VMExecutor, } from './backend'; import BackendHelper from './backendHelper'; import { ContainerEngineClient, MobyClient, NerdctlClient } from './containerClient'; @@ -904,7 +913,12 @@ export default class WSLBackend extends events.EventEmitter implements VMBackend if (typeof optionsOrCommand === 'string') { args.push(optionsOrCommand); } else { - throw new TypeError('Not supported yet'); + const options: execOptions = optionsOrCommand; + + // runTrivyScan() calls spawn({root: true}, …), which we ignore because we are already running as root + if (options.expectFailure || options.logStream || options.env) { + throw new TypeError('Not supported yet'); + } } args.push(...command); diff --git a/pkg/rancher-desktop/components/Images.vue b/pkg/rancher-desktop/components/Images.vue index 942b7e3f913..2c133b30430 100644 --- a/pkg/rancher-desktop/components/Images.vue +++ b/pkg/rancher-desktop/components/Images.vue @@ -361,7 +361,7 @@ export default { scanImage(obj) { const taggedImageName = `${ obj.imageName.trim() }:${ this.imageTag(obj.tag) }`; - this.$router.push({ name: 'images-scans-image-name', params: { image: taggedImageName } }); + this.$router.push({ name: 'images-scans-image-name', params: { image: taggedImageName, namespace: this.selectedNamespace } }); }, imageTag(tag) { return tag === '' ? 'latest' : `${ tag.trim() }`; diff --git a/pkg/rancher-desktop/main/imageEvents.ts b/pkg/rancher-desktop/main/imageEvents.ts index 60218a61293..dd94c6e1f45 100644 --- a/pkg/rancher-desktop/main/imageEvents.ts +++ b/pkg/rancher-desktop/main/imageEvents.ts @@ -143,15 +143,23 @@ export class ImageEventHandler { event.reply('images-process-ended', code); }); - ipcMainProxy.on('do-image-scan', async(event, imageName) => { + ipcMainProxy.on('do-image-scan', async(event, imageName, namespace) => { let taggedImageName = imageName; let code; - if (!imageName.includes(':')) { + // The containerd scanner only supports image names that include the registry name + if (!taggedImageName.includes('/')) { + taggedImageName = `library/${ imageName }`; + } + if (!taggedImageName.split('/')[0].includes('.')) { + taggedImageName = `docker.io/${ taggedImageName }`; + } + if (!taggedImageName.includes(':')) { taggedImageName += ':latest'; } + try { - code = (await this.imageProcessor.scanImage(taggedImageName)).code; + code = (await this.imageProcessor.scanImage(taggedImageName, namespace)).code; await this.imageProcessor.refreshImages(); } catch (err) { console.error(`Failed to scan image ${ imageName }: `, err); diff --git a/pkg/rancher-desktop/pages/images/scans/_image-name.vue b/pkg/rancher-desktop/pages/images/scans/_image-name.vue index ef144f9b717..5c7d8b8d86c 100644 --- a/pkg/rancher-desktop/pages/images/scans/_image-name.vue +++ b/pkg/rancher-desktop/pages/images/scans/_image-name.vue @@ -58,6 +58,7 @@ export default { data() { return { image: this.$route.params.image, + namespace: this.$route.params.namespace, showImageOutput: true, imageManagerOutput: '', imageOutputCuller: null, @@ -128,7 +129,7 @@ export default { methods: { scanImage() { this.startRunningCommand('trivy-image'); - ipcRenderer.send('do-image-scan', this.image); + ipcRenderer.send('do-image-scan', this.image, this.namespace); }, startRunningCommand(command) { this.imageOutputCuller = getImageOutputCuller(command); diff --git a/pkg/rancher-desktop/router.js b/pkg/rancher-desktop/router.js index dc948fef875..34bcf4d67f2 100644 --- a/pkg/rancher-desktop/router.js +++ b/pkg/rancher-desktop/router.js @@ -107,7 +107,7 @@ export const routerOptions = { component: _20fa1c70, name: 'images-add', }, { - path: '/images/scans/:image-name?', + path: '/images/scans/:image-name?/:namespace?', component: _1165c4f2, name: 'images-scans-image-name', }, { diff --git a/pkg/rancher-desktop/typings/electron-ipc.d.ts b/pkg/rancher-desktop/typings/electron-ipc.d.ts index 766b5e3f2b2..18ad0fa65b0 100644 --- a/pkg/rancher-desktop/typings/electron-ipc.d.ts +++ b/pkg/rancher-desktop/typings/electron-ipc.d.ts @@ -43,7 +43,7 @@ export interface IpcMainEvents { 'confirm-do-image-deletion': (imageName: string, imageID: string) => void; 'do-image-build': (taggedImageName: string) => void; 'do-image-pull': (imageName: string) => void; - 'do-image-scan': (imageName: string) => void; + 'do-image-scan': (imageName: string, namespace: string) => void; 'do-image-push': (imageName: string, imageID: string, tag: string) => void; 'do-image-deletion': (imageName: string, imageID: string) => void; 'do-image-deletion-batch': (images: string[]) => void;