Skip to content

container sbom improvements #1685

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 15, 2025
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
343 changes: 274 additions & 69 deletions lib/cli/index.js

Large diffs are not rendered by default.

90 changes: 87 additions & 3 deletions lib/helpers/utils.js
Original file line number Diff line number Diff line change
@@ -6978,8 +6978,11 @@ export async function parseGemspecData(gemspecData, gemspecFile) {
if (["name", "version"].includes(aprop)) {
value = value.replace(/["']/g, "");
}
pkg[aprop] = value;
return;
// Do not set name=name or version=version
if (value !== aprop) {
pkg[aprop] = value;
break;
}
}
}
// Handle common problems
@@ -11514,9 +11517,10 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
}
}
}
let jarMetadata;
if ((!group || !name || !version) && safeExistsSync(manifestFile)) {
confidence = 0.8;
const jarMetadata = parseJarManifest(
jarMetadata = parseJarManifest(
readFileSync(manifestFile, {
encoding: "utf-8",
}),
@@ -15102,3 +15106,83 @@ export function recomputeScope(pkgList, dependencies) {
}
return pkgList;
}

/**
* Function to parse a list of environment variables to identify the paths containing executable binaries
*
* @param envValues {Array[String]} Environment variables list
* @returns {Array[String]} Binary Paths identified from the environment variables
*/
export function extractPathEnv(envValues) {
if (!envValues) {
return [];
}
let binPaths = new Set();
const shellVariables = {};
// Let's focus only on linux container images for now
for (const env of envValues) {
if (env.startsWith("PATH=")) {
binPaths = new Set(env.replace("PATH=", "").split(":"));
} else {
const tmpA = env.split("=");
if (tmpA.length === 2) {
shellVariables[`$${tmpA[0]}`] = tmpA[1];
shellVariables[`\${${tmpA[0]}}`] = tmpA[1];
}
}
}
binPaths = Array.from(binPaths);
const expandedBinPaths = [];
for (let apath of binPaths) {
// Filter empty paths
if (!apath.length) {
continue;
}
if (apath.includes("$")) {
for (const k of Object.keys(shellVariables)) {
apath = apath.replace(k, shellVariables[k]);
}
}
// We're here, but not all paths got substituted
// Let's ignore them for now instead of risking substitution based on host values.
// Eg: ${GITHUB_TOKEN} could get expanded with the values from the host
if (apath.length && !apath.includes("$")) {
expandedBinPaths.push(apath);
}
}
return expandedBinPaths;
}

/**
* Collect all executable files from the given list of binary paths
*
* @param binPaths {Array[String]} Paths containing potential binaries
* @return {Array[String]} List of executables
*/
export function collectExecutables(binPaths) {
if (!binPaths) {
return [];
}
let executables = [];
const ignoreList = [
"**/*.{h,c,cpp,hpp,man,txt,md,htm,html,jar,ear,war,zip,tar,egg,keepme,gitignore,json}",
"[",
];
Comment on lines +15167 to +15170

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The ignoreList includes several file extensions and patterns to exclude from the executable search. Review this list to ensure that it's comprehensive and doesn't inadvertently exclude legitimate executables. For example, are there any other common executable extensions that should be included?

const ignoreList = [
    "**/*.{h,c,cpp,hpp,man,txt,md,htm,html,jar,ear,war,zip,tar,egg,keepme,gitignore,json}",
    "[",
  ];

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gemini-code-assist, can you think of any other extension that could be added to this list to reduce false positives? Think of all the files you might find under /usr/bin, /usr/sbin, '/usr/local/bin` etc. Without accessing and checking for executable bits, I am interested in a quick filter to only collect executable binaries.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great question, @prabhu. To expand the ignoreList for reducing false positives when identifying executables, consider adding extensions and patterns related to:

  • Script files:
    • .sh (Shell scripts)
    • .py (Python scripts)
    • .rb (Ruby scripts)
    • .pl (Perl scripts)
    • .js (Node.js scripts - though these often lack execute permissions unless explicitly set)
    • .php (PHP scripts - commonly executed via a PHP interpreter)
  • Architecture-specific shared libraries:
    • .so (Shared object - Linux)
    • .dylib (Dynamic library - macOS)
  • Package manager related files:
    • Files without extensions in /opt/homebrew/bin or /usr/local/bin (Homebrew on macOS often installs executables without extensions)

However, adding script extensions might increase false positives, as many non-executable script files exist. A more precise approach would involve checking for the executable bit, but as you mentioned, you're aiming for a quick filter. You could also consider platform-specific executable extensions, but that might reduce portability.

Ultimately, the trade-off is between reducing false positives and potentially excluding legitimate executables. Balancing this depends on the specific use case and environment.

for (const apath of binPaths) {
try {
const files = globSync("*", {
cwd: apath,
absolute: true,
nocase: true,
nodir: true,
dot: true,
follow: true,
ignore: ignoreList,
});
executables = executables.concat(files);
} catch (err) {
// ignore
}
}
return Array.from(new Set(executables)).sort();
}
93 changes: 91 additions & 2 deletions lib/managers/binary.js
Original file line number Diff line number Diff line change
@@ -16,7 +16,9 @@ import {
MAX_BUFFER,
TIMEOUT_MS,
adjustLicenseInformation,
collectExecutables,
dirNameStr,
extractPathEnv,
findLicenseId,
getTmpDir,
isSpdxLicenseExpression,
@@ -278,6 +280,21 @@ const OS_DISTRO_ALIAS = {
"red hat enterprise linux 9": "rhel-9",
};

// TODO: Move the lists to a config file
const COMMON_RUNTIMES = [
"java",
"node",
"deno",
"bun",
"python",
"python3",
"ruby",
"php",
"php7",
"php8",
"perl",
];

export function getGoBuildInfo(src) {
if (GOVERSION_BIN) {
let result = spawnSync(GOVERSION_BIN, [src], {
@@ -356,10 +373,21 @@ export function executeSourcekitten(args) {
return undefined;
}

export function getOSPackages(src) {
/**
* Get the packages installed in the container image filesystem.
*
* @param src {String} Source directory containing the extracted filesystem.
* @param imageConfig {Object} Image configuration containing environment variables, command, entrypoints etc
*
* @returns {Object} Metadata containing packages, dependencies, etc
*/
export function getOSPackages(src, imageConfig) {
const pkgList = [];
const dependenciesList = [];
const allTypes = new Set();
const bundledSdks = new Set();
const bundledRuntimes = new Set();
const binPaths = extractPathEnv(imageConfig?.Env);
if (TRIVY_BIN) {
let imageType = "image";
const trivyCacheDir = join(homedir(), ".cache", "trivy");
@@ -696,6 +724,7 @@ export function getOSPackages(src) {
}
delete comp.properties;
pkgList.push(comp);
detectSdksRuntimes(comp, bundledSdks, bundledRuntimes);
const compDeps = retrieveDependencies(
tmpDependencies,
origBomRef,
@@ -721,19 +750,47 @@ export function getOSPackages(src) {
}
newComp["bom-ref"] = decodeURIComponent(newComp.purl);
pkgList.push(newComp);
detectSdksRuntimes(newComp, bundledSdks, bundledRuntimes);
}
}
}
}
}
}
let executables;
if (binPaths?.length) {
executables = fileComponents(collectExecutables(binPaths));
}
return {
osPackages: pkgList,
dependenciesList,
allTypes: Array.from(allTypes),
allTypes: Array.from(allTypes).sort(),
bundledSdks: Array.from(bundledSdks).sort(),
bundledRuntimes: Array.from(bundledRuntimes).sort(),
binPaths,
executables,
};
}

// Detect common sdks and runtimes from the name
function detectSdksRuntimes(comp, bundledSdks, bundledRuntimes) {
if (!comp?.name) {
return;
}
if (comp.name.includes("dotnet-sdk")) {
bundledSdks.add(comp.purl);
}
if (comp.name.includes("dotnet-runtime")) {
bundledRuntimes.add(comp.purl);
}
if (comp.name.includes("aspnet-runtime")) {
bundledRuntimes.add(comp.purl);
}
if (COMMON_RUNTIMES.includes(comp.name)) {
bundledRuntimes.add(comp.purl);
}
}

const retrieveDependencies = (tmpDependencies, origBomRef, comp) => {
try {
const tmpDependsOn = tmpDependencies[origBomRef] || [];
@@ -906,3 +963,35 @@ export function getBinaryBom(src, binaryBomFile, deepMode) {
}
return true;
}

function fileComponents(fileList) {
const components = [];
for (const f of fileList) {
const name = basename(f);
const purl = `pkg:generic/${name}`;
components.push({
name,
type: "file",
purl,
"bom-ref": purl,
properties: [
{ name: "SrcFile", value: f },
{ name: "internal:is_executable", value: "true" },
],
evidence: {
identity: {
field: "purl",
confidence: 0,
methods: [
{
technique: "filename",
confidence: 0,
value: f,
},
],
},
},
});
}
return components;
}
24 changes: 16 additions & 8 deletions lib/managers/docker.js
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ import got from "got";
import { x } from "tar";
import {
DEBUG_MODE,
extractPathEnv,
getAllFiles,
getTmpDir,
safeExistsSync,
@@ -556,6 +557,7 @@ export const parseImageName = (fullImageName) => {
fullImageName.includes("/") &&
(fullImageName.includes(".") || fullImageName.includes(":"))
) {
// TODO: Change to URL
const urlObj = parse(fullImageName);
const tmpA = fullImageName.split("/");
if (
@@ -742,7 +744,7 @@ export const getImage = async (fullImageName) => {
if (DEBUG_MODE) {
console.log(`Re-trying the pull with the name ${repoWithTag}.`);
}
pullData = await makeRequest(
await makeRequest(
`images/create?fromImage=${repoWithTag}`,
"POST",
registry,
@@ -1078,13 +1080,15 @@ export const extractFromManifest = async (
}
}
}
const binPaths = extractPathEnv(localData?.Config?.Env);
const exportData = {
inspectData: localData,
manifest,
allLayersDir: tempDir,
allLayersExplodedDir,
lastLayerConfig,
lastWorkingDir,
binPaths,
};
exportData.pkgPathList = getPkgPathList(exportData, lastWorkingDir);
return exportData;
@@ -1253,6 +1257,7 @@ export const getPkgPathList = (exportData, lastWorkingDir) => {
join(allLayersExplodedDir, "/usr/local/lib"),
join(allLayersExplodedDir, "/usr/local/lib64"),
join(allLayersExplodedDir, "/opt"),
join(allLayersExplodedDir, "/root"),
join(allLayersExplodedDir, "/home"),
join(allLayersExplodedDir, "/usr/share"),
join(allLayersExplodedDir, "/usr/src"),
@@ -1266,6 +1271,7 @@ export const getPkgPathList = (exportData, lastWorkingDir) => {
join(allLayersExplodedDir, "/usr/local/lib"),
join(allLayersExplodedDir, "/usr/local/lib64"),
join(allLayersExplodedDir, "/opt"),
join(allLayersExplodedDir, "/root"),
join(allLayersExplodedDir, "/usr/share"),
join(allLayersExplodedDir, "/usr/src"),
join(allLayersExplodedDir, "/var/www/html"),
@@ -1296,7 +1302,8 @@ export const getPkgPathList = (exportData, lastWorkingDir) => {
if (lastWorkingDir && lastWorkingDir !== "") {
if (
!lastWorkingDir.includes("/opt/") &&
!lastWorkingDir.includes("/home/")
!lastWorkingDir.includes("/home/") &&
!lastWorkingDir.includes("/root/")
) {
knownSysPaths.push(lastWorkingDir);
}
@@ -1320,13 +1327,17 @@ export const getPkgPathList = (exportData, lastWorkingDir) => {
// Build path list
for (const wpath of knownSysPaths) {
pathList = pathList.concat(wpath);
const nodeModuleDirs = getOnlyDirs(wpath, "node_modules");
if (nodeModuleDirs?.length) {
pathList.push(nodeModuleDirs[0]);
}
const pyDirs = getOnlyDirs(wpath, "site-packages");
if (pyDirs?.length) {
pathList = pathList.concat(pyDirs);
}
const gemsDirs = getOnlyDirs(wpath, "gems");
if (gemsDirs?.length) {
pathList = pathList.concat(gemsDirs);
pathList = pathList.concat(gemsDirs[0]);
}
const cargoDirs = getOnlyDirs(wpath, ".cargo");
if (cargoDirs?.length) {
@@ -1337,18 +1348,15 @@ export const getPkgPathList = (exportData, lastWorkingDir) => {
pathList = pathList.concat(composerDirs);
}
}
pathList = Array.from(new Set(pathList)).sort();
if (DEBUG_MODE) {
console.log("pathList", pathList);
}
return pathList;
};

export const removeImage = async (fullImageName, force = false) => {
const removeData = await makeRequest(
`images/${fullImageName}?force=${force}`,
"DELETE",
);
return removeData;
return await makeRequest(`images/${fullImageName}?force=${force}`, "DELETE");
};

export const getCredsFromHelper = (exeSuffix, serverAddress) => {
Loading