From 1e734414894029b152cb339add6059c8c18f54bf Mon Sep 17 00:00:00 2001 From: Prabhu Subramanian Date: Tue, 14 Jan 2025 10:50:22 +0000 Subject: [PATCH 1/2] Working occurrences evidence Signed-off-by: Prabhu Subramanian --- .github/workflows/build-base-images.yml | 77 +++++++++++++++++++ bin/evinse.js | 1 + .../cdxgen/debian/Dockerfile.ruby26 | 31 ++++++++ ci/base-images/debian/Dockerfile.ruby18 | 1 + ci/base-images/debian/Dockerfile.ruby26 | 32 ++++++++ ci/base-images/debian/Dockerfile.ruby33 | 1 + ci/base-images/debian/Dockerfile.ruby34 | 1 + ci/base-images/sle/Dockerfile.ruby25 | 2 + lib/evinser/evinser.js | 31 ++++++-- lib/helpers/envcontext.js | 22 +++++- types/lib/helpers/envcontext.d.ts.map | 2 +- 11 files changed, 190 insertions(+), 11 deletions(-) create mode 100644 ci/base-images/cdxgen/debian/Dockerfile.ruby26 create mode 100644 ci/base-images/debian/Dockerfile.ruby26 diff --git a/.github/workflows/build-base-images.yml b/.github/workflows/build-base-images.yml index 8b50d6450a..48c7b7418a 100644 --- a/.github/workflows/build-base-images.yml +++ b/.github/workflows/build-base-images.yml @@ -491,6 +491,83 @@ jobs: tags: ${{ steps.meta-debian-ruby18.outputs.tags }} labels: ${{ steps.meta-debian-ruby18.outputs.labels }} + debian-ruby26-image: + if: github.repository == 'CycloneDX/cdxgen' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta-debian-ruby26 + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/cyclonedx/debian-ruby26 + + - name: Build and push Docker images + uses: docker/build-push-action@v5 + with: + context: . + file: ci/base-images/debian/Dockerfile.ruby26 + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta-debian-ruby26.outputs.tags }} + labels: ${{ steps.meta-debian-ruby26.outputs.labels }} + + cdxgen-debian-ruby26-image: + if: github.repository == 'CycloneDX/cdxgen' + runs-on: ubuntu-latest + needs: debian-ruby26-image + permissions: + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta-cdxgen-debian-ruby26 + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/cyclonedx/cdxgen-debian-ruby26 + + - name: Build and push Docker images + uses: docker/build-push-action@v5 + if: github.ref == 'refs/heads/master' + with: + context: . + file: ci/base-images/cdxgen/debian/Dockerfile.ruby26 + platforms: linux/amd64,linux/arm64 + push: true + tags: ghcr.io/cyclonedx/cdxgen-debian-ruby26:v11 + labels: ${{ steps.meta-cdxgen-debian-ruby26.outputs.labels }} + sle-dotnet7-image: if: github.repository == 'CycloneDX/cdxgen' runs-on: ubuntu-latest diff --git a/bin/evinse.js b/bin/evinse.js index 0e703967d0..65a3cb5a2e 100755 --- a/bin/evinse.js +++ b/bin/evinse.js @@ -72,6 +72,7 @@ const args = yargs(hideBin(process.argv)) "php", "swift", "ios", + "ruby", ], }) .option("db-path", { diff --git a/ci/base-images/cdxgen/debian/Dockerfile.ruby26 b/ci/base-images/cdxgen/debian/Dockerfile.ruby26 new file mode 100644 index 0000000000..fe793c48ef --- /dev/null +++ b/ci/base-images/cdxgen/debian/Dockerfile.ruby26 @@ -0,0 +1,31 @@ +FROM ghcr.io/cyclonedx/debian-ruby26:master + +LABEL maintainer="CycloneDX" \ + org.opencontainers.image.authors="Team AppThreat " \ + org.opencontainers.image.source="https://github.com/CycloneDX/cdxgen" \ + org.opencontainers.image.url="https://github.com/CycloneDX/cdxgen" \ + org.opencontainers.image.version="rolling" \ + org.opencontainers.image.vendor="AppThreat" \ + org.opencontainers.image.licenses="Apache-2.0" \ + org.opencontainers.image.title="cdxgen" \ + org.opencontainers.image.description="Rolling image with cdxgen SBOM generator for Ruby 2.6 apps" \ + org.opencontainers.docker.cmd="docker run --rm -v /tmp:/tmp -p 9090:9090 -v $(pwd):/app:rw -t ghcr.io/cyclonedx/cdxgen-debian-ruby26:v11 -r /app --server" + +ENV CDXGEN_IN_CONTAINER=true \ + NODE_COMPILE_CACHE="/opt/cdxgen-node-cache" \ + CDXGEN_GEM_HOME="/tmp/gems" \ + ATOM_RUBY_HOME=/root/.rbenv/versions/3.4.1 \ + RUBY_CMD=/root/.rbenv/versions/3.4.1/bin/ruby \ + PYTHONPATH=/opt/pypi +ENV PATH=${PATH}:/usr/local/bin:/opt/pypi/bin:/opt/cdxgen/node_modules/.bin: + +COPY . /opt/cdxgen + +RUN cd /opt/cdxgen && corepack enable && corepack pnpm install --prod --package-import-method copy && corepack pnpm cache delete \ + && mkdir -p /opt/cdxgen-node-cache \ + && node /opt/cdxgen/bin/cdxgen.js --help \ + && rbastgen --help \ + && rm -rf ${CDXGEN_GEM_HOME} && mkdir -p ${CDXGEN_GEM_HOME} \ + && chmod a-w -R /opt + +ENTRYPOINT ["node", "/opt/cdxgen/bin/cdxgen.js"] diff --git a/ci/base-images/debian/Dockerfile.ruby18 b/ci/base-images/debian/Dockerfile.ruby18 index 9d8ff98a4f..d48c4486fd 100644 --- a/ci/base-images/debian/Dockerfile.ruby18 +++ b/ci/base-images/debian/Dockerfile.ruby18 @@ -31,6 +31,7 @@ RUN set -ex \ libncurses5-dev libsqlite3-dev libtool libyaml-dev pkg-config sqlite3 zlib1g-dev libgmp-dev libreadline6-dev libssl-dev libc-dev libxslt-dev libmagickwand-dev \ && command curl -sSL https://rvm.io/mpapis.asc | gpg2 --import - \ && command curl -sSL https://rvm.io/pkuczynski.asc | gpg2 --import - \ + && locale-gen en_US.UTF-8 \ && echo "export rvm_max_time_flag=20" >> ~/.rvmrc \ && curl -sSL https://get.rvm.io | bash -s stable --ruby=${RUBY_VERSION} \ && rvm use ruby-${RUBY_VERSION} \ diff --git a/ci/base-images/debian/Dockerfile.ruby26 b/ci/base-images/debian/Dockerfile.ruby26 new file mode 100644 index 0000000000..034c2e48d9 --- /dev/null +++ b/ci/base-images/debian/Dockerfile.ruby26 @@ -0,0 +1,32 @@ +FROM ruby:2.6.10 + +ARG JAVA_VERSION=23.0.1-tem +ARG NODE_VERSION=20.18.1 +ARG ATOM_RUBY_VERSION=3.4.1 + +ENV JAVA_VERSION=$JAVA_VERSION \ + JAVA_HOME="/opt/java/${JAVA_VERSION}" \ + ATOM_RUBY_VERSION=$ATOM_RUBY_VERSION \ + BUNDLE_SILENCE_ROOT_WARNING=true \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + LANGUAGE=en_US.UTF-8 \ + NVM_DIR="/root/.nvm" +ENV PATH=/root/.nvm/versions/node/v${NODE_VERSION}/bin:${PATH}:/usr/local/bin:/root/.local/bin:/root/.rbenv/bin: + +COPY ci/base-images/debian/install.sh /tmp/ + +RUN apt-get update && apt-get install -qq -y --no-install-recommends curl bash bzip2 git-core zip unzip make gawk \ + && apt-get install -qq -y build-essential gcc-9 g++-9 python2 libmagic-dev locales nodejs \ + && locale-gen en_US.UTF-8 \ + && gem install bundler -v 1.17.3 \ + && bundle config git.allow_insecure true \ + && chmod +x /tmp/install.sh \ + && SKIP_PYTHON=yes ./tmp/install.sh && rm /tmp/install.sh \ + && node -v \ + && npm -v \ + && npm install -g corepack \ + && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ + && rm -rf /var/lib/apt/lists/* + +CMD /bin/bash diff --git a/ci/base-images/debian/Dockerfile.ruby33 b/ci/base-images/debian/Dockerfile.ruby33 index 06af79f34c..8cba615147 100644 --- a/ci/base-images/debian/Dockerfile.ruby33 +++ b/ci/base-images/debian/Dockerfile.ruby33 @@ -18,6 +18,7 @@ COPY ci/base-images/debian/install.sh /tmp/ RUN apt-get update && apt-get install -qq -y --no-install-recommends curl bash bzip2 git-core zip unzip make gawk \ && apt-get install -qq -y build-essential python3 python3-pip python3-dev libmagic-dev locales \ + && locale-gen en_US.UTF-8 \ && chmod +x /tmp/install.sh \ && ./tmp/install.sh && rm /tmp/install.sh \ && node -v \ diff --git a/ci/base-images/debian/Dockerfile.ruby34 b/ci/base-images/debian/Dockerfile.ruby34 index 544f8f8b65..9a9f3ebd44 100644 --- a/ci/base-images/debian/Dockerfile.ruby34 +++ b/ci/base-images/debian/Dockerfile.ruby34 @@ -16,6 +16,7 @@ COPY ci/base-images/debian/install.sh /tmp/ RUN apt-get update && apt-get install -qq -y --no-install-recommends curl bash bzip2 git-core zip unzip make gawk \ && apt-get install -qq -y build-essential python3 python3-pip python3-dev libmagic-dev locales \ + && locale-gen en_US.UTF-8 \ && chmod +x /tmp/install.sh \ && ./tmp/install.sh && rm /tmp/install.sh \ && node -v \ diff --git a/ci/base-images/sle/Dockerfile.ruby25 b/ci/base-images/sle/Dockerfile.ruby25 index 5d0c57ef7f..e562090f51 100644 --- a/ci/base-images/sle/Dockerfile.ruby25 +++ b/ci/base-images/sle/Dockerfile.ruby25 @@ -41,6 +41,8 @@ RUN set -e; \ && mkdir -p "$(rbenv root)/plugins" \ && git clone https://github.com/rbenv/ruby-build.git --depth=1 "$(rbenv root)/plugins/ruby-build" \ && rbenv install ${ATOM_RUBY_VERSION} \ + && ruby --version \ + && java --version \ && zypper clean -a CMD /bin/bash diff --git a/lib/evinser/evinser.js b/lib/evinser/evinser.js index 34e091c04c..c98f9b051c 100644 --- a/lib/evinser/evinser.js +++ b/lib/evinser/evinser.js @@ -259,6 +259,8 @@ export async function createSlice( // Support for crypto slices aka CBOM if (sliceType === "reachables" && options.includeCrypto) { args.push("--include-crypto"); + } else if (sliceType === "usages") { + args.push("--remove-atom"); } args = args.concat([ "-l", @@ -308,6 +310,9 @@ export function purlToLanguage(purl, filePath) { case "composer": language = "php"; break; + case "gem": + language = "ruby"; + break; case "generic": language = "c"; } @@ -321,7 +326,7 @@ export function initFromSbom(components, language) { if (!comp || !comp.evidence) { continue; } - if (language === "php") { + if (["php", "ruby"].includes(language)) { (comp.properties || []) .filter((v) => v.name === "Namespaces") .forEach((v) => { @@ -591,19 +596,28 @@ export async function parseSliceUsages( for (const atype of [ [ausage?.targetObj?.isExternal, ausage?.targetObj?.typeFullName], [ausage?.targetObj?.isExternal, ausage?.targetObj?.resolvedMethod], + [ausage?.definedBy?.name?.includes("::"), ausage?.definedBy?.name], [ausage?.definedBy?.isExternal, ausage?.definedBy?.typeFullName], [ausage?.definedBy?.isExternal, ausage?.definedBy?.resolvedMethod], ...(ausage?.fields || []).map((f) => [f?.isExternal, f?.typeFullName]), ]) { + if ( + !atype[0] && + (!atype[1] || ["ANY", "(...)", ""].includes(atype[1])) + ) { + continue; + } if ( atype[0] !== false && !isFilterableType(language, userDefinedTypesMap, atype[1]) ) { if (!atype[1].includes("(") && !atype[1].includes(".py")) { typesToLookup.add(simplifyType(atype[1])); - // Javascript calls can be resolved to a precise line number only from the call nodes + // Javascript and Ruby calls can be resolved to a precise line number only from the call nodes if ( - ["javascript", "js", "ts", "typescript"].includes(language) && + ["javascript", "js", "ts", "typescript", "ruby"].includes( + language, + ) && ausageLine ) { if (atype[1].includes(":")) { @@ -626,7 +640,10 @@ export async function parseSliceUsages( .concat(ausage?.invokedCalls || []) .concat(ausage?.argToCalls || []) .concat(ausage?.procedures || [])) { - if (acall.resolvedMethod?.startsWith("@")) { + if ( + acall.resolvedMethod?.startsWith("@") || + acall?.callName?.includes("::") + ) { typesToLookup.add(acall.callName); if (acall.lineNumber) { addToOverrides( @@ -713,7 +730,7 @@ export async function parseSliceUsages( if (purlImportsMap && Object.keys(purlImportsMap).length) { for (const apurl of Object.keys(purlImportsMap)) { const apurlImports = purlImportsMap[apurl]; - if (["php", "python"].includes(language)) { + if (["php", "python", "ruby"].includes(language)) { for (const aimp of apurlImports) { if (atype.startsWith(aimp)) { if (!purlLocationMap[apurl]) { @@ -1295,7 +1312,7 @@ export function createEvinseFile(sliceArtefacts, options) { console.log(evinseOutFile, "created successfully."); } else { console.log( - "Unable to identify component evidence for the input SBOM. Only java, javascript, python, swift, and php projects are supported by evinse.", + "Unable to identify component evidence for the input SBOM. Only java, javascript, python, swift, php, and ruby projects are supported by evinse.", ); } if (tempDir?.startsWith(getTmpDir())) { @@ -1590,6 +1607,8 @@ export function getClassTypeFromSignature(language, typeFullName) { .replace(".__init__", ""); } else if (["php"].includes(language)) { typeFullName = typeFullName.split("->")[0].split("::")[0]; + } else if (["ruby"].includes(language)) { + typeFullName = typeFullName.split("::")[0]; } if ( typeFullName.startsWith(" Date: Tue, 14 Jan 2025 11:14:04 +0000 Subject: [PATCH 2/2] Working callstack and services evidence Signed-off-by: Prabhu Subramanian --- lib/evinser/evinser.js | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/lib/evinser/evinser.js b/lib/evinser/evinser.js index c98f9b051c..e8e8b04fc5 100644 --- a/lib/evinser/evinser.js +++ b/lib/evinser/evinser.js @@ -988,7 +988,9 @@ export function detectServicesFromUsages(language, slice, servicesMap = {}) { const definedBy = usage?.definedBy; let endpoints = []; let authenticated = undefined; - if (targetObj?.resolvedMethod) { + if (language === "ruby" && definedBy?.name?.includes("/")) { + endpoints = extractEndpoints(language, definedBy.name); + } else if (targetObj?.resolvedMethod) { if (language !== "php") { endpoints = extractEndpoints(language, targetObj?.resolvedMethod); } @@ -1046,7 +1048,7 @@ export function detectServicesFromUsages(language, slice, servicesMap = {}) { */ export function detectServicesFromUDT(language, userDefinedTypes, servicesMap) { if ( - ["python", "py", "c", "cpp", "c++", "php"].includes(language) && + ["python", "py", "c", "cpp", "c++", "php", "ruby"].includes(language) && userDefinedTypes && userDefinedTypes.length ) { @@ -1157,6 +1159,31 @@ export function extractEndpoints(language, code) { ); } break; + case "ruby": + case "rb": { + let urlPrefix = ""; + let urlSuffix = ""; + if (code.includes("namespace ")) { + urlPrefix = code.split("namespace ").pop().split(" ")[0]; + } + for (const m of ["get", "post", "delete", "options", "put", "head"]) { + if (code.includes(`${m} `)) { + urlSuffix = code.split(`${m} `).pop().split(" ")[0]; + } + } + if (code.includes("http") && code.includes('"')) { + endpoints = code.split('"').filter((s) => s.startsWith("http")); + } + if (urlPrefix !== "" || urlSuffix !== "") { + if (!endpoints) { + endpoints = []; + } + endpoints.push( + `${urlPrefix.replace(/['"]/g, "")}${urlSuffix.replace(/['"]/g, "")}`, + ); + } + break; + } default: endpoints = (code.match(/['"](.*?)['"]/gi) || []) .map((v) => v.replace(/["']/g, "").replace("\n", ""))