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..bf3de952709 100644 --- a/pkg/rancher-desktop/backend/images/imageProcessor.ts +++ b/pkg/rancher-desktop/backend/images/imageProcessor.ts @@ -41,7 +41,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 @@ -155,26 +155,20 @@ export abstract class ImageProcessor extends EventEmitter { /** * Wrapper around the trivy command to scan the specified image. * @param taggedImageName + * @param namespace */ - 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. + * Run trivy inside the VM with the given arguments; the 'trivy' command must be included + * in the args, so it can be called like `CONTAINERD_NAMESPACE=k8s.io trivy image ...`. */ - async runTrivyCommand(args: string[], sendNotifications = true): Promise { - const subcommandName = args[0]; - const child = this.executor?.spawn('trivy', ...args); + async runTrivyCommand(args: string[]): Promise { + const subcommandName = 'image'; + // must run as root to get access to the containerd socket + const child = this.executor?.spawn({ root: true }, ...args); - return await this.processChildOutput(child, subcommandName, sendNotifications, args); + return await this.processChildOutput(child, subcommandName, true, args); } /** @@ -255,12 +249,15 @@ export abstract class ImageProcessor extends EventEmitter { */ async processChildOutput(child: ChildProcess, subcommandName: string, sendNotifications: boolean, args?: string[]): Promise { const result = { stdout: '', stderr: '' }; + const commandName = args ? 'trivy' : this.processorName; + const command = args ? args.join(' ') : `${ commandName } ${ subcommandName }`; return await new Promise((resolve, reject) => { child.stdout?.on('data', (data: Buffer) => { const dataString = data.toString(); - if (sendNotifications) { + // don't dump megabytes of trivy JSON output into the browser + if (sendNotifications && commandName !== 'trivy') { this.emit('images-process-output', dataString, false); } result.stdout += dataString; @@ -268,7 +265,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) @@ -293,20 +290,20 @@ 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 }:${ formatBreak }${ result.stdout.replace(/(?!<\r)\n/g, '\r\n') }`); } if (code === 0) { if (sendNotifications) { @@ -339,7 +336,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..6b6eddcff6e 100644 --- a/pkg/rancher-desktop/backend/images/mobyImageProcessor.ts +++ b/pkg/rancher-desktop/backend/images/mobyImageProcessor.ts @@ -69,11 +69,14 @@ export default class MobyImageProcessor extends imageProcessor.ImageProcessor { false); } - async scanImage(taggedImageName: string): Promise { + async scanImage(taggedImageName: string, namespace: string): Promise { return await this.runTrivyCommand( [ + 'trivy', '--quiet', 'image', + '--image-src', + 'docker', '--format', 'json', taggedImageName, diff --git a/pkg/rancher-desktop/backend/images/nerdctlImageProcessor.ts b/pkg/rancher-desktop/backend/images/nerdctlImageProcessor.ts index ab751ffb259..62418ac2b8b 100644 --- a/pkg/rancher-desktop/backend/images/nerdctlImageProcessor.ts +++ b/pkg/rancher-desktop/backend/images/nerdctlImageProcessor.ts @@ -67,11 +67,16 @@ export default class NerdctlImageProcessor extends imageProcessor.ImageProcessor false); } - async scanImage(taggedImageName: string): Promise { + async scanImage(taggedImageName: string, namespace: string): Promise { return await this.runTrivyCommand( [ + 'CONTAINERD_ADDRESS=/run/k3s/containerd/containerd.sock', + `CONTAINERD_NAMESPACE=${ namespace }`, + 'trivy', '--quiet', 'image', + '--image-src', + 'containerd', '--format', 'json', taggedImageName, 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..152b6bf9d83 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;