snyk-docker-plugin
Version:
Snyk CLI docker plugin
166 lines (148 loc) • 6.13 kB
text/typescript
import { PluginWarningsFact } from "./facts";
/**
* Validates a Docker image reference format using the official Docker reference regex.
* @param imageReference The Docker image reference to validate
* @returns true if valid, false if invalid
*/
export function isValidDockerImageReference(imageReference: string): boolean {
// Docker image reference validation regex from the official Docker packages:
// https://github.com/distribution/reference/blob/ff14fafe2236e51c2894ac07d4bdfc778e96d682/regexp.go#L9
// Original 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]))*|\[(?:[a-fA-F0-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]*)*[:][[:xdigit:]]{32,}))?$
// Note: Converted [[:xdigit:]] to [a-fA-F0-9] and escaped the forward slashes for JavaScript compatibility.
const dockerImageRegex =
/^((?:(?:(?:[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]))*|\[(?:[a-fA-F0-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,}))?$/;
return dockerImageRegex.test(imageReference);
}
// array[*] indicates to truncate each element to the indicated size
export const RESPONSE_SIZE_LIMITS = {
"containerConfig.data.user": { type: "string", limit: 1024 },
"containerConfig.data.exposedPorts": { type: "array", limit: 500 },
"containerConfig.data.exposedPorts[*]": { type: "string", limit: 64 },
"containerConfig.data.env": { type: "array", limit: 500 },
"containerConfig.data.env[*]": { type: "string", limit: 1024 },
"containerConfig.data.entrypoint": { type: "array", limit: 500 },
"containerConfig.data.entrypoint[*]": { type: "string", limit: 1024 },
"containerConfig.data.cmd": { type: "array", limit: 500 },
"containerConfig.data.cmd[*]": { type: "string", limit: 1024 },
"containerConfig.data.volumes": { type: "array", limit: 500 },
"containerConfig.data.volumes[*]": { type: "string", limit: 1024 },
"containerConfig.data.workingDir": { type: "string", limit: 1024 },
"containerConfig.data.stopSignal": { type: "string", limit: 128 },
"history.data": { type: "array", limit: 1000 },
"history.data[*].author": { type: "string", limit: 128 },
"history.data[*].createdBy": { type: "string", limit: 4096 },
"history.data[*].comment": { type: "string", limit: 4096 },
} as const;
interface TruncationInfo {
type: "array" | "string";
countAboveLimit: number;
}
export function truncateAdditionalFacts(facts: any[]): any[] {
const truncationTracker: Record<string, TruncationInfo> = {};
const processedFacts = facts.map((fact) => {
if (!fact || !fact.type || !fact.data) {
return fact;
}
if (fact.type === "depGraph") {
return fact;
}
const truncatedData = truncateDataValue(
fact.data,
fact.type,
"data",
truncationTracker,
);
return { ...fact, data: truncatedData };
});
if (Object.keys(truncationTracker).length > 0) {
const existingWarnings = processedFacts.find(
(f) => f.type === "pluginWarnings",
) as PluginWarningsFact | undefined;
if (existingWarnings) {
existingWarnings.data.truncatedFacts = truncationTracker;
} else {
const pluginWarningsFact: PluginWarningsFact = {
type: "pluginWarnings",
data: {
truncatedFacts: truncationTracker,
},
};
processedFacts.push(pluginWarningsFact);
}
}
return processedFacts;
}
function hasAnyLimitsForPath(factType: string, path: string): boolean {
const prefix = `${factType}.${path}`;
return Object.keys(RESPONSE_SIZE_LIMITS).some((limitKey) =>
limitKey.startsWith(prefix),
);
}
function truncateDataValue(
value: any,
factType: string,
path: string,
truncationTracker: Record<string, TruncationInfo>,
): any {
const limitKey = `${factType}.${path}`;
const limitConfig = RESPONSE_SIZE_LIMITS[limitKey];
// directly truncate if there's a match
if (limitConfig) {
value = truncateValue(value, limitConfig, limitKey, truncationTracker);
}
if (!hasAnyLimitsForPath(factType, path)) {
return value;
}
if (Array.isArray(value)) {
return value.map((item, index) => {
return truncateDataValue(item, factType, `${path}[*]`, truncationTracker);
});
} else if (typeof value === "object" && value !== null) {
const truncatedObject: any = {};
for (const [key, subValue] of Object.entries(value)) {
truncatedObject[key] = truncateDataValue(
subValue,
factType,
`${path}.${key}`,
truncationTracker,
);
}
return truncatedObject;
}
return value;
}
function truncateValue(
value: any,
limitConfig: any,
fieldPath: string,
truncationTracker: Record<string, TruncationInfo>,
): any {
switch (limitConfig.type) {
case "array":
if (Array.isArray(value) && value.length > limitConfig.limit) {
const truncatedCount = value.length - limitConfig.limit;
// report how many elements were truncated
truncationTracker[fieldPath] = {
type: "array",
countAboveLimit: truncatedCount,
};
return value.slice(0, limitConfig.limit);
}
break;
case "string":
if (typeof value === "string" && value.length > limitConfig.limit) {
const truncatedCount = value.length - limitConfig.limit;
// report the maximum number of characters that were truncated for this field
const existing = truncationTracker[fieldPath];
if (!existing || truncatedCount > existing.countAboveLimit) {
truncationTracker[fieldPath] = {
type: "string",
countAboveLimit: truncatedCount,
};
}
return value.substring(0, limitConfig.limit);
}
break;
}
return value;
}