From 87e984ebe93360bf604a5d06a76c8df7b8aa9d96 Mon Sep 17 00:00:00 2001 From: Rhythm Garg Date: Mon, 22 Apr 2024 18:36:24 +0530 Subject: [PATCH] fix: add support for merging boms using cyclonedx-cli --- backend/Dockerfile | 10 +++++++++ backend/src/index.ts | 33 +++++++++++++++++++++++++++- backend/src/utils.ts | 52 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 99a8bc7..cd68ce0 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -3,6 +3,16 @@ ARG CI_ENV=noci ARG GIT_COMMIT=git_commit_undefined ARG GIT_BRANCH=git_branch_undefined ARG VERSION=not_versioned +ARG TARGETARCH +ARG TARGETOS +ARG CYCLONE_DX_CLI_VERSION=v0.25.0 +RUN if [ "$TARGETARCH" = "amd64" ]; then \ + wget https://github.com/CycloneDX/cyclonedx-cli/releases/download/${CYCLONE_DX_CLI_VERSION}/cyclonedx-linux-x64 ;\ + mv cyclonedx-linux-x64 /bin//cyclonedx-cli ;\ + elif [ "$TARGETARCH" = "arm64" ]; then \ + wget https://github.com/CycloneDX/cyclonedx-cli/releases/download/${CYCLONE_DX_CLI_VERSION}/cyclonedx-${TARGETOS}-${TARGETARCH} ;\ + mv cyclonedx-${TARGETOS}-${TARGETARCH} /bin//cyclonedx-cli ;\ +fi RUN mkdir /app RUN echo "version=$VERSION" > /app/version && echo "commit=$GIT_COMMIT" >> /app/version && echo "branch=$GIT_BRANCH" >> /app/version WORKDIR /app diff --git a/backend/src/index.ts b/backend/src/index.ts index 732eabf..de25b3e 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -68,6 +68,7 @@ const typeDefs = gql` allBoms: [Bom] findBom(bomSearch: BomSearch): [Bom] bomById(id: ID): Object + mergeBoms(ids: [ID]!, group: String!, name: String!, version: String!): Object } type Mutation { @@ -106,6 +107,13 @@ const typeDefs = gql` offset: Int } + input BomMerge { + ids: ID, + version: String, + name: String, + group: String + } + scalar Object scalar DateTime `; @@ -129,6 +137,14 @@ const resolvers = { retObj = byIdRows.rows[0].bom } return retObj + }, + mergeBoms: async (parent: any, mergeInput: any) : Promise => { + let queryRes = await utils.runQuery(`select * from rebom.boms where uuid::text in ('` + mergeInput.ids.join('\',\'') + `')`) + let boms = queryRes.rows as BomRecord[] + var mergedBom = null + if(boms.length) + mergedBom = mergeBoms(boms, mergeInput.group, mergeInput.name, mergeInput.version) + return mergedBom } }, Mutation: { @@ -325,4 +341,19 @@ async function startApolloServer(typeDefs: any, resolvers: any) { console.log(`🚀 Server ready at http://localhost:4000`); } -startApolloServer(typeDefs, resolvers) \ No newline at end of file +async function mergeBoms(boms :BomRecord[], group: String, name: String, version: String): Promise { + const bomPaths: String[] = await utils.createTmpFiles(boms.map(bom => bom.bom)) + + const response: Object = await utils.shellExec('cyclonedx-cli', ['merge', + '--output-format', 'json', + '--input-format', 'json', + '--group', group, + '--name', name, + '--version', version, + '--input-files', ...bomPaths + ]) + utils.deleteTmpFiles(bomPaths) + return response +} + +startApolloServer(typeDefs, resolvers) \ No newline at end of file diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 18a8814..e8b7388 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -1,4 +1,8 @@ const pg = require('pg') +const { spawn } = require('node:child_process') +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; // init db const pool = new pg.Pool({ @@ -26,4 +30,52 @@ export async function runQuery (query: string, params: string[]) : Promise // just in case the error handling itself throws an error. client.release() } +} + +export async function shellExec(cmd: string, args: any[], timeout?: number): Promise { // timeout = in ms + return new Promise((resolve, reject) => { + let options: any = {} + if (timeout) options.timeout = timeout + const child = spawn(cmd, args, options) + let resData = "" + child.stdout.on('data', (data: string)=> { + resData += data + }) + + child.stderr.on('data', (data: any) => { + console.error(`shell command error: ${data}`) + }) + + child.on('exit', (code: number) => { + if (code !== 0) console.log(`shell process exited with code ${code}`) + if (code === 0) { + if (resData) { + resData = resData.replace(/\n$/, "") + } + resolve(resData) + } else { + console.error(resData) + reject(resData) + } + }) + }) +} + +export async function createTmpFiles(dataArr: any[]): Promise { + const tmpDir = os.tmpdir(); + const filePaths: string[] = []; + + for (const data of dataArr) { + const tmpFilePath = path.join(tmpDir, `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`); + await fs.promises.writeFile(tmpFilePath, JSON.stringify(data)); + filePaths.push(tmpFilePath); + } + + return filePaths; +} + +export async function deleteTmpFiles(filePaths: string[]): Promise { + for (const filePath of filePaths) { + await fs.promises.unlink(filePath); + } } \ No newline at end of file