Skip to content

Commit 2143394

Browse files
authored
Merge pull request #993 from snyk/fix/pull-oci-archive
fix: pull OCI archives instead of Docker archives
2 parents e482c1f + c94006b commit 2143394

File tree

9 files changed

+81
-83
lines changed

9 files changed

+81
-83
lines changed

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"needle": "^3.0.0",
4848
"sleep-promise": "^9.1.0",
4949
"snyk-config": "5.0.0",
50-
"snyk-docker-plugin": "^4.33.2",
50+
"snyk-docker-plugin": "^4.34.2",
5151
"source-map-support": "^0.5.21",
5252
"tunnel": "0.0.6",
5353
"typescript": "^4.5.2",

src/scanner/images/index.ts

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
1-
import { unlink } from 'fs';
1+
import { unlink, stat } from 'fs';
2+
import { promisify } from 'util';
23
import { PluginResponse, scan } from 'snyk-docker-plugin';
34
import { DepGraph, legacy } from '@snyk/dep-graph';
45

56
import { logger } from '../../common/logger';
67
import { pull as skopeoCopy, getDestinationForImage } from './skopeo';
7-
import { IPullableImage, IScanImage, SkopeoRepositoryType } from './types';
8+
import { IPullableImage, IScanImage } from './types';
89
import { IScanResult } from '../types';
910
import {
1011
buildDockerPropertiesOnDepTree,
1112
DependencyTree,
1213
extractFactsFromDockerPluginResponse,
1314
LegacyPluginResponse,
1415
} from './docker-plugin-shim';
16+
import type { Telemetry } from '../../transmitter/types';
17+
18+
const statAsync = promisify(stat);
1519

1620
/*
1721
pulled images by skopeo archive repo type:
@@ -22,23 +26,11 @@ async function pullImageBySkopeoRepo(
2226
): Promise<IPullableImage> {
2327
// Scan image by digest if exists, other way fallback tag
2428
const scanId = imageToPull.imageWithDigest ?? imageToPull.imageName;
25-
imageToPull.skopeoRepoType = SkopeoRepositoryType.DockerArchive;
26-
try {
27-
// copy docker archive image
28-
await skopeoCopy(
29-
scanId,
30-
imageToPull.fileSystemPath,
31-
imageToPull.skopeoRepoType,
32-
);
33-
} catch (dockerError) {
34-
imageToPull.skopeoRepoType = SkopeoRepositoryType.OciArchive;
35-
// copy oci archive image
36-
await skopeoCopy(
37-
scanId,
38-
imageToPull.fileSystemPath,
39-
imageToPull.skopeoRepoType,
40-
);
41-
}
29+
await skopeoCopy(
30+
scanId,
31+
imageToPull.fileSystemPath,
32+
imageToPull.skopeoRepoType,
33+
);
4234
return imageToPull;
4335
}
4436

@@ -117,25 +109,17 @@ export function getImageParts(imageWithTag: string): {
117109

118110
export async function scanImages(
119111
images: IPullableImage[],
112+
telemetry: Partial<Telemetry>,
120113
): Promise<IScanResult[]> {
121114
const scannedImages: IScanResult[] = [];
122115

123-
for (const {
124-
imageName,
125-
fileSystemPath,
126-
imageWithDigest,
127-
skopeoRepoType,
128-
} of images) {
116+
for (const { imageName, fileSystemPath, imageWithDigest } of images) {
129117
try {
130118
const shouldIncludeAppVulns = true;
131-
const archiveType =
132-
skopeoRepoType == SkopeoRepositoryType.DockerArchive
133-
? 'docker-archive'
134-
: 'oci-archive';
135-
const dockerArchivePath = `${archiveType}:${fileSystemPath}`;
119+
const archivePath = `docker-archive:${fileSystemPath}`;
136120

137121
const pluginResponse = await scan({
138-
path: dockerArchivePath,
122+
path: archivePath,
139123
imageNameAndTag: imageName,
140124
'app-vulns': shouldIncludeAppVulns,
141125
});
@@ -148,6 +132,19 @@ export async function scanImages(
148132
throw Error('Unexpected empty result from docker-plugin');
149133
}
150134

135+
try {
136+
const fileStats = await statAsync(fileSystemPath);
137+
if (!telemetry.imageSizeBytes) {
138+
telemetry.imageSizeBytes = 0;
139+
}
140+
telemetry.imageSizeBytes += fileStats.size;
141+
} catch (err) {
142+
logger.warn(
143+
{ error: err, imageName, imageWithDigest, fileSystemPath },
144+
'could not determine archive size',
145+
);
146+
}
147+
151148
const depTree = await getDependencyTreeFromPluginResponse(
152149
pluginResponse,
153150
imageName,

src/scanner/images/skopeo.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ function prefixRespository(target: string, type: SkopeoRepositoryType): string {
2626
case SkopeoRepositoryType.ImageRegistry:
2727
return `${type}://${target}`;
2828
case SkopeoRepositoryType.DockerArchive:
29-
case SkopeoRepositoryType.OciArchive:
3029
return `${type}:${target}`;
3130
default:
3231
throw new Error(`Unhandled Skopeo repository type ${type}`);

src/scanner/images/types.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,5 @@ export interface IPullableImage {
1616
*/
1717
export enum SkopeoRepositoryType {
1818
DockerArchive = 'docker-archive',
19-
OciArchive = 'oci',
2019
ImageRegistry = 'docker',
21-
Directory = 'dir', // Note, skopeo marks this as a non-standard format
2220
}

src/scanner/index.ts

Lines changed: 38 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ import {
1717
ILocalWorkloadLocator,
1818
Telemetry,
1919
} from '../transmitter/types';
20-
import { IPullableImage, IScanImage } from './images/types';
20+
import {
21+
IPullableImage,
22+
IScanImage,
23+
SkopeoRepositoryType,
24+
} from './images/types';
2125
import {
2226
getWorkloadAlreadyScanned,
2327
getWorkloadImageAlreadyScanned,
@@ -29,7 +33,7 @@ export async function processWorkload(
2933
): Promise<void> {
3034
// every workload metadata references the same workload name, grab it from the first one
3135
const workloadName = workloadMetadata[0].name;
32-
const uniqueImages: IScanImage[] = getUniqueImages(workloadMetadata);
36+
const uniqueImages = getUniqueImages(workloadMetadata);
3337

3438
logger.info(
3539
{ workloadName, imageCount: uniqueImages.length },
@@ -74,42 +78,37 @@ export async function sendDeleteWorkloadRequest(
7478
}
7579

7680
export function getUniqueImages(workloadMetadata: IWorkload[]): IScanImage[] {
77-
const uniqueImages: { [key: string]: IScanImage } = workloadMetadata.reduce(
78-
(accum, meta) => {
79-
logger.info(
80-
{
81-
workloadName: workloadMetadata[0].name,
82-
name: meta.imageName,
83-
id: meta.imageId,
84-
},
85-
'image metadata',
86-
);
87-
// example: For DCR "redis:latest"
88-
// example: For GCR "gcr.io/test-dummy/redis:latest"
89-
// example: For ECR "291964488713.dkr.ecr.us-east-2.amazonaws.com/snyk/redis:latest"
90-
// meta.imageName can be different depends on CR
91-
const { imageName } = getImageParts(meta.imageName);
92-
// meta.imageId can be different depends on CR
93-
// example: For DCR "docker.io/library/redis@sha256:8e9f8546050da8aae393a41d65ad37166b4f0d8131d627a520c0f0451742e9d6"
94-
// example: For GCR "sha256:8e9f8546050da8aae393a41d65ad37166b4f0d8131d627a520c0f0451742e9d6"
95-
// example: For ECR "sha256:8e9f8546050da8aae393a41d65ad37166b4f0d8131d627a520c0f0451742e9d6"
96-
let digest: string | undefined = undefined;
97-
if (
98-
meta.imageId.lastIndexOf('@') > -1 ||
99-
meta.imageId.startsWith('sha')
100-
) {
101-
digest = meta.imageId.substring(meta.imageId.lastIndexOf('@') + 1);
102-
}
103-
104-
accum[meta.imageName] = {
105-
imageWithDigest: digest && `${imageName}@${digest}`,
106-
imageName: meta.imageName, // Image name with tag
107-
};
108-
109-
return accum;
110-
},
111-
{},
112-
);
81+
const uniqueImages = workloadMetadata.reduce((accum, meta) => {
82+
logger.info(
83+
{
84+
workloadName: workloadMetadata[0].name,
85+
name: meta.imageName,
86+
id: meta.imageId,
87+
},
88+
'image metadata',
89+
);
90+
// example: For DCR "redis:latest"
91+
// example: For GCR "gcr.io/test-dummy/redis:latest"
92+
// example: For ECR "291964488713.dkr.ecr.us-east-2.amazonaws.com/snyk/redis:latest"
93+
// meta.imageName can be different depends on CR
94+
const { imageName } = getImageParts(meta.imageName);
95+
// meta.imageId can be different depends on CR
96+
// example: For DCR "docker.io/library/redis@sha256:8e9f8546050da8aae393a41d65ad37166b4f0d8131d627a520c0f0451742e9d6"
97+
// example: For GCR "sha256:8e9f8546050da8aae393a41d65ad37166b4f0d8131d627a520c0f0451742e9d6"
98+
// example: For ECR "sha256:8e9f8546050da8aae393a41d65ad37166b4f0d8131d627a520c0f0451742e9d6"
99+
let digest: string | undefined = undefined;
100+
if (meta.imageId.lastIndexOf('@') > -1 || meta.imageId.startsWith('sha')) {
101+
digest = meta.imageId.substring(meta.imageId.lastIndexOf('@') + 1);
102+
}
103+
104+
accum[meta.imageName] = {
105+
imageWithDigest: digest && `${imageName}@${digest}`,
106+
imageName: meta.imageName, // Image name with tag
107+
skopeoRepoType: SkopeoRepositoryType.DockerArchive,
108+
};
109+
110+
return accum;
111+
}, {} as Record<string, IScanImage>);
113112

114113
return Object.values(uniqueImages);
115114
}
@@ -121,7 +120,7 @@ async function scanImagesAndSendResults(
121120
telemetry: Partial<Telemetry>,
122121
): Promise<void> {
123122
const imageScanStartTimestampMs = Date.now();
124-
const scannedImages = await scanImages(pulledImages);
123+
const scannedImages = await scanImages(pulledImages, telemetry);
125124
const imageScanDurationMs = Date.now() - imageScanStartTimestampMs;
126125

127126
if (scannedImages.length === 0) {

src/supervisor/watchers/handlers/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ async function isSupportedWorkload(
161161
attemptedApiCall.response.statusCode < 300
162162
);
163163
} catch (error) {
164-
logger.info(
164+
logger.debug(
165165
{ error, workloadKind },
166166
'Failed on Kubernetes API call to list DeploymentConfig',
167167
);
@@ -176,7 +176,7 @@ export async function setupInformer(
176176
const logContext: Record<string, unknown> = { namespace, workloadKind };
177177
const isSupported = await isSupportedWorkload(namespace, workloadKind);
178178
if (!isSupported) {
179-
logger.info(
179+
logger.debug(
180180
logContext,
181181
'The Kubernetes cluster does not support this workload',
182182
);

src/transmitter/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ export interface IRequestError {
101101
export interface Telemetry {
102102
enqueueDurationMs: number;
103103
queueSize: number;
104+
/** This metric captures the total duration to pull all images of a workload. */
104105
imagePullDurationMs: number;
106+
/** This metric captures the total duration to scan all images of a workload. */
105107
imageScanDurationMs: number;
108+
/** This metric captures the combined size of all images of a workload. */
109+
imageSizeBytes: number;
106110
}

test/system/kind.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ test('Kubernetes-Monitor with KinD', async (jestDoneCallback) => {
254254
enqueueDurationMs: expect.any(Number),
255255
imagePullDurationMs: expect.any(Number),
256256
imageScanDurationMs: expect.any(Number),
257+
imageSizeBytes: expect.any(Number),
257258
queueSize: expect.any(Number),
258259
},
259260
imageLocator: expect.objectContaining({

0 commit comments

Comments
 (0)