Skip to content

Commit

Permalink
Configure trivy to scan local images instead of downloading them again
Browse files Browse the repository at this point in the history
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.

Signed-off-by: Jan Dubois <[email protected]>
  • Loading branch information
jandubois committed Jan 24, 2025
1 parent a85c85a commit 175259a
Show file tree
Hide file tree
Showing 9 changed files with 53 additions and 36 deletions.
5 changes: 5 additions & 0 deletions pkg/rancher-desktop/assets/lima-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 21 additions & 26 deletions pkg/rancher-desktop/backend/images/imageProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<childResultType> {
return await this.runTrivyCommand([
'--quiet',
'image',
'--format',
'json',
taggedImageName,
]);
}
abstract scanImage(taggedImageName: string, namespace: string): Promise<childResultType>;

/**
* 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<childResultType> {
const subcommandName = args[0];
const child = this.executor?.spawn('trivy', ...args);
async runTrivyCommand(args: string[]): Promise<childResultType> {
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);
}

/**
Expand Down Expand Up @@ -255,20 +249,23 @@ export abstract class ImageProcessor extends EventEmitter {
*/
async processChildOutput(child: ChildProcess, subcommandName: string, sendNotifications: boolean, args?: string[]): Promise<childResultType> {
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;
});
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)
Expand All @@ -293,20 +290,18 @@ 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) {
Expand Down Expand Up @@ -339,7 +334,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.
*
Expand Down
7 changes: 5 additions & 2 deletions pkg/rancher-desktop/backend/images/mobyImageProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<imageProcessor.childResultType> {
Expand Down Expand Up @@ -69,11 +69,14 @@ export default class MobyImageProcessor extends imageProcessor.ImageProcessor {
false);
}

async scanImage(taggedImageName: string): Promise<imageProcessor.childResultType> {
async scanImage(taggedImageName: string, namespace: string): Promise<imageProcessor.childResultType> {
return await this.runTrivyCommand(
[
'trivy',
'--quiet',
'image',
'--image-src',
'docker',
'--format',
'json',
taggedImageName,
Expand Down
7 changes: 6 additions & 1 deletion pkg/rancher-desktop/backend/images/nerdctlImageProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,16 @@ export default class NerdctlImageProcessor extends imageProcessor.ImageProcessor
false);
}

async scanImage(taggedImageName: string): Promise<imageProcessor.childResultType> {
async scanImage(taggedImageName: string, namespace: string): Promise<imageProcessor.childResultType> {
return await this.runTrivyCommand(
[
'CONTAINERD_ADDRESS=/run/k3s/containerd/containerd.sock',
`CONTAINERD_NAMESPACE=${ namespace }`,
'trivy',
'--quiet',
'image',
'--image-src',
'containerd',
'--format',
'json',
taggedImageName,
Expand Down
2 changes: 1 addition & 1 deletion pkg/rancher-desktop/components/Images.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 === '<none>' ? 'latest' : `${ tag.trim() }`;
Expand Down
14 changes: 11 additions & 3 deletions pkg/rancher-desktop/main/imageEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion pkg/rancher-desktop/pages/images/scans/_image-name.vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export default {
data() {
return {
image: this.$route.params.image,
namespace: this.$route.params.namespace,
showImageOutput: true,
imageManagerOutput: '',
imageOutputCuller: null,
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion pkg/rancher-desktop/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}, {
Expand Down
2 changes: 1 addition & 1 deletion pkg/rancher-desktop/typings/electron-ipc.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 175259a

Please sign in to comment.