1 | import * as fs from "fs";
|
2 | import * as path from "path";
|
3 |
|
4 | import { getImageArchive } from "./analyzer/image-inspector";
|
5 | import { readDockerfileAndAnalyse } from "./dockerfile";
|
6 | import { DockerFileAnalysis } from "./dockerfile/types";
|
7 | import { ImageName } from "./extractor/image";
|
8 | import { fullImageSavePath } from "./image-save-path";
|
9 | import { getArchivePath, getImageType } from "./image-type";
|
10 | import { isNumber, isTrue } from "./option-utils";
|
11 | import * as staticModule from "./static";
|
12 | import { ImageType, PluginOptions, PluginResponse } from "./types";
|
13 |
|
14 |
|
15 | export function mergeEnvVarsIntoCredentials(
|
16 | options: Partial<PluginOptions>,
|
17 | ): void {
|
18 | options.username = options.username || process.env.SNYK_REGISTRY_USERNAME;
|
19 | options.password = options.password || process.env.SNYK_REGISTRY_PASSWORD;
|
20 | }
|
21 |
|
22 | export async function scan(
|
23 | options?: Partial<PluginOptions>,
|
24 | ): Promise<PluginResponse> {
|
25 | if (!options) {
|
26 | throw new Error("No plugin options provided");
|
27 | }
|
28 |
|
29 | mergeEnvVarsIntoCredentials(options);
|
30 |
|
31 | if (!options.path) {
|
32 | throw new Error("No image identifier or path provided");
|
33 | }
|
34 |
|
35 | const nestedJarsDepth =
|
36 | options["nested-jars-depth"] || options["shaded-jars-depth"];
|
37 | if (
|
38 | (isTrue(nestedJarsDepth) || isNumber(nestedJarsDepth)) &&
|
39 | isTrue(options["exclude-app-vulns"])
|
40 | ) {
|
41 | throw new Error(
|
42 | "To use --nested-jars-depth, you must not use --exclude-app-vulns",
|
43 | );
|
44 | }
|
45 |
|
46 | if (
|
47 | (!isNumber(nestedJarsDepth) &&
|
48 | !isTrue(nestedJarsDepth) &&
|
49 | typeof nestedJarsDepth !== "undefined") ||
|
50 | Number(nestedJarsDepth) < 0
|
51 | ) {
|
52 | throw new Error(
|
53 | "--nested-jars-depth accepts only numbers bigger than or equal to 0",
|
54 | );
|
55 | }
|
56 |
|
57 |
|
58 | if (options.globsToFind) {
|
59 | options.globsToFind.include = options.globsToFind.include.filter(
|
60 | (glob) => !glob.includes("composer"),
|
61 | );
|
62 | }
|
63 |
|
64 | const targetImage = appendLatestTagIfMissing(options.path);
|
65 |
|
66 | const dockerfilePath = options.file;
|
67 | const dockerfileAnalysis = await readDockerfileAndAnalyse(dockerfilePath);
|
68 |
|
69 | const imageType = getImageType(targetImage);
|
70 | switch (imageType) {
|
71 | case ImageType.DockerArchive:
|
72 | case ImageType.OciArchive:
|
73 | return localArchiveAnalysis(
|
74 | targetImage,
|
75 | imageType,
|
76 | dockerfileAnalysis,
|
77 | options,
|
78 | );
|
79 | case ImageType.Identifier:
|
80 | return imageIdentifierAnalysis(
|
81 | targetImage,
|
82 | imageType,
|
83 | dockerfileAnalysis,
|
84 | options,
|
85 | );
|
86 |
|
87 | default:
|
88 | throw new Error("Unhandled image type for image " + targetImage);
|
89 | }
|
90 | }
|
91 |
|
92 | async function localArchiveAnalysis(
|
93 | targetImage: string,
|
94 | imageType: ImageType,
|
95 | dockerfileAnalysis: DockerFileAnalysis | undefined,
|
96 | options: Partial<PluginOptions>,
|
97 | ): Promise<PluginResponse> {
|
98 | const globToFind = {
|
99 | include: options.globsToFind?.include || [],
|
100 | exclude: options.globsToFind?.exclude || [],
|
101 | };
|
102 |
|
103 | const archivePath = getArchivePath(targetImage);
|
104 | if (!fs.existsSync(archivePath)) {
|
105 | throw new Error(
|
106 | "The provided archive path does not exist on the filesystem",
|
107 | );
|
108 | }
|
109 | if (!fs.lstatSync(archivePath).isFile()) {
|
110 | throw new Error("The provided archive path is not a file");
|
111 | }
|
112 |
|
113 | const imageIdentifier =
|
114 | options.imageNameAndTag ||
|
115 |
|
116 | path.basename(archivePath);
|
117 |
|
118 | let imageName: ImageName | undefined;
|
119 | if (
|
120 | (options.digests?.manifest || options.digests?.index) &&
|
121 | options.imageNameAndTag
|
122 | ) {
|
123 | imageName = new ImageName(options.imageNameAndTag, {
|
124 | manifest: options.digests?.manifest,
|
125 | index: options.digests?.index,
|
126 | });
|
127 | }
|
128 |
|
129 | return await staticModule.analyzeStatically(
|
130 | imageIdentifier,
|
131 | dockerfileAnalysis,
|
132 | imageType,
|
133 | archivePath,
|
134 | globToFind,
|
135 | options,
|
136 | imageName,
|
137 | );
|
138 | }
|
139 |
|
140 | async function imageIdentifierAnalysis(
|
141 | targetImage: string,
|
142 | imageType: ImageType,
|
143 | dockerfileAnalysis: DockerFileAnalysis | undefined,
|
144 | options: Partial<PluginOptions>,
|
145 | ): Promise<PluginResponse> {
|
146 | const globToFind = {
|
147 | include: options.globsToFind?.include || [],
|
148 | exclude: options.globsToFind?.exclude || [],
|
149 | };
|
150 |
|
151 | const imageSavePath = fullImageSavePath(options.imageSavePath);
|
152 | const archiveResult = await getImageArchive(
|
153 | targetImage,
|
154 | imageSavePath,
|
155 | options.username,
|
156 | options.password,
|
157 | options.platform,
|
158 | );
|
159 |
|
160 | const imagePath = archiveResult.path;
|
161 | const imageName = archiveResult.imageName;
|
162 | try {
|
163 | return await staticModule.analyzeStatically(
|
164 | targetImage,
|
165 | dockerfileAnalysis,
|
166 | imageType,
|
167 | imagePath,
|
168 | globToFind,
|
169 | options,
|
170 | imageName,
|
171 | );
|
172 | } finally {
|
173 | archiveResult.removeArchive();
|
174 | }
|
175 | }
|
176 |
|
177 | export function appendLatestTagIfMissing(targetImage: string): string {
|
178 | if (
|
179 | getImageType(targetImage) === ImageType.Identifier &&
|
180 | !targetImage.includes(":")
|
181 | ) {
|
182 | return `${targetImage}:latest`;
|
183 | }
|
184 | return targetImage;
|
185 | }
|