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.

Co-authored-by: Mark Yen <[email protected]>
Signed-off-by: Jan Dubois <[email protected]>
Signed-off-by: Mark Yen <[email protected]>
  • Loading branch information
jandubois and mook-as committed Jan 31, 2025
1 parent a85c85a commit 4ff11a7
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 64 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
98 changes: 64 additions & 34 deletions pkg/rancher-desktop/backend/images/imageProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<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.
* 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<childResultType> {
const subcommandName = args[0];
const child = this.executor?.spawn('trivy', ...args);
async runTrivyScan(taggedImageName: string, env?: Record<string, string>) {
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 },
},
);
}

/**
Expand Down Expand Up @@ -248,27 +276,31 @@ 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<childResultType> {
async processChildOutput(child: ChildProcess, options: ProcessChildOutputOptions): Promise<childResultType> {
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;
});
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 @@ -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);
}
});
Expand All @@ -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 });
Expand Down Expand Up @@ -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.
*
Expand Down
22 changes: 11 additions & 11 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 All @@ -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<imageProcessor.childResultType> {
Expand Down Expand Up @@ -69,15 +76,8 @@ export default class MobyImageProcessor extends imageProcessor.ImageProcessor {
false);
}

async scanImage(taggedImageName: string): Promise<imageProcessor.childResultType> {
return await this.runTrivyCommand(
[
'--quiet',
'image',
'--format',
'json',
taggedImageName,
]);
scanImage(taggedImageName: string, namespace: string): Promise<imageProcessor.childResultType> {
return this.runTrivyScan(taggedImageName);
}

relayNamespaces(): Promise<void> {
Expand Down
26 changes: 16 additions & 10 deletions pkg/rancher-desktop/backend/images/nerdctlImageProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<imageProcessor.childResultType> {
Expand Down Expand Up @@ -67,15 +74,14 @@ export default class NerdctlImageProcessor extends imageProcessor.ImageProcessor
false);
}

async scanImage(taggedImageName: string): Promise<imageProcessor.childResultType> {
return await this.runTrivyCommand(
[
'--quiet',
'image',
'--format',
'json',
taggedImageName,
]);
scanImage(taggedImageName: string, namespace: string): Promise<imageProcessor.childResultType> {
return this.runTrivyScan(
taggedImageName,
{
CONTAINERD_ADDRESS: '/run/k3s/containerd/containerd.sock',
CONTAINERD_NAMESPACE: namespace,
},
);
}

async getNamespaces(): Promise<Array<string>> {
Expand Down
18 changes: 16 additions & 2 deletions pkg/rancher-desktop/backend/wsl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);

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 4ff11a7

Please sign in to comment.