Skip to content

Commit 31c7e4e

Browse files
Merge pull request #580 from snyk/feat/pull-image-by-sha
feat: pull image by SHA instead of tag
2 parents 319e748 + 02de36d commit 31c7e4e

File tree

8 files changed

+53
-31
lines changed

8 files changed

+53
-31
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: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,25 @@ import { pullImages, removePulledImages, getImagesWithFileSystemPath, scanImages
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: { [key: string]: IScanImage } = workloadMetadata.reduce((accum, meta) => {
12+
// example: "docker.io/library/redis@sha256:33ca074e6019b451235735772a9c3e7216f014aae8eb0580d7e94834fe23efb3"
13+
const imageWithDigest = meta.imageId.substring(meta.imageId.lastIndexOf('/') + 1);
1414

15-
logger.info({workloadName, imageCount: uniqueImages.length}, 'pulling unique images');
16-
const imagesWithFileSystemPath = getImagesWithFileSystemPath(uniqueImages);
15+
accum[meta.imageName] = {
16+
imageName: meta.imageName,
17+
imageWithDigest,
18+
};
19+
20+
return accum;
21+
}, {});
22+
23+
logger.info({workloadName, imageCount: Object.values(uniqueImages).length}, 'pulling unique images');
24+
const imagesWithFileSystemPath = getImagesWithFileSystemPath(Object.values(uniqueImages));
1725
const pulledImages = await pullImages(imagesWithFileSystemPath);
1826
if (pulledImages.length === 0) {
1927
logger.info({workloadName}, 'no images were pulled, halting scanner process.');

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/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)