Skip to content

Commit defb054

Browse files
committed
fix: revert pulling image by SHA
This reverts commit 393fa09, reversing changes made to 87d17f2.
1 parent 393fa09 commit defb054

File tree

9 files changed

+124
-32
lines changed

9 files changed

+124
-32
lines changed

src/scanner/images/index.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,31 @@ import * as plugin from 'snyk-docker-plugin';
33

44
import logger = require('../../common/logger');
55
import { pull as skopeoCopy, getDestinationForImage } from './skopeo';
6-
import { IPullableImage } from './types';
6+
import { IPullableImage, IScanImage } from './types';
77
import { IStaticAnalysisOptions, StaticAnalysisImageType, IScanResult, IPluginOptions } from '../types';
88

99
export async function pullImages(images: IPullableImage[]): Promise<IPullableImage[]> {
1010
const pulledImages: IPullableImage[] = [];
1111

1212
for (const image of images) {
13-
const {imageName, fileSystemPath} = image;
13+
const {imageWithDigest, fileSystemPath} = image;
1414
if (!fileSystemPath) {
1515
continue;
1616
}
1717

1818
try {
19-
await skopeoCopy(imageName, fileSystemPath);
19+
await skopeoCopy(imageWithDigest, fileSystemPath);
2020
pulledImages.push(image);
2121
} catch (error) {
22-
logger.error({error, image: imageName}, 'failed to pull image');
22+
logger.error({error, image: imageWithDigest}, 'failed to pull image');
2323
}
2424
}
2525

2626
return pulledImages;
2727
}
2828

29-
export function getImagesWithFileSystemPath(images: string[]): IPullableImage[] {
30-
return images.map((image) => ({ imageName: image, fileSystemPath: getDestinationForImage(image) }));
29+
export function getImagesWithFileSystemPath(images: IScanImage[]): IPullableImage[] {
30+
return images.map((image) => ({ ...image, fileSystemPath: getDestinationForImage(image.imageName) }));
3131
}
3232

3333
export async function removePulledImages(images: IPullableImage[]): Promise<void> {
@@ -41,15 +41,15 @@ export async function removePulledImages(images: IPullableImage[]): Promise<void
4141
}
4242

4343
// Exported for testing
44-
export function getImageParts(imageWithTag: string) : {imageName: string, imageTag: string} {
44+
export function getImageParts(imageWithTag: string) : {imageName: string, imageTag: string, imageDigest: string} {
4545
// we're matching pattern: <registry:port_number>(optional)/<image_name>(mandatory):<image_tag>(optional)@<tag_identifier>(optional)
4646
// extracted from https://github.com/docker/distribution/blob/master/reference/regexp.go
4747
const regex = /^((?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(?::[0-9]+)?\/)?[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?(?:(?:\/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?)(?::([\w][\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][A-Fa-f0-9]{32,}))?$/ig;
4848
const groups = regex.exec(imageWithTag);
49-
49+
5050
if(!groups){
5151
logger.error({image: imageWithTag}, 'Image with tag is malformed, cannot extract valid parts');
52-
return {imageName: imageWithTag, imageTag: ''};
52+
return { imageName: imageWithTag, imageTag: '', imageDigest: '' };
5353
}
5454

5555
const IMAGE_NAME_GROUP = 1;
@@ -58,8 +58,8 @@ export function getImageParts(imageWithTag: string) : {imageName: string, imageT
5858

5959
return {
6060
imageName: groups[IMAGE_NAME_GROUP],
61-
// prefer tag over digest
62-
imageTag: groups[IMAGE_TAG_GROUP] || groups[IMAGE_DIGEST_GROUP] || '',
61+
imageTag: groups[IMAGE_TAG_GROUP] || '',
62+
imageDigest: groups[IMAGE_DIGEST_GROUP] || '',
6363
};
6464
}
6565

@@ -78,7 +78,7 @@ export async function scanImages(images: IPullableImage[]): Promise<IScanResult[
7878

7979
const dockerfile = undefined;
8080

81-
for (const {imageName, fileSystemPath} of images) {
81+
for (const { imageName, fileSystemPath, imageWithDigest } of images) {
8282
try {
8383
const staticAnalysisOptions = constructStaticAnalysisOptions(fileSystemPath);
8484
const options: IPluginOptions = {
@@ -92,16 +92,18 @@ export async function scanImages(images: IPullableImage[]): Promise<IScanResult[
9292
throw Error('Unexpected empty result from docker-plugin');
9393
}
9494

95-
const imageParts: {imageName: string, imageTag: string} = getImageParts(imageName);
95+
const imageParts = getImageParts(imageName);
9696

9797
result.imageMetadata = {
9898
image: imageParts.imageName,
9999
imageTag: imageParts.imageTag,
100+
imageDigest: getImageParts(imageWithDigest).imageDigest,
100101
};
101102

102103
scannedImages.push({
103104
image: imageParts.imageName,
104105
imageWithTag: imageName,
106+
imageWithDigest: imageWithDigest,
105107
pluginResult: result,
106108
});
107109
} catch (error) {

src/scanner/images/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
export interface IScanImage {
2+
imageName: string;
3+
imageWithDigest: string;
4+
}
5+
16
export interface IPullableImage {
27
imageName: string;
8+
imageWithDigest: string;
39
fileSystemPath: string;
410
}
511

src/scanner/index.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
11
import logger = require('../common/logger');
2-
import { pullImages, removePulledImages, getImagesWithFileSystemPath, scanImages } from './images';
2+
import { pullImages, removePulledImages, getImagesWithFileSystemPath, scanImages, getImageParts } from './images';
33
import { deleteWorkload, sendDepGraph } from '../transmitter';
44
import { constructDeleteWorkload, constructDepGraph } from '../transmitter/payload';
55
import { IWorkload, ILocalWorkloadLocator } from '../transmitter/types';
6-
import { IPullableImage } from './images/types';
6+
import { IPullableImage, IScanImage } from './images/types';
77

88
export async function processWorkload(workloadMetadata: IWorkload[]): Promise<void> {
99
// every workload metadata references the same workload name, grab it from the first one
1010
const workloadName = workloadMetadata[0].name;
11-
const allImages = workloadMetadata.map((meta) => meta.imageName);
12-
logger.info({workloadName, imageCount: allImages.length}, 'queried workloads');
13-
const uniqueImages = [...new Set<string>(allImages)];
11+
const uniqueImages = getUniqueImages(workloadMetadata);
1412

15-
logger.info({workloadName, imageCount: uniqueImages.length}, 'pulling unique images');
13+
logger.info({ workloadName, imageCount: uniqueImages.length }, 'pulling unique images');
1614
const imagesWithFileSystemPath = getImagesWithFileSystemPath(uniqueImages);
1715
const pulledImages = await pullImages(imagesWithFileSystemPath);
1816
if (pulledImages.length === 0) {
19-
logger.info({workloadName}, 'no images were pulled, halting scanner process.');
17+
logger.info({ workloadName }, 'no images were pulled, halting scanner process.');
2018
return;
2119
}
2220

@@ -35,6 +33,31 @@ export async function sendDeleteWorkloadRequest(workloadName: string, localWorkl
3533
await deleteWorkload(deletePayload);
3634
}
3735

36+
export function getUniqueImages(workloadMetadata: IWorkload[]): IScanImage[] {
37+
const uniqueImages: { [key: string]: IScanImage } = workloadMetadata.reduce((accum, meta) => {
38+
// example: For DCR "redis:latest"
39+
// example: For GCR "gcr.io/test-dummy/redis:latest"
40+
// example: For ECR "291964488713.dkr.ecr.us-east-2.amazonaws.com/snyk/redis:latest"
41+
// meta.imageName can be different depends on CR
42+
const { imageName } = getImageParts(meta.imageName);
43+
// meta.imageId can be different depends on CR
44+
// example: For DCR "docker.io/library/redis@sha256:8e9f8546050da8aae393a41d65ad37166b4f0d8131d627a520c0f0451742e9d6"
45+
// example: For GCR "sha256:8e9f8546050da8aae393a41d65ad37166b4f0d8131d627a520c0f0451742e9d6"
46+
// example: For ECR "sha256:8e9f8546050da8aae393a41d65ad37166b4f0d8131d627a520c0f0451742e9d6"
47+
const digest = meta.imageId.substring(meta.imageId.lastIndexOf('@') + 1);
48+
const imageWithDigest = `${imageName}@${digest}`;
49+
50+
accum[imageWithDigest] = {
51+
imageWithDigest,
52+
imageName: meta.imageName, // Image name with tag
53+
};
54+
55+
return accum;
56+
}, {});
57+
58+
return Object.values(uniqueImages);
59+
}
60+
3861
async function scanImagesAndSendResults(
3962
workloadName: string,
4063
pulledImages: IPullableImage[],

src/scanner/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export interface IScanResult {
22
image: string;
3+
imageWithDigest: string;
34
imageWithTag: string;
45
pluginResult: any;
56
}

src/transmitter/payload.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export function constructDepGraph(
2727
const imageLocator: IImageLocator = {
2828
userLocator: config.INTEGRATION_ID,
2929
imageId: scannedImage.image,
30+
imageWithDigest: scannedImage.imageWithDigest,
3031
cluster,
3132
namespace,
3233
type,

src/transmitter/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export interface IWorkloadMetadata {
2525

2626
export interface IImageLocator extends IWorkloadLocator {
2727
imageId: string;
28+
imageWithDigest: string;
2829
}
2930

3031
export interface IKubernetesMonitorMetadata {

test/unit/scanner/images.test.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import * as tap from 'tap';
22

3-
import { IPullableImage } from '../../../src/scanner/images/types';
3+
import {IPullableImage, IScanImage} from '../../../src/scanner/images/types';
44
import config = require('../../../src/common/config');
55
import * as scannerImages from '../../../src/scanner/images';
66

7-
87
tap.test('getImagesWithFileSystemPath()', async (t) => {
9-
const noImages: string[] = [];
8+
const noImages: IScanImage[] = [];
109
const noImagesResult = scannerImages.getImagesWithFileSystemPath(noImages);
1110
t.same(noImagesResult, [], 'correctly maps an empty array');
1211

13-
const image = ['nginx:latest'];
12+
const image: IScanImage[] = [{
13+
imageName: 'nginx:latest',
14+
imageWithDigest: 'nginx@sha256:4949aa7259aa6f827450207db5ad94cabaa9248277c6d736d5e1975d200c7e43',
15+
}];
1416
const imageResult = scannerImages.getImagesWithFileSystemPath(image);
1517
t.same(imageResult.length, 1, 'expected 1 item');
1618

@@ -28,7 +30,10 @@ tap.test('getImagesWithFileSystemPath()', async (t) => {
2830
t.ok(expectedPattern, 'the file system path starts with an expected pattern');
2931

3032
// Ensure that two consecutive calls do not return the same file system path
31-
const someImage = ['centos:latest'];
33+
const someImage = [{
34+
imageName: 'centos:latest',
35+
imageWithDigest: 'centos@sha256:fc4a234b91cc4b542bac8a6ad23b2ddcee60ae68fc4dbd4a52efb5f1b0baad71',
36+
}];
3237
const firstCallResult = scannerImages.getImagesWithFileSystemPath(someImage)[0];
3338
const secondCallResult = scannerImages.getImagesWithFileSystemPath(someImage)[0];
3439
t.ok(
@@ -44,8 +49,6 @@ tap.test('pullImages() skips on missing file system path', async (t) => {
4449
});
4550

4651
tap.test('constructStaticAnalysisOptions() tests', async (t) => {
47-
t.plan(1);
48-
4952
const somePath = '/var/tmp/file.tar';
5053
const options = scannerImages.constructStaticAnalysisOptions(somePath);
5154
const expectedResult = {
@@ -57,11 +60,10 @@ tap.test('constructStaticAnalysisOptions() tests', async (t) => {
5760
});
5861

5962
tap.test('extracted image tag tests', async (t) => {
60-
t.plan(6);
61-
6263
const imageWithSha = 'nginx@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2';
6364
const imageWithShaResult = scannerImages.getImageParts(imageWithSha);
64-
t.same(imageWithShaResult.imageTag, 'sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2', 'image sha is returned');
65+
t.same(imageWithShaResult.imageTag, '', 'image tag is empty');
66+
t.same(imageWithShaResult.imageDigest, 'sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2', 'image digest is returned');
6567

6668
const imageWithTag = 'nginx:latest';
6769
const imageWithTagResult = scannerImages.getImageParts(imageWithTag);
@@ -85,8 +87,6 @@ tap.test('extracted image tag tests', async (t) => {
8587
});
8688

8789
tap.test('extracted image name tests', async (t) => {
88-
t.plan(5);
89-
9090
t.same(scannerImages.getImageParts('nginx:latest').imageName, 'nginx', 'removed image:tag');
9191
t.same(scannerImages.getImageParts('node@sha256:215a9fbef4df2c1ceb7c79481d3cfd94ad8f1f0105bade39f3be907bf386c5e1').imageName, 'node', 'removed image@sha:hex');
9292
t.same(scannerImages.getImageParts('kind-registry:5000/python:rc-buster').imageName, 'kind-registry:5000/python', 'removed repository/image:tag');

test/unit/scanner/index.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import * as tap from 'tap';
2+
3+
import * as scanner from '../../../src/scanner';
4+
import { IWorkload } from '../../../src/transmitter/types';
5+
6+
tap.test('getUniqueImages()', async (t) => {
7+
const workload: Partial<IWorkload>[] = [
8+
// 1.DCR
9+
{
10+
imageName: 'redis:latest',
11+
imageId: 'docker.io/library/redis@sha256:8e9f8546050da8aae393a41d65ad37166b4f0d8131d627a520c0f0451742e9d6'
12+
},
13+
// 2.Duplicate to verify uniqueness
14+
{
15+
imageName: 'redis:latest',
16+
imageId: 'docker.io/library/redis@sha256:8e9f8546050da8aae393a41d65ad37166b4f0d8131d627a520c0f0451742e9d6'
17+
},
18+
// 3. Duplicate without tag
19+
{
20+
imageName: 'redis',
21+
imageId: 'docker.io/library/redis@sha256:8e9f8546050da8aae393a41d65ad37166b4f0d8131d627a520c0f0451742e9d6'
22+
},
23+
// 4. Duplicate with SHA instead of tag
24+
{
25+
imageName: 'redis@sha256:8e9f8546050da8aae393a41d65ad37166b4f0d8131d627a520c0f0451742e9d6',
26+
imageId: 'docker.io/library/redis@sha256:8e9f8546050da8aae393a41d65ad37166b4f0d8131d627a520c0f0451742e9d6'
27+
},
28+
// 5. GCR
29+
{
30+
imageName: 'gcr.io/test-dummy/redis:latest',
31+
imageId: 'sha256:8e9f8546050da8aae393a41d65ad37166b4f0d8131d627a520c0f0451742e9d6'
32+
},
33+
// 6. ECR
34+
{
35+
imageName: '291964488713.dkr.ecr.us-east-2.amazonaws.com/snyk/redis:latest',
36+
imageId: 'sha256:8e9f8546050da8aae393a41d65ad37166b4f0d8131d627a520c0f0451742e9d6'
37+
},
38+
];
39+
40+
const result = scanner.getUniqueImages(workload as any);
41+
42+
t.strictEqual(result.length, 3, 'removed duplicate image');
43+
result.map((metaData) => {
44+
t.ok(metaData.imageWithDigest.includes('redis'), 'has name in imageWithDigest');
45+
t.ok(metaData.imageWithDigest.includes('sha256:8e9f8546050da8aae393a41d65ad37166b4f0d8131d627a520c0f0451742e9d6'), 'has digest');
46+
47+
if (metaData.imageWithDigest.includes('gcr')) {
48+
t.ok(metaData.imageWithDigest.includes('/'), 'contains / in GCR imageWithDigest');
49+
}
50+
51+
if (metaData.imageWithDigest.includes('ecr')) {
52+
t.ok(metaData.imageWithDigest.includes('/'), 'contains / in ECR imageWithDigest');
53+
}
54+
});
55+
});

test/unit/transmitter-payload.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ tap.test('constructDepGraph breaks when workloadMetadata is missing items', asyn
1111
{
1212
image: 'myImage',
1313
imageWithTag: 'myImage:tag',
14+
imageWithDigest: 'myImage@sha256:idontcarewhatissha',
1415
pluginResult: 'whatever1',
1516
},
1617
{
1718
image: 'anotherImage',
1819
imageWithTag: 'anotherImage:1.2.3-alpha',
20+
imageWithDigest: 'myImage@sha256:somuchdifferentsha256',
1921
pluginResult: 'whatever3',
2022
},
2123
];
@@ -48,6 +50,7 @@ tap.test('constructDepGraph happy flow', async (t) => {
4850
{
4951
image: 'myImage',
5052
imageWithTag: 'myImage:tag',
53+
imageWithDigest: 'myImage@sha256:idontcarewhatissha',
5154
pluginResult: 'whatever1',
5255
},
5356
];

0 commit comments

Comments
 (0)