Skip to content

Commit

Permalink
Generate a summary spdx manifest when multiple manifests are loaded
Browse files Browse the repository at this point in the history
  • Loading branch information
rhyskoedijk committed Jan 3, 2025
1 parent abd1a2a commit a1a5cfb
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 3 deletions.
2 changes: 1 addition & 1 deletion shared/spdx/convertSpdxToSvg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { IDocument } from '../models/spdx/2.3/IDocument';

/**
* Convert an SPDX document to SVG graph diagram
* @param spdxJson The SPDX document
* @param spdx The SPDX document
* @return The SPDX as SVG buffer
*/
export async function convertSpdxToSvgAsync(spdx: IDocument): Promise<Buffer> {
Expand Down
42 changes: 42 additions & 0 deletions shared/spdx/mergeSpdxDocuments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import path from 'path';
import { getCreatorOrganization } from '../models/spdx/2.3/ICreationInfo';
import { DocumentVersion, IDocument } from '../models/spdx/2.3/IDocument';

/**
* Merge multiple SPDX documents to a single SPDX document
* @param packageName The name of the merged ackage
* @param packageVersion The version of the merged package
* @param sourceDocuments The source SPDX documents to be merged
* @return The merged SPDX document
*/
export function mergeSpdxDocuments(
packageName: string | undefined,
packageVersion: string | undefined,
sourceDocuments: IDocument[],
): IDocument {
const packageGuid = crypto.randomUUID();
const rootDocument = sourceDocuments[0];
const rootNamespaceUri = new URL(rootDocument.documentNamespace);
const rootOrganisation = getCreatorOrganization(rootDocument.creationInfo);
return {
SPDXID: 'SPDXRef-DOCUMENT',
spdxVersion: DocumentVersion.SPDX_2_3,
name: `${packageName} ${packageVersion}`.trim(),
dataLicense: rootDocument.dataLicense || 'CC0-1.0',
documentNamespace: `${rootNamespaceUri.protocol}//${rootNamespaceUri.host}/${packageName}/${packageVersion}/${packageGuid}`,
creationInfo: {
created: new Date().toISOString(),
creators: [`Organization: ${rootOrganisation}`, `Tool: rhyskoedijk/sbom-azure-devops-1.0`],
},
files: sourceDocuments.flatMap((d) =>
d.files.map((f) => ({
...f,
fileName: path.join(d.packages.find((p) => p.SPDXID == d.documentDescribes[0])?.name || '', f.fileName),
})),
),
packages: sourceDocuments.flatMap((d) => d.packages),
externalDocumentRefs: sourceDocuments.flatMap((d) => d.externalDocumentRefs),
relationships: sourceDocuments.flatMap((d) => d.relationships),
documentDescribes: sourceDocuments.flatMap((d) => d.documentDescribes),
};
}
60 changes: 58 additions & 2 deletions ui/sbom-report-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ZeroData } from 'azure-devops-ui/ZeroData';

import { ISbomBuildArtifact } from '../shared/models/ISbomBuildArtifact';
import { getDisplayNameForDocument, IDocument } from '../shared/models/spdx/2.3/IDocument';
import { mergeSpdxDocuments } from '../shared/spdx/mergeSpdxDocuments';
import { BuildRestClient } from './clients/BuildRestClient';
import { SbomDocumentPage } from './components/sbom/SbomDocumentPage';

Expand All @@ -26,7 +27,13 @@ const SPDX_JSON_ATTACHMENT_TYPE = 'spdx.json';
const SPDX_SVG_ATTACHMENT_TYPE = 'spdx.svg';

interface State {
build?: {
id: number;
name?: string;
number?: string;
};
artifacts?: ISbomBuildArtifact[];
summaryArtifact?: ISbomBuildArtifact;
loadingMessage?: string;
loadError?: any;
}
Expand Down Expand Up @@ -81,6 +88,13 @@ export class Root extends React.Component<{}, State> {
if (!buildId) {
throw new Error('Unable to access the current build data');
}
this.setState({
build: {
id: buildId,
name: (buildPageData.build as any)?.name || buildPageData.definition?.name,
number: (buildPageData.build as any)?.number || buildPageData.build?.buildNumber,
},
});

// Get all SPDX JSON artifact attachments for the current build
const buildClient = getClient(BuildRestClient);
Expand Down Expand Up @@ -158,6 +172,10 @@ export class Root extends React.Component<{}, State> {
}
}

// Generate a summary SBOM artifact if multiple artifacts were loaded
this.generateSbomSummaryArtifact();

// Update the state with the loaded SBOM artifacts
console.info(`Loaded ${Object.keys(sbomArtifacts).length} SBOM artifact(s) for build ${buildId}`);
this.selectedArtifactId.value = sbomArtifacts[0]?.spdxDocument?.documentNamespace || '';
this.setState({
Expand All @@ -178,6 +196,7 @@ export class Root extends React.Component<{}, State> {
private async loadSbomArtifactsFromFileUpload(files: File[]): Promise<void> {
for (const file of files) {
try {
// Load the SPDX JSON file
console.info(`Loading SBOM artifact from file upload '${file.name}'...`);
const spdxJsonStream = await file.arrayBuffer();
const spdxJsonDocument = JSON.parse(new TextDecoder().decode(spdxJsonStream)) as IDocument;
Expand All @@ -192,12 +211,16 @@ export class Root extends React.Component<{}, State> {
spdxJsonDocument: spdxJsonStream,
};

// Update the state with the new SBOM artifact
this.selectedArtifactId.value = newArtifact.id;
this.setState({
artifacts: [...(this.state.artifacts || []), newArtifact],
loadingMessage: undefined,
loadError: undefined,
});

// Generate a summary SBOM artifact if multiple artifacts were loaded
this.generateSbomSummaryArtifact();
} catch (error) {
console.error(error);
this.setState({ loadError: error });
Expand All @@ -206,6 +229,28 @@ export class Root extends React.Component<{}, State> {
}
}

/**
* Merge all SBOM artifacts into a single summary artifact
*/
private generateSbomSummaryArtifact() {
if (this.state?.artifacts) {
if (this.state.artifacts.length > 1) {
const mergedDocument = mergeSpdxDocuments(
this.state.build?.name,
this.state.build?.number,
this.state.artifacts.map((a) => a.spdxDocument),
);
this.setState({
summaryArtifact: {
id: mergedDocument.documentNamespace,
spdxDocument: mergedDocument,
spdxJsonDocument: new TextEncoder().encode(JSON.stringify(mergedDocument, null, 2)).buffer,
},
});
}
}
}

private onSelectedArtifactTabChanged = (newSpdxId: string) => {
this.selectedArtifactId.value = newSpdxId;
};
Expand Down Expand Up @@ -245,6 +290,13 @@ export class Root extends React.Component<{}, State> {
className="bolt-tabbar-grey bolt-tabbar-compact flex-shrink"
tabGroups={[{ id: 'manifests', name: 'Manifests' }]}
>
{this.state.summaryArtifact && (
<Tab
id={this.state.summaryArtifact.id}
name="Summary"
iconProps={{ iconName: 'ViewDashboard', className: 'margin-right-4' }}
/>
)}
{this.state.artifacts.map((artifact, index) => (
<Tab
key={index}
Expand All @@ -258,8 +310,12 @@ export class Root extends React.Component<{}, State> {
<TabContent>
<Observer selectedArtifactId={this.selectedArtifactId}>
{(props: { selectedArtifactId: string }) =>
props.selectedArtifactId &&
this.state.artifacts?.find((artifact) => artifact.id === props.selectedArtifactId) ? (
props.selectedArtifactId && this.state.summaryArtifact?.id === props.selectedArtifactId ? (
<SbomDocumentPage
artifact={this.state.summaryArtifact}
onLoadArtifacts={(files) => this.loadSbomArtifactsFromFileUpload(files)}
/>
) : this.state.artifacts?.find((artifact) => artifact.id === props.selectedArtifactId) ? (
<SbomDocumentPage
artifact={this.state.artifacts?.find((artifact) => artifact.id === props.selectedArtifactId)!}
onLoadArtifacts={(files) => this.loadSbomArtifactsFromFileUpload(files)}
Expand Down

0 comments on commit a1a5cfb

Please sign in to comment.