snyk-docker-plugin
Version:
Snyk CLI docker plugin
203 lines (185 loc) • 5.02 kB
text/typescript
import { PackageURL } from "packageurl-js";
import {
AnalysisType,
AnalyzedPackage,
AnalyzedPackageWithVersion,
IAptFiles,
ImagePackagesAnalysis,
OSRelease,
} from "../types";
export function analyze(
targetImage: string,
aptFiles: IAptFiles,
osRelease?: OSRelease,
): Promise<ImagePackagesAnalysis> {
const pkgs = parseDpkgFile(aptFiles.dpkgFile, osRelease);
if (aptFiles.extFile) {
setAutoInstalledPackages(aptFiles.extFile, pkgs);
}
return Promise.resolve({
Image: targetImage,
AnalyzeType: AnalysisType.Apt,
Analysis: pkgs,
});
}
export function analyzeDistroless(
targetImage: string,
aptFiles: string[],
osRelease?: OSRelease,
): Promise<ImagePackagesAnalysis> {
const analyzedPackages: AnalyzedPackageWithVersion[] = [];
for (const fileContent of aptFiles) {
const currentPackages = parseDpkgFile(fileContent, osRelease);
analyzedPackages.push(...currentPackages);
}
return Promise.resolve({
Image: targetImage,
AnalyzeType: AnalysisType.Apt,
Analysis: analyzedPackages,
});
}
function parseDpkgFile(
text: string,
osRelease?: OSRelease,
): AnalyzedPackageWithVersion[] {
const pkgs: AnalyzedPackageWithVersion[] = [];
let curPkg: AnalyzedPackageWithVersion | null = null;
for (const line of text.split("\n")) {
curPkg = parseDpkgLine(line, curPkg!, pkgs);
if (curPkg) {
curPkg.Purl = purl(curPkg, osRelease);
}
}
return pkgs;
}
const debianCodenames = new Map<string, string>([
["8", "jessie"],
["9", "stretch"],
["10", "buster"],
["11", "bullseye"],
["12", "bookworm"],
["13", "trixie"],
["unstable", "sid"],
]);
export function purl(
curPkg: AnalyzedPackageWithVersion,
osRelease?: OSRelease,
): string | undefined {
let vendor = "";
if (!curPkg.Name || !curPkg.Version) {
return undefined;
}
const qualifiers: { [key: string]: string } = {};
if (curPkg.Source && curPkg.SourceVersion) {
qualifiers.upstream = `${curPkg.Source}@${curPkg.SourceVersion}`;
} else if (curPkg.Source) {
qualifiers.upstream = curPkg.Source;
}
if (osRelease) {
const codenameOrVersion =
debianCodenames.get(osRelease.version) ?? osRelease.version;
qualifiers.distro = `${osRelease.name}-${codenameOrVersion}`;
vendor = osRelease.name;
}
return new PackageURL(
"deb",
vendor,
curPkg.Name,
curPkg.Version,
// make sure that we pass in undefined if there are no qualifiers, because
// the packageurl-js library doesn't handle that properly...
Object.keys(qualifiers).length !== 0 ? qualifiers : undefined,
undefined,
).toString();
}
function parseDpkgLine(
text: string,
curPkg: AnalyzedPackageWithVersion,
pkgs: AnalyzedPackageWithVersion[],
): AnalyzedPackageWithVersion {
const [key, value] = text.split(": ");
switch (key) {
case "Package":
curPkg = {
Name: value,
Version: "",
Source: undefined,
SourceVersion: undefined,
Provides: [],
Deps: {},
AutoInstalled: undefined,
};
pkgs.push(curPkg);
break;
case "Version":
curPkg.Version = value;
break;
case "Source":
/**
* The value may look something like this:
* libgcc6 (1.3.0-b1)
* <name> (<version>)
*
* For example, Syft matches these values with a regex:
* https://github.com/anchore/syft/blob/1764e1c3f6bd66781f8350d957a1f95e4d9ad3de/syft/pkg/cataloger/deb/parse_dpkg_db.go#L169-L173
*/
const parts = value.split(" ");
curPkg.Source = parts[0];
if (parts.length > 1) {
curPkg.SourceVersion = parts[1]
.trim()
.replace("(", "")
.replace(")", "");
}
break;
case "Provides":
for (let name of value.split(",")) {
name = name.trim().split(" ")[0];
curPkg.Provides.push(name);
}
break;
case "Pre-Depends":
case "Depends":
for (const depElem of value.split(",")) {
for (let name of depElem.split("|")) {
name = name.trim().split(" ")[0];
curPkg.Deps[name] = true;
}
}
break;
}
return curPkg;
}
function setAutoInstalledPackages(text: string, pkgs: AnalyzedPackage[]) {
const autoPkgs = parseExtFile(text);
for (const pkg of pkgs) {
if (autoPkgs[pkg.Name]) {
pkg.AutoInstalled = true;
}
}
}
interface PkgMap {
[name: string]: boolean;
}
function parseExtFile(text: string) {
const pkgMap: PkgMap = {};
let curPkgName: any = null;
for (const line of text.split("\n")) {
curPkgName = parseExtLine(line, curPkgName, pkgMap);
}
return pkgMap;
}
function parseExtLine(text: string, curPkgName: string, pkgMap: PkgMap) {
const [key, value] = text.split(": ");
switch (key) {
case "Package":
curPkgName = value;
break;
case "Auto-Installed":
if (parseInt(value, 10) === 1) {
pkgMap[curPkgName] = true;
}
break;
}
return curPkgName;
}