Skip to content

Commit

Permalink
feat: support verbose gradle graphs for sbom generation (#290)
Browse files Browse the repository at this point in the history
  • Loading branch information
orsagie authored Oct 1, 2024
1 parent 756bc8e commit 4a69447
Show file tree
Hide file tree
Showing 7 changed files with 585 additions and 50 deletions.
41 changes: 32 additions & 9 deletions lib/graph.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DepGraphBuilder, PkgManager } from '@snyk/dep-graph';
import { DepGraphBuilder, PkgInfo, PkgManager } from '@snyk/dep-graph';

import type { CoordinateMap } from './types';

Expand All @@ -13,12 +13,14 @@ export interface GradleGraph {
interface QueueItem {
id: string;
parentId: string;
ancestry: string[];
}

export async function buildGraph(
gradleGraph: GradleGraph,
rootPkgName: string,
projectVersion: string,
verbose?: boolean,
coordinateMap?: CoordinateMap,
) {
const pkgManager: PkgManager = { name: 'gradle' };
Expand All @@ -33,15 +35,16 @@ export async function buildGraph(
return depGraphBuilder.build();
}

const visited: string[] = [];
const visitedMap: Record<string, PkgInfo> = {};
const queue: QueueItem[] = [];
queue.push(...findChildren('root-node', gradleGraph)); // queue direct dependencies
queue.push(...findChildren('root-node', [], gradleGraph)); // queue direct dependencies

// breadth first search
while (queue.length > 0) {
const item = queue.shift();
if (!item) continue;
let { id, parentId } = item;
const { ancestry } = item;
// take a copy as id maybe mutated below and we need this id when finding childing in GradleGraph
const gradleGraphId = `${id}`;
const node = gradleGraph[id];
Expand All @@ -59,32 +62,52 @@ export async function buildGraph(
parentId = coordinateMap[parentId];
}
}
if (visited.includes(id)) {

const visited = visitedMap[id];
if (!verbose && visited) {
const prunedId = id + ':pruned';
depGraphBuilder.addPkgNode({ name, version }, prunedId, {
labels: { pruned: 'true' },
});
depGraphBuilder.connectDep(parentId, prunedId);
continue; // don't queue any more children
}
depGraphBuilder.addPkgNode({ name, version }, id);
depGraphBuilder.connectDep(parentId, id);
queue.push(...findChildren(gradleGraphId, gradleGraph)); // queue children
visited.push(id);

if (verbose && ancestry.includes(id)) {
const prunedId = id + ':pruned';
depGraphBuilder.addPkgNode(visited, prunedId, {
labels: { pruned: 'cyclic' },
});
depGraphBuilder.connectDep(parentId, prunedId);
continue; // don't queue any more children
}

if (verbose && visited) {
// use visited node when omitted dependencies found (verbose)
depGraphBuilder.addPkgNode(visited, id);
depGraphBuilder.connectDep(parentId, id);
} else {
depGraphBuilder.addPkgNode({ name, version }, id);
depGraphBuilder.connectDep(parentId, id);
visitedMap[id] = { name, version };
}
// Remember to push updated ancestry here
queue.push(...findChildren(gradleGraphId, [...ancestry, id], gradleGraph)); // queue children
}

return depGraphBuilder.build();
}

export function findChildren(
parentId: string,
ancestry: string[],
gradleGraph: GradleGraph,
): QueueItem[] {
const result: QueueItem[] = [];
for (const id of Object.keys(gradleGraph)) {
const node = gradleGraph[id];
if (node?.parentIds?.includes(parentId)) {
result.push({ id, parentId });
result.push({ id, ancestry, parentId });
}
}
return result;
Expand Down
48 changes: 16 additions & 32 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import { getGradleAttributesPretty } from './gradle-attributes-pretty';
import { buildGraph, GradleGraph } from './graph';
import type {
CoordinateMap,
GradleInspectOptions,
PomCoords,
Sha1Map,
SnykHttpClient,
} from './types';
import { getMavenPackageInfo } from './search';
import debugModule = require('debug');
import { CliOptions } from './types';

type ScannedProject = legacyCommon.ScannedProject;

Expand All @@ -43,49 +45,25 @@ const cannotResolveVariantMarkers = [
'Unable to find a matching variant of project',
];

// TODO(kyegupov): the types below will be extracted to a common plugin interface library

export interface GradleInspectOptions {
// A regular expression (Java syntax, case-insensitive) to select Gradle configurations.
// If only one configuration matches, its attributes will be used for dependency resolution;
// otherwise, an artificial merged configuration will be created (see configuration-attributes
// below).
// Attributes are important for dependency resolution in Android builds (see
// https://developer.android.com/studio/build/dependencies#variant_aware )
// This replaces legacy `-- --configuration=foo` argument specification.
'configuration-matching'?: string;

// A comma-separated list of key:value pairs, e.g. "buildtype=release,usage=java-runtime".
// If specified, will scan all configurations for attributes with names that contain "keys" (case-insensitive)
// and have values that have a string representation that match the specified one, and will copy
// these attributes into the merged configuration.
// Attributes are important for dependency resolution in Android builds (see
// https://developer.android.com/studio/build/dependencies#variant_aware )
'configuration-attributes'?: string;

// For some reason, `--no-daemon` is not required for Unix, but on Windows, without this flag, apparently,
// Gradle process just never exits, from the Node's standpoint.
// Leaving default usage `--no-daemon`, because of backwards compatibility
daemon?: boolean;
initScript?: string;
gradleNormalizeDeps?: boolean;
}

type Options = api.InspectOptions & GradleInspectOptions;
type Options = api.InspectOptions & GradleInspectOptions & CliOptions;
type VersionBuildInfo = api.VersionBuildInfo;

// Overload type definitions, so that when you call inspect() with an `options` literal (e.g. in tests),
// you will get a result of a specific corresponding type.
export async function inspect(
root: string,
targetFile: string,
options?: api.SingleSubprojectInspectOptions & GradleInspectOptions,
options?: api.SingleSubprojectInspectOptions &
GradleInspectOptions &
CliOptions,
snykHttpClient?: SnykHttpClient,
): Promise<api.SinglePackageResult>;
export async function inspect(
root: string,
targetFile: string,
options: api.MultiSubprojectInspectOptions & GradleInspectOptions,
options: api.MultiSubprojectInspectOptions &
GradleInspectOptions &
CliOptions,
snykHttpClient?: SnykHttpClient,
): Promise<api.MultiProjectResult>;

Expand Down Expand Up @@ -526,7 +504,11 @@ async function getAllDeps(
concurrency: 100,
});
}
return await processProjectsInExtractedJSON(extractedJSON, coordinateMap);
return await processProjectsInExtractedJSON(
extractedJSON,
options['print-graph'],
coordinateMap,
);
} catch (err) {
const error: Error = err;
const gradleErrorMarkers = /^\s*>\s.*$/;
Expand Down Expand Up @@ -611,6 +593,7 @@ ${chalk.red.bold(mainErrorMessage)}`;

export async function processProjectsInExtractedJSON(
extractedJSON: JsonDepsScriptResult,
verbose?: boolean,
coordinateMap?: CoordinateMap,
) {
for (const projectId in extractedJSON.projects) {
Expand All @@ -632,6 +615,7 @@ export async function processProjectsInExtractedJSON(
gradleGraph,
rootPkgName,
projectVersion,
verbose,
coordinateMap,
);
// this property usage ends here
Expand Down
31 changes: 31 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,34 @@ export interface GetPackageData {
data: GetPackageResponseData;
links: GetPackageLinks;
}

// TODO(kyegupov): the types below will be extracted to a common plugin interface library
export interface GradleInspectOptions {
// A regular expression (Java syntax, case-insensitive) to select Gradle configurations.
// If only one configuration matches, its attributes will be used for dependency resolution;
// otherwise, an artificial merged configuration will be created (see configuration-attributes
// below).
// Attributes are important for dependency resolution in Android builds (see
// https://developer.android.com/studio/build/dependencies#variant_aware )
// This replaces legacy `-- --configuration=foo` argument specification.
'configuration-matching'?: string;

// A comma-separated list of key:value pairs, e.g. "buildtype=release,usage=java-runtime".
// If specified, will scan all configurations for attributes with names that contain "keys" (case-insensitive)
// and have values that have a string representation that match the specified one, and will copy
// these attributes into the merged configuration.
// Attributes are important for dependency resolution in Android builds (see
// https://developer.android.com/studio/build/dependencies#variant_aware )
'configuration-attributes'?: string;

// For some reason, `--no-daemon` is not required for Unix, but on Windows, without this flag, apparently,
// Gradle process just never exits, from the Node's standpoint.
// Leaving default usage `--no-daemon`, because of backwards compatibility
daemon?: boolean;
initScript?: string;
gradleNormalizeDeps?: boolean;
}

export interface CliOptions {
'print-graph'?: boolean; // this will need to change as it will affect all gradle sboms
}
13 changes: 13 additions & 0 deletions test/fixtures/verbose/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
plugins {
id 'java'
}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.apache.ignite:ignite-spring:2.13.0'
implementation 'org.apache.ignite:ignite-indexing:2.13.0'
implementation 'org.apache.ignite:ignite-core:2.13.0'
}
Loading

0 comments on commit 4a69447

Please sign in to comment.