Add a command to use trivy to scan images for vulnerabilities

* Install trivy in download-resources
* On Windows, we run trivy via wsl because there's no windows port
* But... to speed things up, we want wsl commands to hit the linux mount, not the windows mount
  - So we move the files from the windows mount to the linux once we know the
    rancher-desktop subsystem is available.
  - And to simplify this work, provide a resources function `wslify` to convert full windows
    paths into wsl paths in the windows mount.

Signed-off-by: Eric Promislow <epromislow@suse.com>
This commit is contained in:
Eric Promislow
2021-07-12 11:36:22 -07:00
parent 8d3619e627
commit 5c9d1139f0
15 changed files with 507 additions and 28 deletions

View File

@@ -0,0 +1,21 @@
[
{{ $firstVulSet := true }}{{ range . }}{{ if $firstVulSet}} {{ $firstVulSet = false}} {{ else }}, {{end}}
{
"Target": "{{.Target}}",
"Vulnerabilities": [
{{ $firstVul := true }}{{ range .Vulnerabilities }}{{ if $firstVul}} {{ $firstVul = false}} {{ else }}, {{end}}{
"Package": "{{.PkgName | js}}",
"Severity": "{{.Severity | js}}",
"Title": "{{.Title | js}}",
"VulnerabilityID": "{{.VulnerabilityID | js}}",
"InstalledVersion": "{{.InstalledVersion | js}}",
"FixedVersion": "{{.FixedVersion | js}}",
"PrimaryURL": "{{.PrimaryURL | js}}",
"Description": "{{.Description | js}}"
}
{{ end }}
]
}
{{ end }}
]

View File

@@ -39,6 +39,12 @@ if ($Step -eq "InstallLinuxUpdatePackage-03") {
Invoke-WebRequest -Uri https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi -OutFile $wslMsiFile
msiexec /norestart /i$wslMsiFile /passive
wsl --set-default-version 2
Write-Information 'about to install trivy after installing wsl'
$thisScript = $myInvocation.MyCommand.Definition
$thisDir = Split-Path -parent $thisScript
. (Join-Path $thisDir "install-trivy-in-wsl.ps1")
Write-Host -NoNewLine 'WSL is now installed - press any key to continue'
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown');
}

View File

@@ -20,16 +20,6 @@
@input="handleShowAllCheckbox"
/>
</template>
<template #row-actions="{ row }">
<!-- We want to use the defalut rowActions from the SortableTable;
- so just replace it with a dummy if we _don't_ want it on this row
-->
<i
v-if="!hasDropdownActions(row)"
disabled
class="btn btn-sm icon icon-actions actions role-multi-action role-link select-all-check"
/>
</template>
</SortableTable>
<Card :show-highlight-border="false" :show-actions="false">
@@ -139,7 +129,7 @@ export default {
data() {
return {
kimRunningCommand: null,
currentCommand: null,
headers: [
{
name: 'imageName',
@@ -198,6 +188,12 @@ export default {
enabled: this.isDeletable(image),
icon: 'icon icon-delete',
},
{
label: 'Scan...',
action: 'scanImage',
enabled: true,
icon: 'icon icon-info',
},
].filter(x => x.enabled);
}
// ActionMenu callbacks - SortableTable assumes that these methods live
@@ -208,15 +204,18 @@ export default {
if (!image.deleteImage) {
image.deleteImage = this.deleteImage.bind(this, image);
}
if (!image.scanImage) {
image.scanImage = this.scanImage.bind(this, image);
}
}
return this.filteredImages;
},
showImageManagerOutput() {
return !!this.kimRunningCommand || this.keepImageManagerOutputWindowOpen;
return !!this.currentCommand || this.keepImageManagerOutputWindowOpen;
},
imageManagerProcessIsFinished() {
return !this.kimRunningCommand;
return !this.currentCommand;
},
imageToBuildButtonDisabled() {
return this.showImageManagerOutput || !this.imageToBuild.includes(':');
@@ -256,6 +255,11 @@ export default {
value: row,
});
}
items.push({
label: `Scan...`,
action: this.scanImage,
value: row,
});
return items;
},
@@ -283,27 +287,34 @@ export default {
}
},
deleteImage(obj) {
this.kimRunningCommand = `delete ${ obj.imageName }:${ obj.tag }`;
this.currentCommand = `delete ${ obj.imageName }:${ obj.tag }`;
this.startRunningCommand('delete');
ipcRenderer.send('confirm-do-image-deletion', obj.imageName.trim(), obj.imageID.trim());
},
doPush(obj) {
this.kimRunningCommand = `push ${ obj.imageName }:${ obj.tag }`;
this.currentCommand = `push ${ obj.imageName }:${ obj.tag }`;
this.startRunningCommand('push');
ipcRenderer.send('do-image-push', obj.imageName.trim(), obj.imageID.trim(), obj.tag.trim());
},
doBuildAnImage() {
this.kimRunningCommand = `build ${ this.imageToBuild }`;
this.currentCommand = `build ${ this.imageToBuild }`;
this.fieldToClear = 'imageToBuild';
this.startRunningCommand('build');
ipcRenderer.send('do-image-build', this.imageToBuild.trim());
},
doPullAnImage() {
this.kimRunningCommand = `pull ${ this.imageToPull }`;
this.currentCommand = `pull ${ this.imageToPull }`;
this.fieldToClear = 'imageToPull';
this.startRunningCommand('pull');
ipcRenderer.send('do-image-pull', this.imageToPull.trim());
},
scanImage(obj) {
const taggedImageName = `${ obj.imageName.trim() }:${ obj.tag.trim() }`;
this.currentCommand = `scan image ${ taggedImageName }`;
this.startRunningCommand('trivy-image');
ipcRenderer.send('do-image-scan', taggedImageName);
},
handleProcessCancelled() {
this.closeOutputWindow(null);
},
@@ -316,10 +327,10 @@ export default {
// Don't know what would make this null, but it happens on windows sometimes
this.imageManagerOutput = this.imageOutputCuller.getProcessedData();
}
if (this.kimRunningCommand?.startsWith('delete') && this.imageManagerOutput === '') {
if (this.currentCommand?.startsWith('delete') && this.imageManagerOutput === '') {
this.closeOutputWindow(null);
}
this.kimRunningCommand = null;
this.currentCommand = null;
},
isDeletable(row) {
return row.imageName !== 'moby/buildkit' && !row.imageName.startsWith('rancher/');

View File

@@ -3,6 +3,7 @@ import { spawn } from 'child_process';
import { Console } from 'console';
import { EventEmitter } from 'events';
import net from 'net';
import os from 'os';
import path from 'path';
import timers from 'timers';
import tls from 'tls';
@@ -125,8 +126,25 @@ class Kim extends EventEmitter {
return this._isReady;
}
async runCommand(args: string[], sendNotifications = true): Promise<childResultType> {
const child = spawn(resources.executable('kim'), args);
async runKimCommand(args: string[], sendNotifications = true): Promise<childResultType> {
return await this.runCommand(resources.executable('kim'), args, sendNotifications);
}
async runTrivyCommand(args: string[], sendNotifications = true): Promise<childResultType> {
let command;
if (os.platform().startsWith('win')) {
command = 'wsl';
args = ['-d', 'rancher-desktop', 'trivy'].concat(args);
} else {
command = resources.executable('trivy');
}
return await this.runCommand(command, args, sendNotifications);
}
async runCommand(command: string, args: string[], sendNotifications: boolean): Promise<childResultType> {
const child = spawn(command, args);
const result = { stdout: '', stderr: '' };
return await new Promise((resolve, reject) => {
@@ -368,23 +386,30 @@ class Kim extends EventEmitter {
args.push(taggedImageName);
args.push(dirPart);
return await this.runCommand(args);
return await this.runKimCommand(args);
}
async deleteImage(imageID: string): Promise<childResultType> {
return await this.runCommand(['rmi', imageID]);
return await this.runKimCommand(['rmi', imageID]);
}
async pullImage(taggedImageName: string): Promise<childResultType> {
return await this.runCommand(['pull', taggedImageName, '--debug']);
return await this.runKimCommand(['pull', taggedImageName, '--debug']);
}
async pushImage(taggedImageName: string): Promise<childResultType> {
return await this.runCommand(['push', taggedImageName, '--debug']);
return await this.runKimCommand(['push', taggedImageName, '--debug']);
}
async getImages(): Promise<childResultType> {
return await this.runCommand(['images', '--all'], false);
return await this.runKimCommand(['images', '--all'], false);
}
async scanImage(taggedImageName: string): Promise<childResultType> {
const templatePath = os.platform().startsWith('win') ? '/var/lib/trivy.tpl' : resources.get('templates', 'trivy.tpl');
return await this.runTrivyCommand(['image', '--no-progress', '--format', 'template',
'--template', `@${ templatePath }`, taggedImageName]);
}
parse(data: string): imageType[] {

View File

@@ -244,6 +244,36 @@ export default class WSLBackend extends events.EventEmitter implements K8s.Kuber
}
}
protected async installTrivy() {
// download-resources.sh installed trivy into the ubuntu wsl windows mount area
// This function moves it and the trivy.tpl into /usr/local/bin/ and /var/lib/
// respectively so when trivy is invoked to run through wsl, it runs faster.
console.log('Installing trivy into /usr/local/bin/...');
// No bash on the rd wsl either
let trivyMvArgs = ['-d', 'rancher-desktop', 'sh', '-c',
'if [ ! -f /usr/local/bin/trivy ] ; then mv work/trivy /usr/local/bin/ && rmdir work ; fi'];
const { stdout } = await childProcess.spawnFile('wsl.exe', trivyMvArgs, {
stdio: ['ignore', 'pipe', await Logging.wsl.fdStream],
windowsHide: true
});
console.log(stdout);
console.log('Installing trivy.tpl into /var/lib/...');
// And put the trivy image template on the wsl partition
const trivyPath = resources.wslify(resources.get('templates', 'trivy.tpl'));
trivyMvArgs = ['-d', 'rancher-desktop', 'sh', '-c',
`if [ ! -f /var/lib/trivy.tpl ] ; then mv "${ trivyPath }" /var/lib/ ; fi`];
const moveOutput = await childProcess.spawnFile('wsl.exe', trivyMvArgs, {
stdio: ['ignore', 'pipe', await Logging.wsl.fdStream],
windowsHide: true
});
console.log(moveOutput.stdout);
}
/**
* execCommand runs the given command in the K3s WSL environment and returns
* the standard output.
@@ -343,6 +373,7 @@ export default class WSLBackend extends events.EventEmitter implements K8s.Kuber
await Promise.all([
this.ensureDistroRegistered(),
this.installTrivy(),
this.k3sHelper.ensureK3sImages(desiredVersion),
]);
// We have no good estimate for the rest of the steps, go indeterminate.

View File

@@ -147,6 +147,26 @@ export function setupKim() {
event.reply('kim-process-ended', code);
});
Electron.ipcMain.on('do-image-scan', async(event, imageName) => {
let taggedImageName = imageName;
let code;
if (!imageName.includes(':')) {
taggedImageName += ':latest';
}
try {
code = (await imageManager.scanImage(taggedImageName)).code;
await imageManager.refreshImages();
} catch (err) {
code = err.code;
Electron.dialog.showMessageBox({
message: `Error trying to scan ${ taggedImageName }:\n\n ${ err.stderr } `,
type: 'error'
});
}
event.reply('kim-process-ended', code);
});
Electron.ipcMain.on('do-image-push', async(event, imageName, imageID, tag) => {
const taggedImageName = `${ imageName }:${ tag }`;
let code;

View File

@@ -8,6 +8,7 @@ const adjustNameWithDir = {
helm: path.join('bin', 'helm'),
kim: path.join('bin', 'kim'),
kubectl: path.join('bin', 'kubectl'),
trivy: path.join('bin', 'trivy'),
};
function fixedSourceName(name) {
@@ -37,4 +38,17 @@ function _executable(name) {
}
const executable = memoize(_executable);
module.exports = { get, executable };
function _wslify(path) {
const m = /^(\w):(.+)$/.exec(path);
if (!m) {
return path;
}
return `/mnt/${ m[1].toLowerCase() }${ m[2].replace(/\\/g, '/') }`;
}
const wslify = memoize(_wslify);
module.exports = {
get, executable, wslify
};

View File

@@ -1,8 +1,14 @@
import KimNonBuildOutputCuller from '~/utils/processOutputInterpreters/kim-non-build-output';
import KimBuildOutputCuller from '~/utils/processOutputInterpreters/kim-build-output';
import TrivyScanImageOutputCuller from '~/utils/processOutputInterpreters/trivy-image-output';
const cullersByName = {
build: KimBuildOutputCuller,
'trivy-image': TrivyScanImageOutputCuller
};
export default function getImageOutputCuller(command) {
const klass = command === 'build' ? KimBuildOutputCuller : KimNonBuildOutputCuller;
const klass = cullersByName[command] || KimNonBuildOutputCuller;
return new klass();
}

View File

@@ -0,0 +1 @@
,ericp,chirico.local,09.07.2021 16:14,file:///Users/ericp/Library/Application%20Support/OpenOffice/4;

View File

@@ -0,0 +1,8 @@
2021-07-09T15:58:24.556-0700 INFO Detected OS: debian
2021-07-09T15:58:24.557-0700 INFO Detecting Debian vulnerabilities...
2021-07-09T15:58:24.559-0700 INFO Number of PL dependency files: 0
rancher/metrics-server:v0.3.6 (debian 9.9)
==========================================
Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)

View File

@@ -0,0 +1,8 @@
INFO Detected OS: debian
INFO Detecting Debian vulnerabilities...
INFO Number of PL dependency files: 0
rancher/metrics-server:v0.3.6 (debian 9.9)
==========================================
Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)

View File

@@ -0,0 +1,114 @@
2021-07-13T13:49:11.694-0700 INFO Detected OS: debian
2021-07-13T13:49:11.694-0700 INFO Detecting Debian vulnerabilities...
2021-07-13T13:49:11.705-0700 INFO Number of PL dependency files: 0
[
{
"Target": "postgres (debian 10.10)",
"Vulnerabilities": [
{
"Package": "apt",
"Severity": "LOW",
"Title": "",
"VulnerabilityID": "CVE-2011-3374",
"InstalledVersion": "1.8.2.3",
"FixedVersion": "",
"PrimaryURL": "https://avd.aquasec.com/nvd/cve-2011-3374",
"Description": "It was found that apt-key in apt, all versions, do not correctly validate gpg keys with the master keyring, leading to a potential man-in-the-middle attack."
}
,
{
"Package": "bash",
"Severity": "LOW",
"Title": "bash: when effective UID is not equal to its real UID the saved UID is not dropped",
"VulnerabilityID": "CVE-2019-18276",
"InstalledVersion": "5.0-4",
"FixedVersion": "5.0-5",
"PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-18276",
"Description": "An issue was discovered in disable_priv_mode in shell.c in GNU Bash through 5.0 patch 11. By default, if Bash is run with its effective UID not equal to its real UID, it will drop privileges by setting its effective UID to its real UID. However, it does so incorrectly. On Linux and other systems that support \"saved UID\" functionality, the saved UID is not dropped. An attacker with command execution in the shell can use \"enable -f\" for runtime loading of a new builtin, which can be a shared object that calls setuid() and therefore regains privileges. However, binaries running with an effective UID of 0 are unaffected."
}
,
{
"Package": "bash",
"Severity": "LOW",
"Title": "",
"VulnerabilityID": "TEMP-0841856-B18BAF",
"InstalledVersion": "5.0-4",
"FixedVersion": "",
"PrimaryURL": "https://security-tracker.debian.org/tracker/TEMP-0841856-B18BAF",
"Description": ""
}
,
{
"Package": "libc-bin",
"Severity": "CRITICAL",
"Title": "glibc: mq_notify does not handle separately allocated thread attributes",
"VulnerabilityID": "CVE-2021-33574",
"InstalledVersion": "2.28-10",
"FixedVersion": "",
"PrimaryURL": "https://avd.aquasec.com/nvd/cve-2021-33574",
"Description": "The mq_notify function in the GNU C Library (aka glibc) versions 2.32 and 2.33 has a use-after-free. It may use the notification thread attributes object (passed through its struct sigevent parameter) after it has been freed by the caller, leading to a denial of service (application crash) or possibly unspecified other impact."
}
,
{
"Package": "libc-bin",
"Severity": "HIGH",
"Title": "glibc: array overflow in backtrace functions for powerpc",
"VulnerabilityID": "CVE-2020-1751",
"InstalledVersion": "2.28-10",
"FixedVersion": "",
"PrimaryURL": "https://avd.aquasec.com/nvd/cve-2020-1751",
"Description": "An out-of-bounds write vulnerability was found in glibc before 2.31 when handling signal trampolines on PowerPC. Specifically, the backtrace function did not properly check the array bounds when storing the frame address, resulting in a denial of service or potential code execution. The highest threat from this vulnerability is to system availability."
}
,
{
"Package": "libc-bin",
"Severity": "HIGH",
"Title": "glibc: use-after-free in glob() function when expanding ~user",
"VulnerabilityID": "CVE-2020-1752",
"InstalledVersion": "2.28-10",
"FixedVersion": "",
"PrimaryURL": "https://avd.aquasec.com/nvd/cve-2020-1752",
"Description": "A use-after-free vulnerability introduced in glibc upstream version 2.14 was found in the way the tilde expansion was carried out. Directory paths containing an initial tilde followed by a valid username were affected by this issue. A local attacker could exploit this flaw by creating a specially crafted path that, when processed by the glob function, would potentially lead to arbitrary code execution. This was fixed in version 2.32."
}
,
{
"Package": "libc-bin",
"Severity": "MEDIUM",
"Title": "glibc: buffer over-read in iconv when processing invalid multi-byte input sequences in the EUC-KR encoding",
"VulnerabilityID": "CVE-2019-25013",
"InstalledVersion": "2.28-10",
"FixedVersion": "",
"PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-25013",
"Description": "The iconv feature in the GNU C Library (aka glibc or libc6) through 2.32, when processing invalid multi-byte input sequences in the EUC-KR encoding, may have a buffer over-read."
}
,
{
"Package": "libc-bin",
"Severity": "MEDIUM",
"Title": "glibc: stack corruption from crafted input in cosl, sinl, sincosl, and tanl functions",
"VulnerabilityID": "CVE-2020-10029",
"InstalledVersion": "2.28-10",
"FixedVersion": "",
"PrimaryURL": "https://avd.aquasec.com/nvd/cve-2020-10029",
"Description": "The GNU C Library (aka glibc or libc6) before 2.32 could overflow an on-stack buffer during range reduction if an input to an 80-bit long double function contains a non-canonical bit pattern, a seen when passing a 0x5d414141414141410000 value to sinl on x86 targets. This is related to sysdeps/ieee754/ldbl-96/e_rem_pio2l.c."
}
]
}
]

View File

@@ -0,0 +1,66 @@
INFO Detected OS: debian
INFO Detecting Debian vulnerabilities...
INFO Number of PL dependency files: 0
Target: postgres (debian 10.10)
Package: libc-bin
VulnerabilityID: CVE-2021-33574
Severity: CRITICAL
Title: glibc: mq_notify does not handle separately allocated thread attributes
InstalledVersion: 2.28-10
Description: The mq_notify function in the GNU C Library (aka glibc) versions 2.32 and 2.33 has a use-after-free. It may use the notification thread attributes object (passed through its struct sigevent parameter) after it has been freed by the caller, leading to a denial of service (application crash) or possibly unspecified other impact.
PrimaryURL: https://avd.aquasec.com/nvd/cve-2021-33574
Package: libc-bin
VulnerabilityID: CVE-2020-1751
Severity: HIGH
Title: glibc: array overflow in backtrace functions for powerpc
InstalledVersion: 2.28-10
Description: An out-of-bounds write vulnerability was found in glibc before 2.31 when handling signal trampolines on PowerPC. Specifically, the backtrace function did not properly check the array bounds when storing the frame address, resulting in a denial of service or potential code execution. The highest threat from this vulnerability is to system availability.
PrimaryURL: https://avd.aquasec.com/nvd/cve-2020-1751
Package: libc-bin
VulnerabilityID: CVE-2020-1752
Severity: HIGH
Title: glibc: use-after-free in glob() function when expanding ~user
InstalledVersion: 2.28-10
Description: A use-after-free vulnerability introduced in glibc upstream version 2.14 was found in the way the tilde expansion was carried out. Directory paths containing an initial tilde followed by a valid username were affected by this issue. A local attacker could exploit this flaw by creating a specially crafted path that, when processed by the glob function, would potentially lead to arbitrary code execution. This was fixed in version 2.32.
PrimaryURL: https://avd.aquasec.com/nvd/cve-2020-1752
Package: libc-bin
VulnerabilityID: CVE-2019-25013
Severity: MEDIUM
Title: glibc: buffer over-read in iconv when processing invalid multi-byte input sequences in the EUC-KR encoding
InstalledVersion: 2.28-10
Description: The iconv feature in the GNU C Library (aka glibc or libc6) through 2.32, when processing invalid multi-byte input sequences in the EUC-KR encoding, may have a buffer over-read.
PrimaryURL: https://avd.aquasec.com/nvd/cve-2019-25013
Package: libc-bin
VulnerabilityID: CVE-2020-10029
Severity: MEDIUM
Title: glibc: stack corruption from crafted input in cosl, sinl, sincosl, and tanl functions
InstalledVersion: 2.28-10
Description: The GNU C Library (aka glibc or libc6) before 2.32 could overflow an on-stack buffer during range reduction if an input to an 80-bit long double function contains a non-canonical bit pattern, a seen when passing a 0x5d414141414141410000 value to sinl on x86 targets. This is related to sysdeps/ieee754/ldbl-96/e_rem_pio2l.c.
PrimaryURL: https://avd.aquasec.com/nvd/cve-2020-10029
Package: apt
VulnerabilityID: CVE-2011-3374
Severity: LOW
InstalledVersion: 1.8.2.3
Description: It was found that apt-key in apt, all versions, do not correctly validate gpg keys with the master keyring, leading to a potential man-in-the-middle attack.
PrimaryURL: https://avd.aquasec.com/nvd/cve-2011-3374
Package: bash
VulnerabilityID: CVE-2019-18276
Severity: LOW
Title: bash: when effective UID is not equal to its real UID the saved UID is not dropped
InstalledVersion: 5.0-4
FixedVersion: 5.0-5
Description: An issue was discovered in disable_priv_mode in shell.c in GNU Bash through 5.0 patch 11. By default, if Bash is run with its effective UID not equal to its real UID, it will drop privileges by setting its effective UID to its real UID. However, it does so incorrectly. On Linux and other systems that support "saved UID" functionality, the saved UID is not dropped. An attacker with command execution in the shell can use "enable -f" for runtime loading of a new builtin, which can be a shared object that calls setuid() and therefore regains privileges. However, binaries running with an effective UID of 0 are unaffected.
PrimaryURL: https://avd.aquasec.com/nvd/cve-2019-18276
Package: bash
VulnerabilityID: TEMP-0841856-B18BAF
Severity: LOW
InstalledVersion: 5.0-4
PrimaryURL: https://security-tracker.debian.org/tracker/TEMP-0841856-B18BAF

View File

@@ -0,0 +1,42 @@
import fs from 'fs';
import path from 'path';
import resources from '@/resources';
import TrivyScanImageOutputCuller from '@/utils/processOutputInterpreters/trivy-image-output';
describe('trivy image output', () => {
it('echoes a zero-vul image back', () => {
const inputPath = path.join('./src/utils/processOutputInterpreters/__tests__/assets', 'trivy-image-metric-server-input.txt');
const outputPath = path.join('./src/utils/processOutputInterpreters/__tests__/assets', 'trivy-image-metric-server-output.txt');
const inputData = fs.readFileSync(inputPath).toString();
const expectedOutputData = fs.readFileSync(outputPath).toString().replace(/\r/g, '');
const culler = new TrivyScanImageOutputCuller();
culler.addData(inputData);
const processedData = culler.getProcessedData();
expect(expectedOutputData).toEqual(processedData);
});
it('converts lines to records and handles inherited cells', () => {
const inputPath = path.join('./src/utils/processOutputInterpreters/__tests__/assets', 'trivy-image-postgres-input.txt');
const outputPath = path.join('./src/utils/processOutputInterpreters/__tests__/assets', 'trivy-image-postgres-output.txt');
const inputData = fs.readFileSync(inputPath).toString();
const expectedOutputData = fs.readFileSync(outputPath).toString().replace(/\r/g, '');
const culler = new TrivyScanImageOutputCuller();
culler.addData(inputData);
const processedData = culler.getProcessedData();
expect(expectedOutputData).toEqual(processedData);
});
it('converts windows paths into wsl paths', () => {
const windowsPath = 'd:\\we\\really\\like\\backslash\\separators.tpl';
const expectedWindowsPath = '/mnt/d/we/really/like/backslash/separators.tpl';
const normalPath = '/hey,/no/problem/here';
expect(resources.wslify(windowsPath)).toBe(expectedWindowsPath);
expect(resources.wslify(normalPath)).toBe(normalPath);
});
});

View File

@@ -0,0 +1,106 @@
const LineSplitter = /\r?\n/;
const dateHeader = /^[-\d]+T[-:.\d]+Z?\s+\033\[\d+m([A-Z]+)\033\[\d+m\s+(.*)$/;
const borderPattern = /^\+(?:-+\+){6}\s*$/;
const internalBorderPattern = /^\+(?:(?:-+|\s+)\+){6}\s*$/;
const rowPattern = /^(?:\|[^|]+){6}\|\s*$/;
const CVEKeys = ['Package', 'VulnerabilityID', 'Severity', 'Title', 'InstalledVersion', 'FixedVersion', 'Description', 'PrimaryURL'];
const severityRatings: Record<string, number> = {
LOW: 1,
MEDIUM: 2,
HIGH: 3,
CRITICAL: 4
};
type finalVulType = Record<string, string>;
export default class TrivyScanImageOutputCuller {
prelimLines: Array<string>;
JSONLines: Array<string>;
inJSON = false;
constructor() {
this.prelimLines = [];
this.JSONLines = [];
}
fixLines(lines: Array<string>) {
// "key": "value with an escaped \' single quote isn't valid json"
return lines.map(line => line.replace(/\\'/g, "'"));
}
addData(data: string): void {
if (this.inJSON) {
this.JSONLines.push(data.replace(/\\'/g, "'"));
return;
}
const lines = data.split(LineSplitter);
const jsonStartIndex = lines.indexOf('[');
if (jsonStartIndex >= 0) {
this.prelimLines = this.prelimLines.concat(lines.slice(0, jsonStartIndex));
this.inJSON = true;
this.JSONLines = this.fixLines(lines.slice(jsonStartIndex));
} else {
this.prelimLines = this.prelimLines.concat(lines);
}
}
getProcessedData() {
const prelimLines = this.prelimLines.map(line => line.replace(dateHeader, '$1 $2'));
if (!this.inJSON) {
// No JSON, just so return the lines we have
return prelimLines.join('\n');
}
let core;
try {
core = JSON.parse(this.JSONLines.join(''));
} catch (e) {
console.log(`Error json parsing ${ this.JSONLines.join('') }`);
return prelimLines.join('\n');
}
const detailLines: Array<string> = [];
core.forEach((targetWithVuls: { [x: string]: any; }) => {
const target = targetWithVuls['Target'];
const sourceVulnerabilities = targetWithVuls['Vulnerabilities'];
if (!sourceVulnerabilities.length) {
return;
}
detailLines.push(`Target: ${ target }`, '');
const processedVulnerabilities: Array<finalVulType> = sourceVulnerabilities.map((v: any) => {
const record: finalVulType = {};
CVEKeys.forEach((key) => {
if (v[key]) {
record[key] = v[key];
}
});
return record;
});
processedVulnerabilities.sort();
processedVulnerabilities.sort((a, b) => {
return severityRatings[b['Severity']] - severityRatings[a['Severity']];
});
processedVulnerabilities.forEach((vul) => {
CVEKeys.forEach((key) => {
if (key in vul) {
detailLines.push(`${ key }: ${ vul[key] }`);
}
});
detailLines.push('');
});
});
return prelimLines.concat(detailLines).join('\n');
}
}