├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ ├── release.yml │ ├── sbom_dev.yml │ └── sbom_release.yml ├── .gitignore ├── .goreleaser.yaml ├── .tool-versions ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── assemble.go ├── dt.go ├── edit.go ├── generate.go ├── root.go └── version.go ├── e2e ├── edit_test.go └── testdata │ └── edit │ ├── edit_test.txt │ ├── expected-output-lite.spdx.json │ └── photon-lite.spdx.json ├── go.mod ├── go.sum ├── internal └── version │ └── version.go ├── main.go ├── pkg ├── assemble │ ├── cdx │ │ ├── interface.go │ │ ├── merge.go │ │ ├── uniq_comp_service.go │ │ └── util.go │ ├── combiner.go │ ├── config.go │ ├── interface.go │ ├── spdx │ │ ├── interface.go │ │ ├── merge.go │ │ └── utils.go │ └── util.go ├── detect │ └── detect.go ├── dt │ ├── dt_interface.go │ └── interface.go ├── edit │ ├── cdx.go │ ├── cdx_edit.go │ ├── config.go │ ├── interface.go │ ├── spdx.go │ ├── spdx_edit.go │ └── utils.go ├── licenses │ ├── embed_licenses.go │ ├── files │ │ ├── licenses_aboutcode.json │ │ ├── licenses_spdx.json │ │ └── licenses_spdx_exception.json │ └── license.go └── logger │ └── log.go └── samples ├── cdx ├── sbomex-cdx.json ├── sbomgr-cdx.json └── sbomqs-cdx.json └── spdx ├── issue-56 ├── example6-bin.spdx ├── example6-lib.spdx └── example6-src.spdx ├── issue-67 ├── example6-bin.spdx ├── example6-lib.spdx └── example6-src.spdx ├── issue-76 ├── duplicate1.spdx └── duplicate2.spdx ├── issue-77 ├── main.spdx └── other.spdx ├── sbom-tool ├── sbomex-spdx.json ├── sbomgr-spdx.json └── sbomqs-spdx.json ├── syft ├── sbomex-spdx.json ├── sbomgr-spdx.json └── sbomqs-spdx.json └── zephyr ├── 96b_avenger96-shell_module-app.spdx ├── 96b_avenger96-shell_module-build.spdx └── 96b_avenger96-shell_module-zephyr.spdx /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Release | Build GHCR image 2 | on: 3 | release: 4 | types: [published] 5 | workflow_dispatch: 6 | 7 | env: 8 | REGISTRY: ghcr.io 9 | IMAGE_NAME: ${{ github.repository }} 10 | 11 | jobs: 12 | build-and-push-image: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | packages: write 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - name: GHCR login 23 | uses: docker/login-action@v3 24 | with: 25 | registry: "${{ env.REGISTRY }}" 26 | username: "${{ github.actor }}" 27 | password: "${{ secrets.GITHUB_TOKEN }}" 28 | - name: Set up QEMU 29 | uses: docker/setup-qemu-action@v3 30 | - name: Set up Docker Buildx 31 | uses: docker/setup-buildx-action@v3 32 | - name: Extract metadata (tags, labels) for Docker 33 | id: meta 34 | uses: docker/metadata-action@v5 35 | with: 36 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 37 | - name: Build and push 38 | uses: docker/build-push-action@v6 39 | with: 40 | context: . 41 | platforms: linux/amd64, linux/arm64 42 | push: true 43 | tags: ${{ steps.meta.outputs.tags }} 44 | labels: ${{ steps.meta.outputs.labels }} 45 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release | Build Binary 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | workflow_dispatch: 8 | 9 | env: 10 | TOOL_NAME: ${{ github.repository }} 11 | LATEST_TAG: v0.0.1 12 | SUPPLIER_NAME: Interlynk 13 | SUPPLIER_URL: https://interlynk.io 14 | PYLYNK_TEMP_DIR: $RUNNER_TEMP/pylynk 15 | SBOM_TEMP_DIR: $RUNNER_TEMP/sbom 16 | MS_SBOM_TOOL_URL: https://github.com/microsoft/sbom-tool/releases/latest/download/sbom-tool-linux-x64 17 | MS_SBOM_TOOL_EXCLUDE_DIRS: "**/samples/**" 18 | 19 | jobs: 20 | releaser: 21 | runs-on: ubuntu-latest 22 | permissions: 23 | id-token: write 24 | contents: write 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 29 | - run: git fetch --force --tags 30 | - uses: actions/setup-go@v3 31 | with: 32 | go-version: ">=1.20" 33 | check-latest: true 34 | cache: true 35 | 36 | - name: Get Tag 37 | id: get_tag 38 | run: echo "LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo 'v0.0.1')" >> $GITHUB_ENV 39 | 40 | - name: Goreleaser 41 | uses: goreleaser/goreleaser-action@v6 42 | with: 43 | install-only: true 44 | 45 | - run: go version 46 | - run: goreleaser -v 47 | 48 | - name: Download sbom-tool 49 | run: | 50 | curl -Lo $RUNNER_TEMP/sbom-tool ${{ env.MS_SBOM_TOOL_URL }} 51 | chmod +x $RUNNER_TEMP/sbom-tool 52 | 53 | - name: Releaser 54 | run: make release 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | 58 | - name: Generate SBOM 59 | shell: bash 60 | run: | 61 | cd ${{ github.workspace }} 62 | mkdir -p ${{ env.SBOM_TEMP_DIR}} 63 | $RUNNER_TEMP/sbom-tool generate -b ${{ env.SBOM_TEMP_DIR }} -bc . -pn ${{ env.TOOL_NAME }} -pv ${{ env.LATEST_TAG }} -ps ${{ env.SUPPLIER_NAME}} -nsb ${{ env.SUPPLIER_URL }} -cd "--DirectoryExclusionList ${{ env.MS_SBOM_TOOL_EXCLUDE_DIRS }}" 64 | ls -lR ${{ env.SBOM_TEMP_DIR }} 65 | 66 | - name: Upload SBOM as Release Asset 67 | uses: actions/upload-artifact@v4 68 | with: 69 | name: sbom 70 | path: /home/runner/work/_temp/sbom/_manifest/spdx_2.2/manifest.spdx.json 71 | if-no-files-found: error 72 | -------------------------------------------------------------------------------- /.github/workflows/sbom_dev.yml: -------------------------------------------------------------------------------- 1 | name: Dev | Build SBOM 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - "main" 7 | pull_request: 8 | branches-ignore: 9 | - "main" 10 | workflow_dispatch: 11 | 12 | env: 13 | TOOL_NAME: ${{ github.repository }} 14 | SUPPLIER_NAME: Interlynk 15 | SUPPLIER_URL: https://interlynk.io 16 | DEFAULT_TAG: v0.0.1 17 | PYLYNK_TEMP_DIR: $RUNNER_TEMP/pylynk 18 | SBOM_TEMP_DIR: $RUNNER_TEMP/sbom 19 | SBOM_ENV: development 20 | SBOM_FILE_PATH: $RUNNER_TEMP/sbom/_manifest/spdx_2.2/manifest.spdx.json 21 | MS_SBOM_TOOL_URL: https://github.com/microsoft/sbom-tool/releases/latest/download/sbom-tool-linux-x64 22 | MS_SBOM_TOOL_EXCLUDE_DIRS: "**/samples/**" 23 | 24 | jobs: 25 | build-sbom: 26 | name: Build SBOM 27 | runs-on: ubuntu-latest 28 | permissions: 29 | id-token: write 30 | contents: write 31 | steps: 32 | - name: Checkout Repository 33 | uses: actions/checkout@v4 34 | with: 35 | fetch-depth: 0 36 | 37 | - name: Get Tag 38 | id: get_tag 39 | run: echo "LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo 'v0.0.1')" >> $GITHUB_ENV 40 | 41 | - name: Set up Python 42 | uses: actions/setup-python@v5 43 | with: 44 | python-version: "3.x" # Specify the Python version needed 45 | 46 | - name: Checkout Python SBOM tool 47 | run: | 48 | git clone https://github.com/interlynk-io/pylynk.git ${{ env.PYLYNK_TEMP_DIR }} 49 | cd ${{ env.PYLYNK_TEMP_DIR }} 50 | git fetch --tags 51 | latest_tag=$(git describe --tags `git rev-list --tags --max-count=1`) 52 | git checkout $latest_tag 53 | echo "Checked out pylynk at tag: $latest_tag" 54 | 55 | - name: Install Python dependencies 56 | run: | 57 | cd ${{ env.PYLYNK_TEMP_DIR }} 58 | pip install -r requirements.txt 59 | 60 | - name: Generate SBOM 61 | shell: bash 62 | run: | 63 | cd ${{ github.workspace }} 64 | mkdir -p ${{ env.SBOM_TEMP_DIR}} 65 | curl -Lo $RUNNER_TEMP/sbom-tool ${{ env.MS_SBOM_TOOL_URL }} 66 | chmod +x $RUNNER_TEMP/sbom-tool 67 | SANITIZED_REF=$(echo "${{ github.ref_name}}" | sed -e 's/[^a-zA-Z0-9.-]/-/g' -e 's/^[^a-zA-Z0-9]*//g') 68 | VERSION=${{ env.LATEST_TAG }}-$SANITIZED_REF 69 | $RUNNER_TEMP/sbom-tool generate -b ${{ env.SBOM_TEMP_DIR }} -bc . -pn ${{ env.TOOL_NAME }} -pv $VERSION -ps ${{ env.SUPPLIER_NAME}} -nsb ${{ env.SUPPLIER_URL }} -cd "--DirectoryExclusionList ${{ env.MS_SBOM_TOOL_EXCLUDE_DIRS }}" 70 | 71 | - name: Upload SBOM 72 | run: | 73 | python3 ${{ env.PYLYNK_TEMP_DIR }}/pylynk.py --verbose upload --prod ${{env.TOOL_NAME}} --env ${{ env.SBOM_ENV }} --sbom ${{ env.SBOM_FILE_PATH }} --token ${{ secrets.INTERLYNK_SECURITY_TOKEN }} 74 | -------------------------------------------------------------------------------- /.github/workflows/sbom_release.yml: -------------------------------------------------------------------------------- 1 | name: Release | Build SBOM 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | env: 9 | TOOL_NAME: ${{ github.repository }} 10 | SUPPLIER_NAME: Interlynk 11 | SUPPLIER_URL: https://interlynk.io 12 | DEFAULT_TAG: v0.0.1 13 | PYLYNK_TEMP_DIR: $RUNNER_TEMP/pylynk 14 | SBOM_TEMP_DIR: $RUNNER_TEMP/sbom 15 | SBOM_ENV: default 16 | SBOM_FILE_PATH: $RUNNER_TEMP/sbom/_manifest/spdx_2.2/manifest.spdx.json 17 | MS_SBOM_TOOL_URL: https://github.com/microsoft/sbom-tool/releases/latest/download/sbom-tool-linux-x64 18 | MS_SBOM_TOOL_EXCLUDE_DIRS: "**/samples/**" 19 | 20 | jobs: 21 | build-sbom: 22 | name: Build SBOM 23 | runs-on: ubuntu-latest 24 | permissions: 25 | id-token: write 26 | contents: write 27 | steps: 28 | - name: Checkout Repository 29 | uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 0 32 | 33 | - name: Get Tag 34 | id: get_tag 35 | run: echo "LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo 'v0.0.1')" >> $GITHUB_ENV 36 | 37 | - name: Set up Python 38 | uses: actions/setup-python@v5 39 | with: 40 | python-version: "3.x" # Specify the Python version needed 41 | 42 | - name: Checkout Python SBOM tool 43 | run: | 44 | git clone https://github.com/interlynk-io/pylynk.git ${{ env.PYLYNK_TEMP_DIR }} 45 | cd ${{ env.PYLYNK_TEMP_DIR }} 46 | git fetch --tags 47 | latest_tag=$(git describe --tags `git rev-list --tags --max-count=1`) 48 | git checkout $latest_tag 49 | echo "Checked out pylynk at tag: $latest_tag" 50 | 51 | - name: Install Python dependencies 52 | run: | 53 | cd ${{ env.PYLYNK_TEMP_DIR }} 54 | pip install -r requirements.txt 55 | 56 | - name: Generate SBOM 57 | shell: bash 58 | run: | 59 | cd ${{ github.workspace }} 60 | mkdir -p ${{ env.SBOM_TEMP_DIR}} 61 | curl -Lo $RUNNER_TEMP/sbom-tool ${{ env.MS_SBOM_TOOL_URL }} 62 | chmod +x $RUNNER_TEMP/sbom-tool 63 | $RUNNER_TEMP/sbom-tool generate -b ${{ env.SBOM_TEMP_DIR }} -bc . -pn ${{ env.TOOL_NAME }} -pv ${{ env.LATEST_TAG }} -ps ${{ env.SUPPLIER_NAME}} -nsb ${{ env.SUPPLIER_URL }} -cd "--DirectoryExclusionList ${{ env.MS_SBOM_TOOL_EXCLUDE_DIRS }}" 64 | 65 | - name: Upload SBOM 66 | run: | 67 | python3 ${{ env.PYLYNK_TEMP_DIR }}/pylynk.py --verbose upload --prod ${{env.TOOL_NAME}} --env ${{ env.SBOM_ENV }} --sbom ${{ env.SBOM_FILE_PATH }} --token ${{ secrets.INTERLYNK_SECURITY_TOKEN }} 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | .DS_Store 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | vendor/ 17 | build/ 18 | 19 | version.txt 20 | 21 | dist/ 22 | _manifest/ 23 | 24 | .claude/ 25 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: sbomasm 2 | 3 | version: 2 4 | 5 | env: 6 | - GO111MODULE=on 7 | 8 | before: 9 | hooks: 10 | - go mod tidy 11 | - /bin/bash -c 'if [ -n "$(git --no-pager diff --exit-code go.mod go.sum)" ]; then exit 1; fi' 12 | 13 | gomod: 14 | proxy: true 15 | 16 | builds: 17 | - id: binaries 18 | binary: sbomasm-{{ .Os }}-{{ .Arch }} 19 | no_unique_dist_dir: true 20 | main: . 21 | flags: 22 | - -trimpath 23 | mod_timestamp: '{{ .CommitTimestamp }}' 24 | goos: 25 | - linux 26 | - darwin 27 | - windows 28 | goarch: 29 | - amd64 30 | - arm64 31 | ldflags: 32 | - "{{ .Env.LDFLAGS }}" 33 | env: 34 | - CGO_ENABLED=0 35 | 36 | nfpms: 37 | - id: sbomasm 38 | package_name: sbomasm 39 | file_name_template: "{{ .ConventionalFileName }}" 40 | vendor: Interlynk 41 | homepage: https://interlynk.io 42 | maintainer: Interlynk Authors hello@interlynk.io 43 | builds: 44 | - binaries 45 | description: SBOM Assembler - A tool to edit SBOM or assemble multiple sboms into a single sbom. 46 | license: "Apache License 2.0" 47 | formats: 48 | - apk 49 | - deb 50 | - rpm 51 | contents: 52 | - src: /usr/bin/sbomasm-{{ .Os }}-{{ .Arch }} 53 | dst: /usr/bin/sbomasm 54 | type: "symlink" 55 | 56 | archives: 57 | - format: binary 58 | name_template: "{{ .Binary }}" 59 | allow_different_binary_count: true 60 | 61 | snapshot: 62 | name_template: SNAPSHOT-{{ .ShortCommit }} 63 | 64 | release: 65 | prerelease: allow 66 | draft: true 67 | 68 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | golang 1.21.5 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use buildx for multi-platform builds 2 | # Build stage 3 | FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS builder 4 | LABEL org.opencontainers.image.source="https://github.com/interlynk-io/sbomasm" 5 | 6 | RUN apk add --no-cache make git 7 | WORKDIR /app 8 | COPY go.mod go.sum ./ 9 | RUN go mod download 10 | COPY . . 11 | 12 | # Build for multiple architectures 13 | ARG TARGETOS TARGETARCH 14 | RUN make build && chmod +x ./build/sbomasm 15 | 16 | # Final stage 17 | FROM alpine:3.19 18 | LABEL org.opencontainers.image.source="https://github.com/interlynk-io/sbomasm" 19 | LABEL org.opencontainers.image.description="Assembler for your sboms" 20 | LABEL org.opencontainers.image.licenses=Apache-2.0 21 | 22 | # Copy our static executable 23 | COPY --from=builder /app/build/sbomasm /app/sbomasm 24 | 25 | # Disable version check 26 | ENV INTERLYNK_DISABLE_VERSION_CHECK=true 27 | 28 | ENTRYPOINT [ "/app/sbomasm" ] 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 Interlynk.io 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Interlynk.io 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | #inspired by https://github.com/pinterb/go-semver/blob/master/Makefile 18 | 19 | 20 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 21 | ifeq (,$(shell go env GOBIN)) 22 | GOBIN=$(shell go env GOPATH)/bin 23 | else 24 | GOBIN=$(shell go env GOBIN) 25 | endif 26 | 27 | GIT_VERSION ?= $(shell git describe --tags --always --dirty) 28 | GIT_HASH ?= $(shell git rev-parse HEAD) 29 | DATE_FMT = +'%Y-%m-%dT%H:%M:%SZ' 30 | SOURCE_DATE_EPOCH ?= $(shell git log -1 --pretty=%ct) 31 | ifdef SOURCE_DATE_EPOCH 32 | BUILD_DATE ?= $(shell date -u -d "@$(SOURCE_DATE_EPOCH)" "$(DATE_FMT)" 2>/dev/null || date -u -r "$(SOURCE_DATE_EPOCH)" "$(DATE_FMT)" 2>/dev/null || date -u "$(DATE_FMT)") 33 | else 34 | BUILD_DATE ?= $(shell date "$(DATE_FMT)") 35 | endif 36 | GIT_TREESTATE = "clean" 37 | DIFF = $(shell git diff --quiet >/dev/null 2>&1; if [ $$? -eq 1 ]; then echo "1"; fi) 38 | ifeq ($(DIFF), 1) 39 | GIT_TREESTATE = "dirty" 40 | endif 41 | 42 | PKG ?= sigs.k8s.io/release-utils/version 43 | LDFLAGS=-buildid= -X $(PKG).gitVersion=$(GIT_VERSION) \ 44 | -X $(PKG).gitCommit=$(GIT_HASH) \ 45 | -X $(PKG).gitTreeState=$(GIT_TREESTATE) \ 46 | -X $(PKG).buildDate=$(BUILD_DATE) 47 | 48 | 49 | BUILD_DIR = ./build 50 | 51 | .PHONY: dep 52 | dep: 53 | go mod vendor 54 | go mod tidy 55 | 56 | .PHONY: generate 57 | generate: 58 | go generate ./... 59 | 60 | .PHONY: test 61 | test: generate 62 | go test -cover -race ./... 63 | 64 | .PHONY: build 65 | build: 66 | CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/sbomasm main.go 67 | 68 | .PHONY: clean 69 | clean: 70 | \rm -rf $(BUILD_DIR) 71 | 72 | .PHONY: snapshot 73 | snapshot: 74 | LDFLAGS="$(LDFLAGS)" \goreleaser release --clean --snapshot --timeout 120m 75 | 76 | .PHONY: release 77 | release: 78 | LDFLAGS="$(LDFLAGS)" \goreleaser release --clean --timeout 120m 79 | 80 | .PHONY: updatedeps 81 | updatedeps: 82 | go get -u all 83 | -------------------------------------------------------------------------------- /cmd/assemble.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | package cmd 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "os" 22 | 23 | "github.com/interlynk-io/sbomasm/pkg/assemble" 24 | "github.com/interlynk-io/sbomasm/pkg/logger" 25 | "github.com/spf13/cobra" 26 | ) 27 | 28 | // assembleCmd represents the assemble command 29 | var assembleCmd = &cobra.Command{ 30 | Use: "assemble", 31 | Short: "helps assembling sboms into a final sbom", 32 | Long: `The assemble command will help assembling sboms into a final sbom. 33 | 34 | Basic Example: 35 | $ sbomasm assemble -n "mega-app" -v "1.0.0" -t "application" in-sbom1.json in-sbom2.json 36 | $ sbomasm assemble -n "mega-app" -v "1.0.0" -t "application" -f -o "mega_app_flat.sbom.json" in-sbom1.json in-sbom2.json 37 | 38 | Advanced Example: 39 | $ sbomasm generate > config.yaml (edit the config file to add your settings) 40 | $ sbomasm assemble -c config.yaml -o final_sbom_cdx.json in-sbom1.json in-sbom2.json 41 | `, 42 | SilenceUsage: true, 43 | RunE: func(cmd *cobra.Command, args []string) error { 44 | if len(args) == 0 { 45 | return fmt.Errorf("please provide at least one sbom file to assemble") 46 | } 47 | 48 | debug, _ := cmd.Flags().GetBool("debug") 49 | if debug { 50 | logger.InitDebugLogger() 51 | } else { 52 | logger.InitProdLogger() 53 | } 54 | 55 | ctx := logger.WithLogger(context.Background()) 56 | 57 | assembleParams, err := extractArgs(cmd, args) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | assembleParams.Ctx = &ctx 63 | 64 | // Populate the config object 65 | config, err := assemble.PopulateConfig(assembleParams) 66 | if err != nil { 67 | fmt.Println("Error populating config:", err) 68 | } 69 | return assemble.Assemble(config) 70 | }, 71 | } 72 | 73 | func init() { 74 | rootCmd.AddCommand(assembleCmd) 75 | assembleCmd.Flags().StringP("output", "o", "", "path to assembled sbom, defaults to stdout") 76 | assembleCmd.Flags().StringP("configPath", "c", "", "path to config file") 77 | 78 | assembleCmd.Flags().StringP("name", "n", "", "name of the assembled sbom") 79 | assembleCmd.Flags().StringP("version", "v", "", "version of the assembled sbom") 80 | assembleCmd.Flags().StringP("type", "t", "", "product type of the assembled sbom (application, framework, library, container, device, firmware)") 81 | assembleCmd.MarkFlagsRequiredTogether("name", "version", "type") 82 | 83 | assembleCmd.Flags().BoolP("flatMerge", "f", false, "flat merge") 84 | assembleCmd.Flags().BoolP("hierMerge", "m", false, "hierarchical merge") 85 | assembleCmd.Flags().BoolP("assemblyMerge", "a", false, "assembly merge") 86 | assembleCmd.MarkFlagsMutuallyExclusive("flatMerge", "hierMerge", "assemblyMerge") 87 | 88 | assembleCmd.Flags().BoolP("outputSpecCdx", "g", true, "output in cdx format") 89 | assembleCmd.Flags().BoolP("outputSpecSpdx", "s", false, "output in spdx format") 90 | assembleCmd.MarkFlagsMutuallyExclusive("outputSpecCdx", "outputSpecSpdx") 91 | 92 | assembleCmd.Flags().StringP("outputSpecVersion", "e", "", "spec version of the output sbom") 93 | 94 | assembleCmd.Flags().BoolP("xml", "x", false, "output in xml format") 95 | assembleCmd.Flags().BoolP("json", "j", true, "output in json format") 96 | assembleCmd.MarkFlagsMutuallyExclusive("xml", "json") 97 | } 98 | 99 | func validatePath(path string) error { 100 | stat, err := os.Stat(path) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | if stat.IsDir() { 106 | return fmt.Errorf("path %s is a directory include only files", path) 107 | } 108 | 109 | return nil 110 | } 111 | 112 | func extractArgs(cmd *cobra.Command, args []string) (*assemble.Params, error) { 113 | aParams := assemble.NewParams() 114 | 115 | configPath, err := cmd.Flags().GetString("configPath") 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | if configPath != "" { 121 | if err := validatePath(configPath); err != nil { 122 | return nil, err 123 | } 124 | aParams.ConfigPath = configPath 125 | } 126 | 127 | output, err := cmd.Flags().GetString("output") 128 | if err != nil { 129 | return nil, err 130 | } 131 | aParams.Output = output 132 | 133 | name, _ := cmd.Flags().GetString("name") 134 | version, _ := cmd.Flags().GetString("version") 135 | typeValue, _ := cmd.Flags().GetString("type") 136 | 137 | aParams.Name = name 138 | aParams.Version = version 139 | aParams.Type = typeValue 140 | 141 | flatMerge, _ := cmd.Flags().GetBool("flatMerge") 142 | hierMerge, _ := cmd.Flags().GetBool("hierMerge") 143 | assemblyMerge, _ := cmd.Flags().GetBool("assemblyMerge") 144 | 145 | aParams.FlatMerge = flatMerge 146 | aParams.HierMerge = hierMerge 147 | aParams.AssemblyMerge = assemblyMerge 148 | 149 | xml, _ := cmd.Flags().GetBool("xml") 150 | json, _ := cmd.Flags().GetBool("json") 151 | 152 | aParams.Xml = xml 153 | aParams.Json = json 154 | 155 | if aParams.Xml { 156 | aParams.Json = false 157 | } 158 | 159 | specVersion, _ := cmd.Flags().GetString("outputSpecVersion") 160 | aParams.OutputSpecVersion = specVersion 161 | 162 | cdx, _ := cmd.Flags().GetBool("outputSpecCdx") 163 | 164 | if cdx { 165 | aParams.OutputSpec = "cyclonedx" 166 | } else { 167 | aParams.OutputSpec = "spdx" 168 | } 169 | 170 | for _, arg := range args { 171 | if err := validatePath(arg); err != nil { 172 | return nil, err 173 | } 174 | aParams.Input = append(aParams.Input, arg) 175 | } 176 | return aParams, nil 177 | } 178 | -------------------------------------------------------------------------------- /cmd/dt.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | package cmd 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "os" 22 | 23 | "github.com/google/uuid" 24 | "github.com/interlynk-io/sbomasm/pkg/assemble" 25 | "github.com/interlynk-io/sbomasm/pkg/dt" 26 | "github.com/interlynk-io/sbomasm/pkg/logger" 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | // assembleCmd represents the assemble command 31 | var dtCmd = &cobra.Command{ 32 | Use: "dt", 33 | Short: "helps assembling multiple DT project sboms into a final sbom", 34 | Long: `The dt command will help assembling sboms into a final sbom. 35 | 36 | Basic Example: 37 | $ sbomasm dt -u "http://localhost:8080/" -k "odt_gwiwooi29i1N5Hewkkddkkeiwi3ii" -n "mega-app" -v "1.0.0" -t "application" -o finalsbom.json 11903ba9-a585-4dfb-9a0c-f348345a5473 34103ba2-rt63-2fga-3a8b-t625261g6262 38 | `, 39 | SilenceUsage: true, 40 | RunE: func(cmd *cobra.Command, args []string) error { 41 | if len(args) == 0 { 42 | return fmt.Errorf("please provide at least one sbom file to assemble") 43 | } 44 | 45 | debug, _ := cmd.Flags().GetBool("debug") 46 | if debug { 47 | logger.InitDebugLogger() 48 | } else { 49 | logger.InitProdLogger() 50 | } 51 | 52 | ctx := logger.WithLogger(context.Background()) 53 | 54 | dtParams, err := extractDtArgs(cmd, args) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | dtParams.Ctx = &ctx 60 | 61 | // retrieve Input Files 62 | dtParams.PopulateInputField(ctx) 63 | 64 | assembleParams, err := extractArgsFromDTtoAssemble(dtParams) 65 | if err != nil { 66 | return err 67 | } 68 | assembleParams.Ctx = &ctx 69 | 70 | config, err := assemble.PopulateConfig(assembleParams) 71 | if err != nil { 72 | fmt.Println("Error populating config:", err) 73 | } 74 | return assemble.Assemble(config) 75 | }, 76 | } 77 | 78 | func extractArgsFromDTtoAssemble(dtParams *dt.Params) (*assemble.Params, error) { 79 | aParams := assemble.NewParams() 80 | 81 | aParams.Output = dtParams.Output 82 | aParams.Upload = dtParams.Upload 83 | aParams.UploadProjectID = dtParams.UploadProjectID 84 | aParams.Url = dtParams.Url 85 | aParams.ApiKey = dtParams.ApiKey 86 | 87 | aParams.Name = dtParams.Name 88 | aParams.Version = dtParams.Version 89 | aParams.Type = dtParams.Type 90 | 91 | aParams.FlatMerge = dtParams.FlatMerge 92 | aParams.HierMerge = dtParams.HierMerge 93 | aParams.AssemblyMerge = dtParams.AssemblyMerge 94 | 95 | aParams.Xml = dtParams.Xml 96 | aParams.Json = dtParams.Json 97 | 98 | aParams.OutputSpecVersion = dtParams.OutputSpecVersion 99 | 100 | aParams.OutputSpec = dtParams.OutputSpec 101 | 102 | aParams.Input = dtParams.Input 103 | 104 | return aParams, nil 105 | } 106 | 107 | func extractDtArgs(cmd *cobra.Command, args []string) (*dt.Params, error) { 108 | aParams := dt.NewParams() 109 | 110 | url, err := cmd.Flags().GetString("url") 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | apiKey, err := cmd.Flags().GetString("api-key") 116 | if err != nil { 117 | return nil, err 118 | } 119 | aParams.Url = url 120 | aParams.ApiKey = apiKey 121 | 122 | name, _ := cmd.Flags().GetString("name") 123 | version, _ := cmd.Flags().GetString("version") 124 | typeValue, _ := cmd.Flags().GetString("type") 125 | 126 | aParams.Name = name 127 | aParams.Version = version 128 | aParams.Type = typeValue 129 | 130 | flatMerge, _ := cmd.Flags().GetBool("flatMerge") 131 | hierMerge, _ := cmd.Flags().GetBool("hierMerge") 132 | assemblyMerge, _ := cmd.Flags().GetBool("assemblyMerge") 133 | 134 | aParams.FlatMerge = flatMerge 135 | aParams.HierMerge = hierMerge 136 | aParams.AssemblyMerge = assemblyMerge 137 | 138 | xml, _ := cmd.Flags().GetBool("xml") 139 | json, _ := cmd.Flags().GetBool("json") 140 | 141 | aParams.Xml = xml 142 | aParams.Json = json 143 | 144 | if aParams.Xml { 145 | aParams.Json = false 146 | } 147 | 148 | specVersion, _ := cmd.Flags().GetString("outputSpecVersion") 149 | aParams.OutputSpecVersion = specVersion 150 | 151 | cdx, _ := cmd.Flags().GetBool("outputSpecCdx") 152 | 153 | if cdx { 154 | aParams.OutputSpec = "cyclonedx" 155 | } else { 156 | aParams.OutputSpec = "spdx" 157 | } 158 | 159 | output, err := cmd.Flags().GetString("output") 160 | if err != nil { 161 | return nil, err 162 | } 163 | // Check if the output is a valid UUID, i.e. project ID 164 | if _, err := uuid.Parse(output); err == nil { 165 | aParams.Upload = true 166 | aParams.UploadProjectID = uuid.MustParse(output) 167 | } else { 168 | // Assume it's a file path 169 | aParams.Output = output 170 | aParams.Upload = false 171 | } 172 | 173 | for _, arg := range args { 174 | // Check if the argument is a file 175 | if _, err := os.Stat(arg); err == nil { 176 | if err := validatePath(arg); err != nil { 177 | return nil, err 178 | } 179 | aParams.Input = append(aParams.Input, arg) 180 | continue 181 | } 182 | 183 | argID, err := uuid.Parse(arg) 184 | if err != nil { 185 | return nil, err 186 | } 187 | aParams.ProjectIds = append(aParams.ProjectIds, argID) 188 | } 189 | return aParams, nil 190 | } 191 | 192 | func init() { 193 | // Add dt as a sub-command of assemble 194 | assembleCmd.AddCommand(dtCmd) 195 | 196 | dtCmd.Flags().StringP("url", "u", "", "dependency track url https://localhost:8080/") 197 | dtCmd.Flags().StringP("api-key", "k", "", "dependency track api key, requires VIEW_PORTFOLIO for scoring and PORTFOLIO_MANAGEMENT for tagging") 198 | dtCmd.MarkFlagsRequiredTogether("url", "api-key") 199 | 200 | dtCmd.Flags().StringP("output", "o", "", "path to file or project id for newly assembled sbom, defaults to stdout") 201 | 202 | dtCmd.Flags().StringP("name", "n", "", "name of the assembled sbom") 203 | dtCmd.Flags().StringP("version", "v", "", "version of the assembled sbom") 204 | dtCmd.Flags().StringP("type", "t", "", "product type of the assembled sbom (application, framework, library, container, device, firmware)") 205 | dtCmd.MarkFlagsRequiredTogether("name", "version", "type") 206 | 207 | dtCmd.Flags().BoolP("flatMerge", "f", false, "flat merge") 208 | dtCmd.Flags().BoolP("hierMerge", "m", false, "hierarchical merge") 209 | dtCmd.Flags().BoolP("assemblyMerge", "a", false, "assembly merge") 210 | dtCmd.MarkFlagsMutuallyExclusive("flatMerge", "hierMerge", "assemblyMerge") 211 | 212 | dtCmd.Flags().BoolP("outputSpecCdx", "g", true, "output in cdx format") 213 | dtCmd.Flags().BoolP("outputSpecSpdx", "s", false, "output in spdx format") 214 | dtCmd.MarkFlagsMutuallyExclusive("outputSpecCdx", "outputSpecSpdx") 215 | 216 | dtCmd.Flags().StringP("outputSpecVersion", "e", "", "spec version of the output sbom") 217 | 218 | dtCmd.Flags().BoolP("xml", "x", false, "output in xml format") 219 | dtCmd.Flags().BoolP("json", "j", true, "output in json format") 220 | dtCmd.MarkFlagsMutuallyExclusive("xml", "json") 221 | } 222 | -------------------------------------------------------------------------------- /cmd/edit.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package cmd 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/interlynk-io/sbomasm/pkg/edit" 23 | "github.com/interlynk-io/sbomasm/pkg/logger" 24 | "github.com/spf13/cobra" 25 | ) 26 | 27 | // editCmd represents the edit command 28 | var editCmd = &cobra.Command{ 29 | Use: "edit", 30 | Short: "helps editing an sbom", 31 | Long: `The edit command allows you to modify an existing Software Bill of Materials (SBOM) by filling in gaps or adding information that may have been missed during the generation process. This command operates by first locating the entity to edit and then adding the required information. The goal of edit is not to provide a full editing experience but to help fill in filling in missing information useful for compliance and security purposes. 32 | 33 | Usage 34 | sbomasm edit [flags] 35 | 36 | Basic Example: 37 | # Edit's an sbom to add app-name and version to the primary component 38 | $ sbomasm edit --subject primary-component --name "my-cool-app" --version "1.0.0" in-sbom-2.json 39 | 40 | # Edit's an sbom with an exiting created-at timestamp and supplier information only for missing fields 41 | $ sbomasm edit --missing --subject document --timestamp --supplier "interlynk (support@interlynk.io)" in-sbom-1.json 42 | 43 | # Edit's an sbom add a new author to the primary component preserving the existing authors in the doc 44 | # if append is not provided the default behavior is to replace. 45 | $ sbomasm edit --append --subject primary-component --author "abc (abc@gmail.com)" in-sbom-2.json 46 | 47 | Advanced Example: 48 | # Edit's an sbom to add purl to a component by search it by name and version 49 | $ sbomasm edit --subject component-name-version --search "abc (v1.0.0)" --purl "pkg:deb/debian/abc@1.0.0" in-sbom-3.json 50 | 51 | # Edit's an sbom to add multiple authors to the document 52 | $ sbomasm edit --subject document --author "abc (abc@gmail.com)" --author "def (def@gmail.com)" in-sbom-4.json 53 | 54 | # Edit's an sbom to add multiple hashes to the primary component 55 | $ sbomasm edit --subject primary-component --hash "MD5 (hash1)" --hash "SHA256 (hash2)" in-sbom-5.json 56 | `, 57 | SilenceUsage: true, 58 | Args: cobra.ExactArgs(1), 59 | RunE: func(cmd *cobra.Command, args []string) error { 60 | debug, _ := cmd.Flags().GetBool("debug") 61 | if debug { 62 | logger.InitDebugLogger() 63 | } else { 64 | logger.InitProdLogger() 65 | } 66 | 67 | ctx := logger.WithLogger(context.Background()) 68 | 69 | editParams, err := extractEditArgs(cmd, args) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | editParams.Ctx = &ctx 75 | return edit.Edit(editParams) 76 | }, 77 | } 78 | 79 | func init() { 80 | rootCmd.AddCommand(editCmd) 81 | // Output controls 82 | editCmd.Flags().StringP("output", "o", "", "path to edited sbom, defaults to stdout") 83 | 84 | // Edit locations 85 | editCmd.Flags().String("subject", "document", "subject to edit (document, primary-component, component-name-version)") 86 | editCmd.MarkFlagRequired("subject") 87 | editCmd.Flags().String("search", "", "search string to find the entity") 88 | 89 | // Edit controls 90 | editCmd.Flags().BoolP("missing", "m", false, "edit only missing fields") 91 | editCmd.Flags().BoolP("append", "a", false, "append to field instead of replacing") 92 | 93 | // Edit fields 94 | editCmd.Flags().String("name", "", "name of the entity") 95 | editCmd.Flags().String("version", "", "version of the entity") 96 | editCmd.Flags().String("supplier", "", "supplier to add e.g 'name (email)'") 97 | editCmd.Flags().StringSlice("author", []string{}, "author to add e.g 'name (email)'") 98 | editCmd.Flags().String("purl", "", "purl to add e.g 'pkg:deb/debian/abc@1.0.0'") 99 | editCmd.Flags().String("cpe", "", "cpe to add e.g 'cpe:2.3:a:microsoft:internet_explorer:8.*:sp?:*:*:*:*:*:*'") 100 | editCmd.Flags().StringSlice("license", []string{}, "license to add e.g 'MIT'") 101 | editCmd.Flags().StringSlice("hash", []string{}, "checksum to add e.g 'MD5 (hash'") 102 | editCmd.Flags().StringSlice("tool", []string{}, "tool to add e.g 'sbomasm (v1.0.0)'") 103 | editCmd.Flags().String("copyright", "", "copyright to add e.g 'Copyright © 2024'") 104 | editCmd.Flags().StringSlice("lifecycle", []string{}, "lifecycle to add e.g 'build'") 105 | editCmd.Flags().String("description", "", "description to add e.g 'this is a cool app'") 106 | editCmd.Flags().String("repository", "", "repository to add e.g 'github.com/interlynk-io/sbomasm'") 107 | editCmd.Flags().String("type", "", "type to add e.g 'application'") 108 | 109 | editCmd.Flags().Bool("timestamp", false, "add created-at timestamp") 110 | } 111 | 112 | func extractEditArgs(cmd *cobra.Command, args []string) (*edit.EditParams, error) { 113 | editParams := edit.NewEditParams() 114 | 115 | editParams.Input = args[0] 116 | editParams.Output, _ = cmd.Flags().GetString("output") 117 | 118 | subject, _ := cmd.Flags().GetString("subject") 119 | editParams.Subject = subject 120 | 121 | search, _ := cmd.Flags().GetString("search") 122 | editParams.Search = search 123 | 124 | missing, _ := cmd.Flags().GetBool("missing") 125 | editParams.Missing = missing 126 | 127 | append, _ := cmd.Flags().GetBool("append") 128 | editParams.Append = append 129 | 130 | name, _ := cmd.Flags().GetString("name") 131 | editParams.Name = name 132 | 133 | version, _ := cmd.Flags().GetString("version") 134 | editParams.Version = version 135 | 136 | supplier, _ := cmd.Flags().GetString("supplier") 137 | editParams.Supplier = supplier 138 | 139 | authors, _ := cmd.Flags().GetStringSlice("author") 140 | editParams.Authors = authors 141 | 142 | purl, _ := cmd.Flags().GetString("purl") 143 | editParams.Purl = purl 144 | 145 | cpe, _ := cmd.Flags().GetString("cpe") 146 | editParams.Cpe = cpe 147 | 148 | licenses, _ := cmd.Flags().GetStringSlice("license") 149 | editParams.Licenses = licenses 150 | 151 | hashes, _ := cmd.Flags().GetStringSlice("hash") 152 | editParams.Hashes = hashes 153 | 154 | tools, _ := cmd.Flags().GetStringSlice("tool") 155 | editParams.Tools = tools 156 | 157 | copyright, _ := cmd.Flags().GetString("copyright") 158 | editParams.CopyRight = copyright 159 | 160 | lifecycles, _ := cmd.Flags().GetStringSlice("lifecycle") 161 | editParams.Lifecycles = lifecycles 162 | 163 | description, _ := cmd.Flags().GetString("description") 164 | editParams.Description = description 165 | 166 | repository, _ := cmd.Flags().GetString("repository") 167 | editParams.Repository = repository 168 | 169 | typ, _ := cmd.Flags().GetString("type") 170 | editParams.Type = typ 171 | 172 | timestamp, _ := cmd.Flags().GetBool("timestamp") 173 | editParams.Timestamp = timestamp 174 | 175 | return editParams, nil 176 | } 177 | -------------------------------------------------------------------------------- /cmd/generate.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | package cmd 17 | 18 | import ( 19 | "fmt" 20 | 21 | "github.com/interlynk-io/sbomasm/pkg/assemble" 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | // generateCmd represents the generate command 26 | var generateCmd = &cobra.Command{ 27 | Use: "generate", 28 | Short: "Generate a sample config file for assembling sboms", 29 | Long: `The generate command will generate a sample config file for assembling sboms. 30 | Example: 31 | $ sbomasm generate > config.yaml 32 | 33 | Please fill in all the fields that are known. Unknown fields can be left blank.`, 34 | Args: cobra.NoArgs, 35 | SilenceUsage: true, 36 | Run: func(cmd *cobra.Command, args []string) { 37 | fmt.Printf("%s", assemble.DefaultConfigYaml()) 38 | }, 39 | } 40 | 41 | func init() { 42 | rootCmd.AddCommand(generateCmd) 43 | } 44 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | package cmd 17 | 18 | import ( 19 | "os" 20 | 21 | "github.com/spf13/cobra" 22 | ) 23 | 24 | // rootCmd represents the base command when called without any subcommands 25 | var rootCmd = &cobra.Command{ 26 | Use: "sbomasm", 27 | Short: "sbomasm is your primary tool to assemble SBOMs, for easy management and distribution.", 28 | Long: `sbomasm is your primary tool to assemble SBOMs, for easy management and distribution. The tool 29 | can process both spdx and cyclonedx input sboms, it autotects the file formats for input sboms. The tool 30 | can output both spdx and cyclonedx sboms. Multiple algorithms are supported for assembling component sboms 31 | into a final sbom. 32 | `, 33 | // Uncomment the following line if your bare application 34 | // has an action associated with it: 35 | } 36 | 37 | // Execute adds all child commands to the root command and sets flags appropriately. 38 | // This is called by main.main(). It only needs to happen once to the rootCmd. 39 | func Execute() { 40 | err := rootCmd.Execute() 41 | if err != nil { 42 | os.Exit(1) 43 | } 44 | } 45 | 46 | func init() { 47 | // Here you will define your flags and configuration settings. 48 | // Cobra supports persistent flags, which, if defined here, 49 | // will be global for your application. 50 | 51 | // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.sbomasm.yaml)") 52 | 53 | // Cobra also supports local flags, which will only run 54 | // when this action is called directly. 55 | rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 56 | rootCmd.PersistentFlags().BoolP("debug", "d", false, "debug output") 57 | } 58 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package cmd 18 | 19 | import ( 20 | _ "embed" 21 | 22 | version "sigs.k8s.io/release-utils/version" 23 | ) 24 | 25 | func init() { 26 | rootCmd.AddCommand(version.Version()) 27 | } 28 | -------------------------------------------------------------------------------- /e2e/edit_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package e2e_edit_test 18 | 19 | import ( 20 | "io" 21 | "os" 22 | "path/filepath" 23 | "testing" 24 | 25 | "github.com/interlynk-io/sbomasm/cmd" 26 | 27 | "github.com/rogpeppe/go-internal/testscript" 28 | ) 29 | 30 | func TestSbomasmEdit(t *testing.T) { 31 | if testing.Short() { 32 | t.Skip("skipping test in short mode.") 33 | } 34 | 35 | t.Parallel() 36 | testscript.Run(t, testscript.Params{ 37 | Dir: "testdata/edit", 38 | RequireExplicitExec: true, 39 | Setup: func(env *testscript.Env) error { 40 | // copy required files to the workspace 41 | if err := copyFile("testdata/edit/photon-lite.spdx.json", filepath.Join(env.WorkDir, "photon-lite.spdx.json")); err != nil { 42 | return err 43 | } 44 | if err := copyFile("testdata/edit/expected-output-lite.spdx.json", filepath.Join(env.WorkDir, "expected-output-lite.spdx.json")); err != nil { 45 | return err 46 | } 47 | 48 | return nil 49 | }, 50 | }) 51 | } 52 | 53 | func runSbomasm() int { 54 | cmd.Execute() 55 | return 0 56 | } 57 | 58 | func TestMain(m *testing.M) { 59 | exitCode := testscript.RunMain(m, map[string]func() int{ 60 | "sbomasm": runSbomasm, 61 | }) 62 | os.Exit(exitCode) 63 | } 64 | 65 | // Helper function to copy files 66 | func copyFile(src, dst string) error { 67 | sourceFile, err := os.Open(src) 68 | if err != nil { 69 | return err 70 | } 71 | defer sourceFile.Close() 72 | 73 | destFile, err := os.Create(dst) 74 | if err != nil { 75 | return err 76 | } 77 | defer destFile.Close() 78 | 79 | _, err = io.Copy(destFile, sourceFile) 80 | return err 81 | } 82 | -------------------------------------------------------------------------------- /e2e/testdata/edit/edit_test.txt: -------------------------------------------------------------------------------- 1 | # Run the sbomasm edit command 2 | exec sbomasm edit --missing --subject document --tool 'trivy (0.56.1)' --tool 'parlay (0.5.1)' --tool 'bomctl (v0.4.1)' photon-lite.spdx.json --output photon-missing.spdx.json 3 | 4 | # Check that the output file exists 5 | exists photon-missing.spdx.json 6 | 7 | # Validate the output content matches the expected result 8 | cmp photon-missing.spdx.json expected-output-lite.spdx.json 9 | -------------------------------------------------------------------------------- /e2e/testdata/edit/expected-output-lite.spdx.json: -------------------------------------------------------------------------------- 1 | { 2 | "spdxVersion": "SPDX-2.3", 3 | "dataLicense": "CC0-1.0", 4 | "SPDXID": "SPDXRef-DOCUMENT", 5 | "name": "Tern report for photon", 6 | "documentNamespace": "https://spdx.org/spdxdocs/tern-report-b8e13d1780cd3a02204226bba3d0772d95da24a0-photon-21d2cd0a-064e-4198-8bf9-99882f2897aa", 7 | "comment": "This document was generated by the Tern Project: https://github.com/tern-tools/tern", 8 | "creationInfo": { 9 | "licenseListVersion": "3.19", 10 | "creators": [ 11 | "Tool: tern-b8e13d1780cd3a02204226bba3d0772d95da24a0", 12 | "Tool: trivy-0.56.1", 13 | "Tool: parlay-0.5.1", 14 | "Tool: bomctl-v0.4.1", 15 | "Tool: sbomasm-0.1.9" 16 | ], 17 | "created": "2023-01-12T22:06:03Z" 18 | }, 19 | "packages": [ 20 | { 21 | "name": "photon", 22 | "SPDXID": "SPDXRef-photon-3.0", 23 | "versionInfo": "3.0", 24 | "downloadLocation": "NOASSERTION", 25 | "filesAnalyzed": false, 26 | "packageVerificationCode": { 27 | "packageVerificationCodeValue": "" 28 | }, 29 | "licenseConcluded": "NOASSERTION", 30 | "licenseDeclared": "NOASSERTION", 31 | "copyrightText": "NOASSERTION" 32 | } 33 | ], 34 | "relationships": [ 35 | { 36 | "spdxElementId": "SPDXRef-DOCUMENT", 37 | "relatedSpdxElement": "SPDXRef-photon-3.0", 38 | "relationshipType": "DESCRIBES" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /e2e/testdata/edit/photon-lite.spdx.json: -------------------------------------------------------------------------------- 1 | { 2 | "SPDXID": "SPDXRef-DOCUMENT", 3 | "spdxVersion": "SPDX-2.2", 4 | "creationInfo": { 5 | "created": "2023-01-12T22:06:03Z", 6 | "creators": ["Tool: tern-b8e13d1780cd3a02204226bba3d0772d95da24a0"], 7 | "licenseListVersion": "3.19" 8 | }, 9 | "name": "Tern report for photon", 10 | "dataLicense": "CC0-1.0", 11 | "comment": "This document was generated by the Tern Project: https://github.com/tern-tools/tern", 12 | "documentNamespace": "https://spdx.org/spdxdocs/tern-report-b8e13d1780cd3a02204226bba3d0772d95da24a0-photon-21d2cd0a-064e-4198-8bf9-99882f2897aa", 13 | "documentDescribes": ["SPDXRef-photon-3.0"], 14 | "packages": [ 15 | { 16 | "name": "photon", 17 | "SPDXID": "SPDXRef-photon-3.0", 18 | "versionInfo": "3.0", 19 | "downloadLocation": "NOASSERTION", 20 | "filesAnalyzed": false, 21 | "licenseConcluded": "NOASSERTION", 22 | "licenseDeclared": "NOASSERTION", 23 | "copyrightText": "NOASSERTION" 24 | } 25 | ], 26 | "relationships": [ 27 | { 28 | "spdxElementId": "SPDXRef-DOCUMENT", 29 | "relatedSpdxElement": "SPDXRef-photon-3.0", 30 | "relationshipType": "DESCRIBES" 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/interlynk-io/sbomasm 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.1 6 | 7 | require ( 8 | github.com/CycloneDX/cyclonedx-go v0.9.2 9 | github.com/github/go-spdx/v2 v2.3.3 10 | github.com/google/uuid v1.6.0 11 | github.com/mitchellh/copystructure v1.2.0 12 | github.com/pingcap/log v1.1.0 13 | github.com/samber/lo v1.50.0 14 | github.com/spdx/tools-golang v0.5.5 15 | github.com/spf13/cobra v1.9.1 16 | go.uber.org/zap v1.27.0 17 | gopkg.in/yaml.v2 v2.4.0 18 | sigs.k8s.io/release-utils v0.11.1 19 | ) 20 | 21 | require golang.org/x/mod v0.24.0 // indirect 22 | 23 | require golang.org/x/tools v0.32.0 // indirect 24 | 25 | require ( 26 | github.com/DependencyTrack/client-go v0.16.0 27 | github.com/anchore/go-struct-converter v0.0.0-20250211213226-cce56d595160 // indirect 28 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect 29 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 30 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 31 | github.com/rogpeppe/go-internal v1.14.1 32 | github.com/spdx/gordf v0.0.0-20250128162952-000978ccd6fb // indirect 33 | github.com/spf13/pflag v1.0.6 // indirect 34 | go.uber.org/multierr v1.11.0 // indirect 35 | golang.org/x/sys v0.32.0 // indirect 36 | golang.org/x/text v0.24.0 // indirect 37 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect 38 | sigs.k8s.io/yaml v1.4.0 // indirect 39 | ) 40 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package version 16 | 17 | import "sigs.k8s.io/release-utils/version" 18 | 19 | var Version = version.GetVersionInfo().GitVersion 20 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | package main 17 | 18 | import "github.com/interlynk-io/sbomasm/cmd" 19 | 20 | func main() { 21 | cmd.Execute() 22 | } 23 | -------------------------------------------------------------------------------- /pkg/assemble/cdx/interface.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | package cdx 17 | 18 | import ( 19 | "context" 20 | "errors" 21 | "strings" 22 | 23 | cydx "github.com/CycloneDX/cyclonedx-go" 24 | "github.com/google/uuid" 25 | "github.com/samber/lo" 26 | ) 27 | 28 | var cdx_strings_to_types = map[string]cydx.ComponentType{ 29 | "application": cydx.ComponentTypeApplication, 30 | "container": cydx.ComponentTypeContainer, 31 | "device": cydx.ComponentTypeDevice, 32 | "file": cydx.ComponentTypeFile, 33 | "framework": cydx.ComponentTypeFramework, 34 | "library": cydx.ComponentTypeLibrary, 35 | "firmware": cydx.ComponentTypeFirmware, 36 | "operating-system": cydx.ComponentTypeOS, 37 | } 38 | 39 | var cdx_hash_algos = map[string]cydx.HashAlgorithm{ 40 | "MD5": cydx.HashAlgoMD5, 41 | "SHA-1": cydx.HashAlgoSHA1, 42 | "SHA-256": cydx.HashAlgoSHA256, 43 | "SHA-384": cydx.HashAlgoSHA384, 44 | "SHA-512": cydx.HashAlgoSHA512, 45 | "SHA3-256": cydx.HashAlgoSHA3_256, 46 | "SHA3-384": cydx.HashAlgoSHA3_384, 47 | "SHA3-512": cydx.HashAlgoSHA3_512, 48 | "BLAKE2b-256": cydx.HashAlgoBlake2b_256, 49 | "BLAKE2b-384": cydx.HashAlgoBlake2b_384, 50 | "BLAKE2b-512": cydx.HashAlgoBlake2b_512, 51 | "BLAKE3": cydx.HashAlgoBlake3, 52 | } 53 | 54 | func SupportedChecksums() []string { 55 | return lo.Keys(cdx_hash_algos) 56 | } 57 | 58 | func IsSupportedChecksum(algo, value string) bool { 59 | ualgo := strings.ToUpper(algo) 60 | if _, ok := cdx_hash_algos[ualgo]; ok { 61 | return value != "" 62 | } 63 | return false 64 | } 65 | 66 | type Author struct { 67 | Name string 68 | Email string 69 | Phone string 70 | } 71 | 72 | type License struct { 73 | Id string 74 | Expression string 75 | } 76 | 77 | type Supplier struct { 78 | Name string 79 | Email string 80 | } 81 | 82 | type Checksum struct { 83 | Algorithm string 84 | Value string 85 | } 86 | 87 | type app struct { 88 | Name string 89 | Version string 90 | Description string 91 | Authors []Author 92 | PrimaryPurpose string 93 | Purl string 94 | CPE string 95 | License License 96 | Supplier Supplier 97 | Checksums []Checksum 98 | Copyright string 99 | } 100 | 101 | type output struct { 102 | FileFormat string 103 | Spec string 104 | SpecVersion string 105 | File string 106 | Upload bool 107 | UploadProjectID uuid.UUID 108 | Url string 109 | ApiKey string 110 | } 111 | 112 | type input struct { 113 | Files []string 114 | } 115 | 116 | type assemble struct { 117 | IncludeDependencyGraph bool 118 | IncludeComponents bool 119 | IncludeDuplicateComponents bool 120 | FlatMerge bool 121 | HierarchicalMerge bool 122 | AssemblyMerge bool 123 | } 124 | 125 | type MergeSettings struct { 126 | Ctx *context.Context 127 | App app 128 | Output output 129 | Input input 130 | Assemble assemble 131 | } 132 | 133 | func Merge(ms *MergeSettings) error { 134 | if len(ms.Output.Spec) > 0 && ms.Output.Spec != "cyclonedx" { 135 | return errors.New("invalid output spec") 136 | } 137 | 138 | if len(ms.Output.SpecVersion) > 0 && !validSpecVersion(ms.Output.SpecVersion) { 139 | return errors.New("invalid CycloneDX spec version") 140 | } 141 | 142 | merger := newMerge(ms) 143 | return merger.combinedMerge() 144 | } 145 | -------------------------------------------------------------------------------- /pkg/assemble/cdx/merge.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package cdx 18 | 19 | import ( 20 | "encoding/base64" 21 | "io" 22 | "os" 23 | "strings" 24 | 25 | cydx "github.com/CycloneDX/cyclonedx-go" 26 | dtrack "github.com/DependencyTrack/client-go" 27 | "github.com/interlynk-io/sbomasm/pkg/logger" 28 | "github.com/samber/lo" 29 | ) 30 | 31 | type merge struct { 32 | settings *MergeSettings 33 | out *cydx.BOM 34 | in []*cydx.BOM 35 | } 36 | 37 | func newMerge(ms *MergeSettings) *merge { 38 | return &merge{ 39 | settings: ms, 40 | out: cydx.NewBOM(), 41 | in: []*cydx.BOM{}, 42 | } 43 | } 44 | 45 | func (m *merge) loadBoms() { 46 | for _, path := range m.settings.Input.Files { 47 | bom, err := loadBom(*m.settings.Ctx, path) 48 | if err != nil { 49 | panic(err) // TODO: return error instead of panic 50 | } 51 | m.in = append(m.in, bom) 52 | } 53 | } 54 | 55 | func (m *merge) combinedMerge() error { 56 | log := logger.FromContext(*m.settings.Ctx) 57 | 58 | log.Debug("loading sboms") 59 | m.loadBoms() 60 | 61 | log.Debugf("initialize component service") 62 | //cs := newComponentService(*m.settings.Ctx) 63 | cs := newUniqueComponentService(*m.settings.Ctx) 64 | 65 | // Build primary component list from each sbom 66 | priCompList := buildPrimaryComponentList(m.in, cs) 67 | log.Debugf("build primary component list for each sbom found %d", len(priCompList)) 68 | 69 | // Build a flat list of components from each sbom 70 | compList := buildComponentList(m.in, cs) 71 | log.Debugf("build a flat list of components from each sbom found %d", len(compList)) 72 | 73 | // Build a flat list of dependencies from each sbom 74 | depList := buildDependencyList(m.in, cs) 75 | log.Debugf("build a flat list of dependencies from each sbom found %d", len(depList)) 76 | 77 | // build a list of tools from each sbom 78 | toolsList := buildToolList(m.in) 79 | log.Debugf("build a list of tools from each sbom found comps: %d, service: %d", len(*toolsList.Components), len(*toolsList.Services)) 80 | 81 | //Build the final sbom 82 | log.Debugf("generating output sbom") 83 | m.initOutBom() 84 | 85 | log.Debugf("generating primary component") 86 | m.out.Metadata.Component = m.setupPrimaryComp() 87 | 88 | log.Debugf("assign tools to metadata") 89 | m.out.Metadata.Tools = toolsList 90 | 91 | if m.settings.Assemble.FlatMerge { 92 | finalCompList := []cydx.Component{} 93 | finalCompList = append(finalCompList, priCompList...) 94 | finalCompList = append(finalCompList, compList...) 95 | log.Debugf("flat merge: final component list: %d", len(finalCompList)) 96 | m.out.Components = &finalCompList 97 | 98 | priCompIds := lo.Map(priCompList, func(c cydx.Component, _ int) string { 99 | return c.BOMRef 100 | }) 101 | depList = append(depList, cydx.Dependency{ 102 | Ref: m.out.Metadata.Component.BOMRef, 103 | Dependencies: &priCompIds, 104 | }) 105 | log.Debugf("flat merge: final dependency list: %d", len(depList)) 106 | m.out.Dependencies = &depList 107 | } else if m.settings.Assemble.AssemblyMerge { 108 | // Add the sbom primary components to the new primary component 109 | m.out.Metadata.Component.Components = &priCompList 110 | m.out.Components = &compList 111 | m.out.Dependencies = &depList 112 | 113 | log.Debugf("assembly merge: final component list: %d", len(compList)) 114 | log.Debugf("assembly merge: final dependency list: %d", len(depList)) 115 | } else { 116 | for _, b := range m.in { 117 | var oldPc *cydx.Component 118 | var newPc int 119 | 120 | if b.Metadata != nil && b.Metadata.Component != nil { 121 | oldPc = b.Metadata.Component 122 | } 123 | 124 | if oldPc == nil { 125 | log.Error("hierarchical merge: old product does not have any component.") 126 | oldPc = &cydx.Component{} 127 | } 128 | 129 | newPcId, _ := cs.ResolveDepID(oldPc.BOMRef) 130 | 131 | for i, pc := range priCompList { 132 | if pc.BOMRef == newPcId { 133 | newPc = i 134 | break 135 | } 136 | } 137 | 138 | //Initialize the components list for the primary component 139 | priCompList[newPc].Components = &[]cydx.Component{} 140 | 141 | for _, oldComp := range lo.FromPtr(b.Components) { 142 | newCompId, _ := cs.ResolveDepID(oldComp.BOMRef) 143 | for _, comp := range compList { 144 | if comp.BOMRef == newCompId { 145 | *priCompList[newPc].Components = append(*priCompList[newPc].Components, comp) 146 | break 147 | } 148 | } 149 | } 150 | 151 | log.Debugf("hierarchical merge: primary component %s has %d components", priCompList[newPc].BOMRef, len(*priCompList[newPc].Components)) 152 | } 153 | 154 | m.out.Components = &priCompList 155 | 156 | priCompIds := lo.Map(priCompList, func(c cydx.Component, _ int) string { 157 | return c.BOMRef 158 | }) 159 | depList = append(depList, cydx.Dependency{ 160 | Ref: m.out.Metadata.Component.BOMRef, 161 | Dependencies: &priCompIds, 162 | }) 163 | m.out.Dependencies = &depList 164 | log.Debugf("hierarchical merge: final dependency list: %d", len(depList)) 165 | } 166 | 167 | // Writes sbom to file or uploads 168 | log.Debugf("writing sbom") 169 | return m.processSBOM() 170 | } 171 | 172 | func (m *merge) initOutBom() { 173 | //log := logger.FromContext(*m.settings.Ctx) 174 | m.out.SerialNumber = newSerialNumber() 175 | 176 | m.out.Metadata = &cydx.Metadata{} 177 | m.out.Metadata.Timestamp = utcNowTime() 178 | 179 | if m.settings.App.Supplier.Name != "" || m.settings.App.Supplier.Email != "" { 180 | m.out.Metadata.Supplier = &cydx.OrganizationalEntity{} 181 | m.out.Metadata.Supplier.Name = m.settings.App.Supplier.Name 182 | if m.settings.App.Supplier.Email != "" { 183 | m.out.Metadata.Supplier.Contact = &[]cydx.OrganizationalContact{ 184 | {Name: m.settings.App.Supplier.Name, Email: m.settings.App.Supplier.Email}, 185 | } 186 | } 187 | } 188 | 189 | // Always add data sharing license. 190 | m.out.Metadata.Licenses = &cydx.Licenses{ 191 | { 192 | License: &cydx.License{ID: "CC-BY-1.0"}, 193 | }, 194 | } 195 | 196 | if len(m.settings.App.Authors) > 0 { 197 | m.out.Metadata.Authors = &[]cydx.OrganizationalContact{} 198 | for _, author := range m.settings.App.Authors { 199 | *m.out.Metadata.Authors = append(*m.out.Metadata.Authors, cydx.OrganizationalContact{ 200 | Name: author.Name, 201 | Email: author.Email, 202 | }) 203 | } 204 | } 205 | } 206 | 207 | func (m *merge) setupPrimaryComp() *cydx.Component { 208 | pc := cydx.Component{} 209 | 210 | pc.Name = m.settings.App.Name 211 | pc.Version = m.settings.App.Version 212 | pc.Type = cdx_strings_to_types[m.settings.App.PrimaryPurpose] 213 | pc.PackageURL = m.settings.App.Purl 214 | pc.CPE = m.settings.App.CPE 215 | pc.Description = m.settings.App.Description 216 | 217 | if len(m.settings.App.Authors) > 0 { 218 | pc.Author = m.settings.App.Authors[0].Name 219 | } 220 | 221 | if m.settings.App.License.Id != "" { 222 | pc.Licenses = &cydx.Licenses{ 223 | {License: &cydx.License{ID: m.settings.App.License.Id}}, 224 | } 225 | } else if m.settings.App.License.Expression != "" { 226 | pc.Licenses = &cydx.Licenses{ 227 | {Expression: m.settings.App.License.Expression}, 228 | } 229 | } 230 | 231 | if len(m.settings.App.Checksums) > 0 { 232 | pc.Hashes = &[]cydx.Hash{} 233 | for _, c := range m.settings.App.Checksums { 234 | if len(c.Value) == 0 { 235 | continue 236 | } 237 | *pc.Hashes = append(*pc.Hashes, cydx.Hash{ 238 | Algorithm: cdx_hash_algos[c.Algorithm], 239 | Value: c.Value, 240 | }) 241 | } 242 | } 243 | 244 | if m.settings.App.Supplier.Name != "" || m.settings.App.Supplier.Email != "" { 245 | pc.Supplier = &cydx.OrganizationalEntity{} 246 | pc.Supplier.Name = m.settings.App.Supplier.Name 247 | if m.settings.App.Supplier.Email != "" { 248 | pc.Supplier.Contact = &[]cydx.OrganizationalContact{ 249 | {Name: m.settings.App.Supplier.Name, Email: m.settings.App.Supplier.Email}, 250 | } 251 | } 252 | } 253 | 254 | pc.BOMRef = newBomRef() 255 | return &pc 256 | } 257 | 258 | func (m *merge) processSBOM() error { 259 | var output io.Writer 260 | var sb strings.Builder 261 | 262 | log := logger.FromContext(*m.settings.Ctx) 263 | 264 | if m.settings.Output.Upload { 265 | output = &sb 266 | } else if m.settings.Output.File == "" { 267 | output = os.Stdout 268 | } else { 269 | f, err := os.Create(m.settings.Output.File) 270 | if err != nil { 271 | return err 272 | } 273 | defer f.Close() 274 | output = f 275 | } 276 | 277 | var encoder cydx.BOMEncoder 278 | switch m.settings.Output.FileFormat { 279 | case "xml": 280 | log.Debugf("writing sbom in xml format") 281 | encoder = cydx.NewBOMEncoder(output, cydx.BOMFileFormatXML) 282 | default: 283 | log.Debugf("writing sbom in json format") 284 | encoder = cydx.NewBOMEncoder(output, cydx.BOMFileFormatJSON) 285 | } 286 | 287 | encoder.SetPretty(true) 288 | encoder.SetEscapeHTML(true) 289 | 290 | var err error 291 | if m.settings.Output.SpecVersion == "" { 292 | err = encoder.Encode(m.out) 293 | } else { 294 | log.Debugf("writing sbom in version %s", m.settings.Output.SpecVersion) 295 | outputVersion := specVersionMap[m.settings.Output.SpecVersion] 296 | err = encoder.EncodeVersion(m.out, outputVersion) 297 | } 298 | 299 | if err != nil { 300 | return err 301 | } 302 | 303 | if m.settings.Output.Upload { 304 | return m.uploadToServer(sb.String()) 305 | } 306 | 307 | return nil 308 | } 309 | 310 | func (m *merge) uploadToServer(bomContent string) error { 311 | log := logger.FromContext(*m.settings.Ctx) 312 | 313 | log.Debugf("uploading sbom to %s", m.settings.Output.Url) 314 | 315 | dTrackClient, err := dtrack.NewClient(m.settings.Output.Url, 316 | dtrack.WithAPIKey(m.settings.Output.ApiKey), dtrack.WithDebug(false)) 317 | if err != nil { 318 | log.Fatalf("Failed to create Dependency-Track client: %s", err) 319 | return err 320 | } 321 | 322 | encodedBOM := base64.StdEncoding.EncodeToString([]byte(bomContent)) 323 | bomUploadRequest := dtrack.BOMUploadRequest{ 324 | ProjectUUID: &m.settings.Output.UploadProjectID, 325 | BOM: encodedBOM, 326 | } 327 | 328 | token, err := dTrackClient.BOM.Upload(*m.settings.Ctx, bomUploadRequest) 329 | if err != nil { 330 | log.Fatalf("Failed to upload BOM: %s", err) 331 | return err 332 | } 333 | 334 | log.Debugf("bom upload token: %v", token) 335 | return nil 336 | } 337 | -------------------------------------------------------------------------------- /pkg/assemble/cdx/uniq_comp_service.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | package cdx 19 | 20 | import ( 21 | "context" 22 | "fmt" 23 | "strings" 24 | 25 | cydx "github.com/CycloneDX/cyclonedx-go" 26 | ) 27 | 28 | type uniqueComponentService struct { 29 | ctx context.Context 30 | // unique list of new components 31 | compMap map[string]*cydx.Component 32 | 33 | // mapping from old component id to new component id 34 | idMap map[string]string 35 | } 36 | 37 | func newUniqueComponentService(ctx context.Context) *uniqueComponentService { 38 | return &uniqueComponentService{ 39 | ctx: ctx, 40 | compMap: make(map[string]*cydx.Component), 41 | idMap: make(map[string]string), 42 | } 43 | } 44 | 45 | func (s *uniqueComponentService) StoreAndCloneWithNewID(c *cydx.Component) (*cydx.Component, bool) { 46 | if c == nil { 47 | return nil, false 48 | } 49 | 50 | lookupKey := fmt.Sprintf("%s-%s-%s", 51 | strings.ToLower(string(c.Type)), 52 | strings.ToLower(c.Name), 53 | strings.ToLower(c.Version)) 54 | 55 | if foundComp, ok := s.compMap[lookupKey]; ok { 56 | if c.BOMRef != foundComp.BOMRef { 57 | s.idMap[c.BOMRef] = foundComp.BOMRef 58 | } 59 | return foundComp, true 60 | } 61 | 62 | nc, err := cloneComp(c) 63 | if err != nil { 64 | panic(err) 65 | } 66 | 67 | newID := newBomRef() 68 | nc.BOMRef = newID 69 | 70 | s.compMap[lookupKey] = nc 71 | s.idMap[c.BOMRef] = newID 72 | return nc, false 73 | } 74 | 75 | func (s *uniqueComponentService) ResolveDepID(depID string) (string, bool) { 76 | if newID, ok := s.idMap[depID]; ok { 77 | return newID, true 78 | } 79 | return "", false 80 | } 81 | 82 | func (s *uniqueComponentService) ResolveDepIDs(depIDs []string) []string { 83 | ids := make([]string, 0, len(depIDs)) 84 | for _, depID := range depIDs { 85 | if newID, ok := s.idMap[depID]; ok { 86 | ids = append(ids, newID) 87 | } 88 | } 89 | return ids 90 | } 91 | -------------------------------------------------------------------------------- /pkg/assemble/cdx/util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package cdx 18 | 19 | import ( 20 | "context" 21 | "encoding/json" 22 | "fmt" 23 | "os" 24 | "reflect" 25 | "time" 26 | 27 | cydx "github.com/CycloneDX/cyclonedx-go" 28 | "github.com/google/uuid" 29 | "github.com/interlynk-io/sbomasm/pkg/detect" 30 | "github.com/interlynk-io/sbomasm/pkg/logger" 31 | "github.com/samber/lo" 32 | "sigs.k8s.io/release-utils/version" 33 | ) 34 | 35 | var specVersionMap = map[string]cydx.SpecVersion{ 36 | "1.4": cydx.SpecVersion1_4, 37 | "1.5": cydx.SpecVersion1_5, 38 | "1.6": cydx.SpecVersion1_6, 39 | } 40 | 41 | func validSpecVersion(specVersion string) bool { 42 | _, ok := specVersionMap[specVersion] 43 | return ok 44 | } 45 | 46 | func newSerialNumber() string { 47 | u := uuid.New().String() 48 | 49 | return fmt.Sprintf("urn:uuid:%s", u) 50 | } 51 | 52 | func newBomRef() string { 53 | u := uuid.New().String() 54 | 55 | return fmt.Sprintf("lynk:%s", u) 56 | } 57 | 58 | func cloneComp(c *cydx.Component) (*cydx.Component, error) { 59 | var newComp cydx.Component 60 | 61 | // Marshal the original component to JSON 62 | b, err := json.Marshal(c) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | // Unmarshal into a map[string]interface{} to perform cleanup 68 | var tempMap map[string]interface{} 69 | if err := json.Unmarshal(b, &tempMap); err != nil { 70 | return nil, err 71 | } 72 | 73 | // Remove empty fields recursively 74 | cleanedUpMap := removeEmptyFields(tempMap) 75 | 76 | // Marshal the cleaned-up map back to JSON 77 | cleanedUpBytes, err := json.Marshal(cleanedUpMap) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | // Unmarshal the cleaned-up JSON back into a cydx.Component struct 83 | if err := json.Unmarshal(cleanedUpBytes, &newComp); err != nil { 84 | return nil, err 85 | } 86 | 87 | return &newComp, nil 88 | } 89 | 90 | func cloneService(s *cydx.Service) (*cydx.Service, error) { 91 | var newService cydx.Service 92 | b, err := json.Marshal(s) 93 | if err != nil { 94 | return nil, err 95 | } 96 | json.Unmarshal(b, &newService) 97 | return &newService, nil 98 | } 99 | 100 | // Recursive function to remove empty fields, including empty objects and arrays 101 | func removeEmptyFields(data interface{}) interface{} { 102 | switch v := data.(type) { 103 | case map[string]interface{}: 104 | // Loop through map and remove empty fields 105 | for key, value := range v { 106 | v[key] = removeEmptyFields(value) 107 | // Remove empty maps and slices 108 | if isEmptyValue(v[key]) { 109 | delete(v, key) 110 | } 111 | } 112 | case []interface{}: 113 | // Process arrays 114 | var newArray []interface{} 115 | for _, item := range v { 116 | item = removeEmptyFields(item) 117 | if !isEmptyValue(item) { 118 | newArray = append(newArray, item) 119 | } 120 | } 121 | return newArray 122 | } 123 | return data 124 | } 125 | 126 | // Helper function to determine if a value is considered "empty" 127 | func isEmptyValue(v interface{}) bool { 128 | if v == nil { 129 | return true 130 | } 131 | val := reflect.ValueOf(v) 132 | switch val.Kind() { 133 | case reflect.Array, reflect.Slice, reflect.Map: 134 | return val.Len() == 0 135 | case reflect.Struct: 136 | return reflect.DeepEqual(v, reflect.Zero(val.Type()).Interface()) 137 | case reflect.String: 138 | return v == "" 139 | } 140 | return false 141 | } 142 | 143 | func loadBom(ctx context.Context, path string) (*cydx.BOM, error) { 144 | log := logger.FromContext(ctx) 145 | 146 | var err error 147 | var bom *cydx.BOM 148 | 149 | f, err := os.Open(path) 150 | if err != nil { 151 | return nil, err 152 | } 153 | defer f.Close() 154 | 155 | spec, format, err := detect.Detect(f) 156 | if err != nil { 157 | return nil, err 158 | } 159 | 160 | log.Debugf("loading bom:%s spec:%s format:%s", path, spec, format) 161 | 162 | switch format { 163 | case detect.FileFormatJSON: 164 | bom = new(cydx.BOM) 165 | decoder := cydx.NewBOMDecoder(f, cydx.BOMFileFormatJSON) 166 | if err = decoder.Decode(bom); err != nil { 167 | return nil, err 168 | } 169 | case detect.FileFormatXML: 170 | bom = new(cydx.BOM) 171 | decoder := cydx.NewBOMDecoder(f, cydx.BOMFileFormatXML) 172 | if err = decoder.Decode(bom); err != nil { 173 | return nil, err 174 | } 175 | default: 176 | panic("unsupported file format") // TODO: return error instead of panic 177 | } 178 | 179 | return bom, nil 180 | } 181 | 182 | func utcNowTime() string { 183 | location, _ := time.LoadLocation("UTC") 184 | locationTime := time.Now().In(location) 185 | return locationTime.Format(time.RFC3339) 186 | } 187 | 188 | func buildToolList(in []*cydx.BOM) *cydx.ToolsChoice { 189 | tools := cydx.ToolsChoice{} 190 | 191 | tools.Services = &[]cydx.Service{} 192 | tools.Components = &[]cydx.Component{} 193 | 194 | *tools.Components = append(*tools.Components, cydx.Component{ 195 | Type: cydx.ComponentTypeApplication, 196 | Name: "sbomasm", 197 | Version: version.GetVersionInfo().GitVersion, 198 | Description: "Assembler & Editor for your sboms", 199 | Supplier: &cydx.OrganizationalEntity{ 200 | Name: "Interlynk", 201 | URL: &[]string{"https://interlynk.io"}, 202 | Contact: &[]cydx.OrganizationalContact{{Email: "support@interlynk.io"}}, 203 | }, 204 | Licenses: &cydx.Licenses{ 205 | { 206 | License: &cydx.License{ 207 | ID: "Apache-2.0", 208 | }, 209 | }, 210 | }, 211 | }) 212 | 213 | for _, bom := range in { 214 | if bom.Metadata != nil && bom.Metadata.Tools != nil && bom.Metadata.Tools.Tools != nil { 215 | for _, tool := range *bom.Metadata.Tools.Tools { 216 | *tools.Components = append(*tools.Components, cydx.Component{ 217 | Type: cydx.ComponentTypeApplication, 218 | Name: tool.Name, 219 | Version: tool.Version, 220 | Supplier: &cydx.OrganizationalEntity{ 221 | Name: tool.Vendor, 222 | }, 223 | }) 224 | } 225 | } 226 | 227 | if bom.Metadata != nil && bom.Metadata.Tools != nil && bom.Metadata.Tools.Components != nil { 228 | for _, tool := range *bom.Metadata.Tools.Components { 229 | comp, _ := cloneComp(&tool) 230 | *tools.Components = append(*tools.Components, *comp) 231 | } 232 | } 233 | 234 | if bom.Metadata != nil && bom.Metadata.Tools != nil && bom.Metadata.Tools.Services != nil { 235 | for _, service := range *bom.Metadata.Tools.Services { 236 | serv, _ := cloneService(&service) 237 | *tools.Services = append(*tools.Services, *serv) 238 | } 239 | } 240 | } 241 | 242 | uniqTools := lo.UniqBy(*tools.Components, func(c cydx.Component) string { 243 | return fmt.Sprintf("%s-%s", c.Name, c.Version) 244 | }) 245 | 246 | uniqServices := lo.UniqBy(*tools.Services, func(s cydx.Service) string { 247 | return fmt.Sprintf("%s-%s", s.Name, s.Version) 248 | }) 249 | 250 | tools.Components = &uniqTools 251 | tools.Services = &uniqServices 252 | 253 | return &tools 254 | } 255 | 256 | func buildComponentList(in []*cydx.BOM, cs *uniqueComponentService) []cydx.Component { 257 | finalList := []cydx.Component{} 258 | 259 | for _, bom := range in { 260 | for _, comp := range lo.FromPtr(bom.Components) { 261 | newComp, duplicate := cs.StoreAndCloneWithNewID(&comp) 262 | if !duplicate { 263 | finalList = append(finalList, *newComp) 264 | } 265 | } 266 | } 267 | return finalList 268 | } 269 | 270 | func buildPrimaryComponentList(in []*cydx.BOM, cs *uniqueComponentService) []cydx.Component { 271 | return lo.Map(in, func(bom *cydx.BOM, _ int) cydx.Component { 272 | if bom.Metadata != nil && bom.Metadata.Component != nil { 273 | newComp, duplicate := cs.StoreAndCloneWithNewID(bom.Metadata.Component) 274 | if !duplicate { 275 | return *newComp 276 | } 277 | } 278 | return cydx.Component{} 279 | }) 280 | } 281 | 282 | func buildDependencyList(in []*cydx.BOM, cs *uniqueComponentService) []cydx.Dependency { 283 | return lo.Flatten(lo.Map(in, func(bom *cydx.BOM, _ int) []cydx.Dependency { 284 | newDeps := []cydx.Dependency{} 285 | for _, dep := range lo.FromPtr(bom.Dependencies) { 286 | nd := cydx.Dependency{} 287 | ref, found := cs.ResolveDepID(dep.Ref) 288 | if !found { 289 | continue 290 | } 291 | 292 | if len(lo.FromPtr(dep.Dependencies)) == 0 { 293 | continue 294 | } 295 | 296 | deps := cs.ResolveDepIDs(lo.FromPtr(dep.Dependencies)) 297 | nd.Ref = ref 298 | nd.Dependencies = &deps 299 | newDeps = append(newDeps, nd) 300 | } 301 | return newDeps 302 | })) 303 | } 304 | -------------------------------------------------------------------------------- /pkg/assemble/combiner.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package assemble 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | 23 | "github.com/interlynk-io/sbomasm/pkg/assemble/cdx" 24 | "github.com/interlynk-io/sbomasm/pkg/assemble/spdx" 25 | "github.com/interlynk-io/sbomasm/pkg/logger" 26 | "github.com/samber/lo" 27 | ) 28 | 29 | type combiner struct { 30 | c *config 31 | finalSpec string 32 | } 33 | 34 | func newCombiner(c *config) *combiner { 35 | return &combiner{c: c} 36 | } 37 | 38 | func (c *combiner) combine() error { 39 | log := logger.FromContext(*c.c.ctx) 40 | 41 | if strings.EqualFold(c.finalSpec, "cyclonedx") { 42 | log.Debugf("combining %d CycloneDX sboms", len(c.c.input.files)) 43 | ms := toCDXMergerSettings(c.c) 44 | 45 | err := cdx.Merge(ms) 46 | if err != nil { 47 | return err 48 | } 49 | } 50 | 51 | if strings.EqualFold(c.finalSpec, "spdx") { 52 | log.Debugf("combining %d SPDX sboms", len(c.c.input.files)) 53 | 54 | ms := toSpdxMergerSettings(c.c) 55 | 56 | err := spdx.Merge(ms) 57 | if err != nil { 58 | return err 59 | } 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func (c *combiner) canCombine() error { 66 | specs := []string{} 67 | 68 | for _, doc := range c.c.input.files { 69 | spec, _, err := detectSbom(doc) 70 | if err != nil { 71 | return fmt.Errorf("unable to detect sbom format for %s: %v", doc, err) 72 | } 73 | specs = append(specs, spec) 74 | } 75 | 76 | // all input specs should be of the same type 77 | if len(lo.Uniq(specs)) != 1 { 78 | return fmt.Errorf("input sboms are not of the same type") 79 | } 80 | 81 | c.finalSpec = specs[0] 82 | 83 | return nil 84 | } 85 | 86 | func toCDXMergerSettings(c *config) *cdx.MergeSettings { 87 | ms := cdx.MergeSettings{} 88 | 89 | ms.Ctx = c.ctx 90 | 91 | ms.Assemble.FlatMerge = c.Assemble.FlatMerge 92 | ms.Assemble.HierarchicalMerge = c.Assemble.HierarchicalMerge 93 | ms.Assemble.AssemblyMerge = c.Assemble.AssemblyMerge 94 | ms.Assemble.IncludeComponents = c.Assemble.IncludeComponents 95 | ms.Assemble.IncludeDuplicateComponents = c.Assemble.includeDuplicateComponents 96 | ms.Assemble.IncludeDependencyGraph = c.Assemble.IncludeDependencyGraph 97 | 98 | ms.Input.Files = []string{} 99 | ms.Input.Files = append(ms.Input.Files, c.input.files...) 100 | 101 | ms.Output.File = c.Output.file 102 | ms.Output.Upload = c.Output.Upload 103 | ms.Output.UploadProjectID = c.Output.UploadProjectID 104 | ms.Output.Url = c.Output.Url 105 | ms.Output.ApiKey = c.Output.ApiKey 106 | ms.Output.FileFormat = c.Output.FileFormat 107 | ms.Output.Spec = c.Output.Spec 108 | ms.Output.SpecVersion = c.Output.SpecVersion 109 | 110 | ms.App.Name = c.App.Name 111 | ms.App.Version = c.App.Version 112 | ms.App.Description = c.App.Description 113 | ms.App.PrimaryPurpose = c.App.PrimaryPurpose 114 | ms.App.Purl = c.App.Purl 115 | ms.App.CPE = c.App.CPE 116 | ms.App.Copyright = c.App.Copyright 117 | ms.App.Supplier = cdx.Supplier{} 118 | ms.App.Supplier.Name = c.App.Supplier.Name 119 | ms.App.Supplier.Email = c.App.Supplier.Email 120 | 121 | ms.App.License = cdx.License{} 122 | ms.App.License.Id = c.App.License.Id 123 | ms.App.License.Expression = c.App.License.Expression 124 | 125 | ms.App.Authors = []cdx.Author{} 126 | for _, a := range c.App.Author { 127 | ms.App.Authors = append(ms.App.Authors, cdx.Author{ 128 | Name: a.Name, 129 | Email: a.Email, 130 | Phone: a.Phone, 131 | }) 132 | } 133 | 134 | ms.App.Checksums = []cdx.Checksum{} 135 | for _, c := range c.App.Checksums { 136 | ms.App.Checksums = append(ms.App.Checksums, cdx.Checksum{ 137 | Algorithm: c.Algorithm, 138 | Value: c.Value, 139 | }) 140 | } 141 | 142 | return &ms 143 | } 144 | 145 | func toSpdxMergerSettings(c *config) *spdx.MergeSettings { 146 | ms := spdx.MergeSettings{} 147 | 148 | ms.Ctx = c.ctx 149 | 150 | ms.Assemble.FlatMerge = c.Assemble.FlatMerge 151 | ms.Assemble.HierarchicalMerge = c.Assemble.HierarchicalMerge 152 | ms.Assemble.IncludeComponents = c.Assemble.IncludeComponents 153 | ms.Assemble.IncludeDuplicateComponents = c.Assemble.includeDuplicateComponents 154 | ms.Assemble.IncludeDependencyGraph = c.Assemble.IncludeDependencyGraph 155 | 156 | ms.Input.Files = []string{} 157 | ms.Input.Files = append(ms.Input.Files, c.input.files...) 158 | 159 | ms.Output.File = c.Output.file 160 | ms.Output.FileFormat = c.Output.FileFormat 161 | 162 | ms.App.Name = c.App.Name 163 | ms.App.Version = c.App.Version 164 | ms.App.Description = c.App.Description 165 | ms.App.PrimaryPurpose = c.App.PrimaryPurpose 166 | ms.App.Purl = c.App.Purl 167 | ms.App.CPE = c.App.CPE 168 | ms.App.Copyright = c.App.Copyright 169 | ms.App.Supplier = spdx.Supplier{} 170 | ms.App.Supplier.Name = c.App.Supplier.Name 171 | ms.App.Supplier.Email = c.App.Supplier.Email 172 | 173 | ms.App.License = spdx.License{} 174 | ms.App.License.Id = c.App.License.Id 175 | ms.App.License.Expression = c.App.License.Expression 176 | 177 | ms.App.Authors = []spdx.Author{} 178 | for _, a := range c.App.Author { 179 | ms.App.Authors = append(ms.App.Authors, spdx.Author{ 180 | Name: a.Name, 181 | Email: a.Email, 182 | Phone: a.Phone, 183 | }) 184 | } 185 | 186 | ms.App.Checksums = []spdx.Checksum{} 187 | for _, c := range c.App.Checksums { 188 | ms.App.Checksums = append(ms.App.Checksums, spdx.Checksum{ 189 | Algorithm: c.Algorithm, 190 | Value: c.Value, 191 | }) 192 | } 193 | 194 | return &ms 195 | } 196 | -------------------------------------------------------------------------------- /pkg/assemble/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package assemble 18 | 19 | import ( 20 | "context" 21 | "crypto/sha256" 22 | "errors" 23 | "fmt" 24 | "io" 25 | "log" 26 | "os" 27 | "strings" 28 | 29 | "github.com/google/uuid" 30 | "github.com/interlynk-io/sbomasm/pkg/assemble/cdx" 31 | "github.com/interlynk-io/sbomasm/pkg/logger" 32 | "github.com/samber/lo" 33 | "gopkg.in/yaml.v2" 34 | ) 35 | 36 | const ( 37 | DEFAULT_OUTPUT_SPEC = "cyclonedx" 38 | DEFAULT_OUTPUT_SPEC_VERSION = "1.6" 39 | DEFAULT_OUTPUT_FILE_FORMAT = "json" 40 | DEFAULT_OUTPUT_LICENSE = "CC0-1.0" 41 | ) 42 | 43 | type author struct { 44 | Name string `yaml:"name"` 45 | Email string `yaml:"email,omitempty"` 46 | Phone string `yaml:"phone,omitempty"` 47 | } 48 | 49 | type license struct { 50 | Id string `yaml:"id"` 51 | Expression string `yaml:"expression,omitempty"` 52 | } 53 | 54 | type supplier struct { 55 | Name string `yaml:"name"` 56 | Email string `yaml:"email,omitempty"` 57 | } 58 | 59 | type checksum struct { 60 | Algorithm string `yaml:"algorithm"` 61 | Value string `yaml:"value"` 62 | } 63 | 64 | type app struct { 65 | Name string `yaml:"name"` 66 | Version string `yaml:"version"` 67 | Description string `yaml:"description,omitempty"` 68 | Author []author `yaml:"author,omitempty"` 69 | PrimaryPurpose string `yaml:"primary_purpose,omitempty"` 70 | Purl string `yaml:"purl,omitempty"` 71 | CPE string `yaml:"cpe,omitempty"` 72 | License license `yaml:"license,omitempty"` 73 | Supplier supplier `yaml:"supplier,omitempty"` 74 | Checksums []checksum `yaml:"checksum,omitempty"` 75 | Copyright string `yaml:"copyright,omitempty"` 76 | } 77 | 78 | type output struct { 79 | Spec string `yaml:"spec"` 80 | SpecVersion string `yaml:"spec_version"` 81 | FileFormat string `yaml:"file_format"` 82 | file string 83 | Upload bool 84 | UploadProjectID uuid.UUID 85 | Url string 86 | ApiKey string 87 | } 88 | 89 | type input struct { 90 | files []string 91 | } 92 | 93 | type assemble struct { 94 | IncludeDependencyGraph bool `yaml:"include_dependency_graph"` 95 | IncludeComponents bool `yaml:"include_components"` 96 | includeDuplicateComponents bool 97 | FlatMerge bool `yaml:"flat_merge"` 98 | HierarchicalMerge bool `yaml:"hierarchical_merge"` 99 | AssemblyMerge bool `yaml:"assembly_merge"` 100 | } 101 | 102 | type config struct { 103 | ctx *context.Context 104 | App app `yaml:"app"` 105 | Output output `yaml:"output"` 106 | input input 107 | Assemble assemble `yaml:"assemble"` 108 | } 109 | 110 | var defaultConfig = config{ 111 | App: app{ 112 | Name: "[REQUIRED]", 113 | Version: "[REQUIRED]", 114 | Description: "[OPTIONAL]", 115 | PrimaryPurpose: "[REQUIRED]", 116 | Purl: "[OPTIONAL]", 117 | CPE: "[OPTIONAL]", 118 | License: license{ 119 | Id: "[OPTIONAL]", 120 | }, 121 | Supplier: supplier{ 122 | Name: "[OPTIONAL]", 123 | Email: "[OPTIONAL]", 124 | }, 125 | Checksums: []checksum{ 126 | {Algorithm: "[OPTIONAL]", Value: "[OPTIONAL]"}, 127 | }, 128 | Author: []author{ 129 | {Name: "[OPTIONAL]", Email: "[OPTIONAL]"}, 130 | }, 131 | Copyright: "[OPTIONAL]", 132 | }, 133 | Output: output{ 134 | Spec: DEFAULT_OUTPUT_SPEC, 135 | SpecVersion: DEFAULT_OUTPUT_SPEC_VERSION, 136 | FileFormat: DEFAULT_OUTPUT_FILE_FORMAT, 137 | }, 138 | Assemble: assemble{ 139 | FlatMerge: false, 140 | HierarchicalMerge: true, 141 | AssemblyMerge: false, 142 | IncludeComponents: true, 143 | IncludeDependencyGraph: true, 144 | includeDuplicateComponents: true, 145 | }, 146 | } 147 | 148 | // DefaultConfigYaml: Creates a yaml output of the default config. 149 | func DefaultConfigYaml() []byte { 150 | yamlBytes, err := yaml.Marshal(&defaultConfig) 151 | if err != nil { 152 | log.Fatal(err) 153 | } 154 | 155 | return yamlBytes 156 | } 157 | 158 | // NewConfig: Creating a new configuration instance with default values. 159 | func NewConfig() *config { 160 | return &config{ 161 | Output: output{ 162 | Spec: DEFAULT_OUTPUT_SPEC, 163 | SpecVersion: DEFAULT_OUTPUT_SPEC_VERSION, 164 | FileFormat: DEFAULT_OUTPUT_FILE_FORMAT, 165 | }, 166 | Assemble: assemble{ 167 | FlatMerge: false, 168 | HierarchicalMerge: true, 169 | IncludeComponents: true, 170 | IncludeDependencyGraph: true, 171 | includeDuplicateComponents: true, 172 | }, 173 | } 174 | } 175 | 176 | // Function to populate the config object 177 | func PopulateConfig(aParams *Params) (*config, error) { 178 | if aParams.Ctx == nil { 179 | return nil, errors.New("context is not initialized") 180 | } 181 | config := NewConfig() 182 | if err := config.readAndMerge(aParams); err != nil { 183 | return nil, err 184 | } 185 | if err := config.validate(); err != nil { 186 | return nil, err 187 | } 188 | return config, nil 189 | } 190 | 191 | // readAndMerge: Merging user-specified parameters into the configuration. 192 | func (c *config) readAndMerge(p *Params) error { 193 | if p.ConfigPath != "" { 194 | 195 | yF, err := os.ReadFile(p.ConfigPath) 196 | if err != nil { 197 | return err 198 | } 199 | 200 | err = yaml.Unmarshal(yF, &c) 201 | if err != nil { 202 | return err 203 | } 204 | } else { 205 | 206 | c.Assemble.FlatMerge = p.FlatMerge 207 | c.Assemble.HierarchicalMerge = p.HierMerge 208 | c.Assemble.AssemblyMerge = p.AssemblyMerge 209 | } 210 | 211 | c.input.files = p.Input 212 | c.Output.file = p.Output 213 | c.Output.Upload = p.Upload 214 | c.Output.UploadProjectID = p.UploadProjectID 215 | c.Output.Url = p.Url 216 | c.Output.ApiKey = p.ApiKey 217 | c.ctx = p.Ctx 218 | if c.ctx == nil { 219 | return errors.New("config context is not initialized") 220 | } 221 | 222 | // override default config with params 223 | if p.Name != "" { 224 | c.App.Name = strings.Trim(p.Name, " ") 225 | } 226 | 227 | if p.Version != "" { 228 | c.App.Version = strings.Trim(p.Version, " ") 229 | } 230 | 231 | if p.Type != "" { 232 | c.App.PrimaryPurpose = strings.Trim(p.Type, " ") 233 | } 234 | 235 | if p.Xml { 236 | c.Output.FileFormat = "xml" 237 | } 238 | 239 | if p.OutputSpec != "" { 240 | c.Output.Spec = strings.Trim(p.OutputSpec, " ") 241 | } 242 | 243 | if p.OutputSpecVersion != "" { 244 | c.Output.SpecVersion = strings.Trim(p.OutputSpecVersion, " ") 245 | } 246 | 247 | return nil 248 | } 249 | 250 | // Validation: Ensuring the configuration is valid before proceeding. 251 | func (c *config) validate() error { 252 | if c == nil { 253 | return fmt.Errorf("config is not set") 254 | } 255 | 256 | log := logger.FromContext(*c.ctx) 257 | if log == nil { 258 | return errors.New("logger is not initialized") 259 | } 260 | 261 | validValue := func(v string) bool { 262 | vl := strings.ToLower(v) 263 | if vl == "" || vl == "[required]" { 264 | return false 265 | } 266 | return true 267 | } 268 | 269 | sanitize := func(v string) string { 270 | if strings.ToLower(v) == "[optional]" { 271 | return "" 272 | } 273 | 274 | return strings.Trim(v, " ") 275 | } 276 | 277 | if !validValue(c.App.Name) { 278 | return fmt.Errorf("app name is not set") 279 | } 280 | c.App.Name = sanitize(c.App.Name) 281 | 282 | if !validValue(c.App.Version) { 283 | return fmt.Errorf("app version is not set") 284 | } 285 | c.App.Version = sanitize(c.App.Version) 286 | 287 | c.App.PrimaryPurpose = sanitize(c.App.PrimaryPurpose) 288 | c.App.Description = sanitize(c.App.Description) 289 | c.App.License.Id = sanitize(c.App.License.Id) 290 | c.App.Supplier.Name = sanitize(c.App.Supplier.Name) 291 | c.App.Supplier.Email = sanitize(c.App.Supplier.Email) 292 | c.App.Purl = sanitize(c.App.Purl) 293 | c.App.CPE = sanitize(c.App.CPE) 294 | c.App.Copyright = sanitize(c.App.Copyright) 295 | c.Output.Spec = sanitize(c.Output.Spec) 296 | c.Output.SpecVersion = sanitize(c.Output.SpecVersion) 297 | c.Output.FileFormat = sanitize(c.Output.FileFormat) 298 | 299 | for i := range c.App.Author { 300 | c.App.Author[i].Name = sanitize(c.App.Author[i].Name) 301 | c.App.Author[i].Email = sanitize(c.App.Author[i].Email) 302 | } 303 | 304 | for i := range c.App.Checksums { 305 | sAlgo := sanitize(c.App.Checksums[i].Algorithm) 306 | sValue := sanitize(c.App.Checksums[i].Value) 307 | 308 | if sAlgo == "" && sValue == "" { 309 | c.App.Checksums[i].Algorithm = sAlgo 310 | c.App.Checksums[i].Value = sValue 311 | continue 312 | } 313 | 314 | ok := cdx.IsSupportedChecksum(sAlgo, sValue) 315 | if ok { 316 | c.App.Checksums[i].Algorithm = strings.ToUpper(sAlgo) 317 | c.App.Checksums[i].Value = sValue 318 | } else { 319 | return fmt.Errorf("unsupported hash algorithm %s or value %x :: use one of these %+v", sAlgo, sValue, cdx.SupportedChecksums()) 320 | } 321 | } 322 | 323 | if c.Output.Spec == "" && c.Output.SpecVersion == "" { 324 | c.Output.Spec = "" 325 | c.Output.SpecVersion = "" 326 | } 327 | 328 | if c.Output.FileFormat == "" { 329 | c.Output.FileFormat = DEFAULT_OUTPUT_FILE_FORMAT 330 | } 331 | 332 | if c.input.files == nil || len(c.input.files) == 0 { 333 | return fmt.Errorf("input files are not set") 334 | } 335 | 336 | if len(c.input.files) <= 1 { 337 | return fmt.Errorf("assembly requires more than one sbom file") 338 | } 339 | 340 | err := c.validateInputContent() 341 | if err != nil { 342 | return err 343 | } 344 | 345 | log.Debugf("config %+v", c) 346 | 347 | return nil 348 | } 349 | 350 | func (c *config) validateInputContent() error { 351 | log := logger.FromContext(*c.ctx) 352 | sha256 := func(path string) string { 353 | f, err := os.Open(path) 354 | if err != nil { 355 | log.Fatal(err) 356 | } 357 | defer f.Close() 358 | h := sha256.New() 359 | if _, err := io.Copy(h, f); err != nil { 360 | log.Fatal(err) 361 | } 362 | return string(h.Sum(nil)) 363 | } 364 | 365 | sums := []string{} 366 | 367 | for _, v := range c.input.files { 368 | sum := sha256(v) 369 | log.Debugf("sha256 %s : %x", v, sum) 370 | sums = append(sums, sum) 371 | } 372 | 373 | uniqSums := lo.Uniq(sums) 374 | 375 | if len(sums) != len(uniqSums) { 376 | return fmt.Errorf("input sboms contain duplicate content %+v", c.input.files) 377 | } 378 | 379 | return nil 380 | } 381 | -------------------------------------------------------------------------------- /pkg/assemble/interface.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package assemble 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/google/uuid" 23 | ) 24 | 25 | type Params struct { 26 | Ctx *context.Context 27 | Input []string 28 | Output string 29 | ConfigPath string 30 | 31 | // upload requirement 32 | Url string 33 | ApiKey string 34 | Upload bool 35 | UploadProjectID uuid.UUID 36 | 37 | Name string 38 | Version string 39 | Type string 40 | 41 | FlatMerge bool 42 | HierMerge bool 43 | AssemblyMerge bool 44 | 45 | Xml bool 46 | Json bool 47 | 48 | OutputSpec string 49 | OutputSpecVersion string 50 | } 51 | 52 | func NewParams() *Params { 53 | return &Params{} 54 | } 55 | 56 | func Assemble(config *config) error { 57 | err := config.validate() 58 | if err != nil { 59 | return err 60 | } 61 | 62 | cb := newCombiner(config) 63 | 64 | err = cb.canCombine() 65 | if err != nil { 66 | return err 67 | } 68 | 69 | err = cb.combine() 70 | if err != nil { 71 | return err 72 | } 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /pkg/assemble/spdx/interface.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package spdx 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | 23 | "github.com/spdx/tools-golang/spdx" 24 | ) 25 | 26 | var spdx_hash_algos = map[string]spdx.ChecksumAlgorithm{ 27 | "MD5": spdx.MD5, 28 | "SHA-1": spdx.SHA1, 29 | "SHA-256": spdx.SHA256, 30 | "SHA-384": spdx.SHA384, 31 | "SHA-512": spdx.SHA512, 32 | "SHA3-256": spdx.SHA256, 33 | "SHA3-384": spdx.SHA384, 34 | "SHA3-512": spdx.SHA512, 35 | "BLAKE2b-256": spdx.BLAKE2b_256, 36 | "BLAKE2b-384": spdx.BLAKE2b_384, 37 | "BLAKE2b-512": spdx.BLAKE2b_512, 38 | "BLAKE3": spdx.BLAKE3, 39 | } 40 | 41 | var spdx_strings_to_types = map[string]string{ 42 | "application": "APPLICATION", 43 | "framework": "FRAMEWORK", 44 | "library": "LIBRARY", 45 | "container": "CONTAINER", 46 | "operating-system": "OPERATING-SYSTEM", 47 | "device": "DEVICE", 48 | "firmware": "FIRMWARE", 49 | "source": "SOURCE", 50 | "archive": "ARCHIVE", 51 | "file": "FILE", 52 | "install": "INSTALL", 53 | "other": "OTHER", 54 | } 55 | 56 | type Author struct { 57 | Name string 58 | Email string 59 | Phone string 60 | } 61 | 62 | type License struct { 63 | Id string 64 | Expression string 65 | } 66 | 67 | type Supplier struct { 68 | Name string 69 | Email string 70 | } 71 | 72 | type Checksum struct { 73 | Algorithm string 74 | Value string 75 | } 76 | 77 | type app struct { 78 | Name string 79 | Version string 80 | Description string 81 | Authors []Author 82 | PrimaryPurpose string 83 | Purl string 84 | CPE string 85 | License License 86 | Supplier Supplier 87 | Checksums []Checksum 88 | Copyright string 89 | } 90 | 91 | type output struct { 92 | FileFormat string 93 | Spec string 94 | SpecVersion string 95 | File string 96 | } 97 | 98 | type input struct { 99 | Files []string 100 | } 101 | 102 | type assemble struct { 103 | IncludeDependencyGraph bool 104 | IncludeComponents bool 105 | IncludeDuplicateComponents bool 106 | FlatMerge bool 107 | HierarchicalMerge bool 108 | AssemblyMerge bool 109 | } 110 | 111 | type MergeSettings struct { 112 | Ctx *context.Context 113 | App app 114 | Output output 115 | Input input 116 | Assemble assemble 117 | } 118 | 119 | func Merge(ms *MergeSettings) error { 120 | 121 | if len(ms.Output.Spec) > 0 && ms.Output.Spec != "spdx" { 122 | return errors.New("invalid output spec") 123 | } 124 | 125 | if len(ms.Output.SpecVersion) > 0 && !validSpecVersion(ms.Output.SpecVersion) { 126 | return errors.New("invalid CycloneDX spec version") 127 | } 128 | 129 | merger := newMerge(ms) 130 | merger.loadBoms() 131 | return merger.combinedMerge() 132 | } 133 | -------------------------------------------------------------------------------- /pkg/assemble/spdx/merge.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package spdx 18 | 19 | import ( 20 | "github.com/google/uuid" 21 | "github.com/interlynk-io/sbomasm/pkg/logger" 22 | "github.com/spdx/tools-golang/spdx" 23 | "github.com/spdx/tools-golang/spdx/v2/common" 24 | ) 25 | 26 | type merge struct { 27 | settings *MergeSettings 28 | out *spdx.Document 29 | in []*spdx.Document 30 | rootPackageID string 31 | } 32 | 33 | func newMerge(ms *MergeSettings) *merge { 34 | return &merge{ 35 | settings: ms, 36 | in: []*spdx.Document{}, 37 | out: &spdx.Document{}, 38 | rootPackageID: uuid.New().String(), 39 | } 40 | } 41 | 42 | func (m *merge) loadBoms() { 43 | for _, path := range m.settings.Input.Files { 44 | bom, err := loadBom(*m.settings.Ctx, path) 45 | if err != nil { 46 | panic(err) // TODO: return error instead of panic 47 | } 48 | m.in = append(m.in, bom) 49 | } 50 | } 51 | 52 | func (m *merge) combinedMerge() error { 53 | log := logger.FromContext(*m.settings.Ctx) 54 | log.Debugf("starting merge with settings: %v", m.settings) 55 | 56 | doc, err := genSpdxDocument(m) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | log.Debugf("generated document: %s, with ID %s", doc.DocumentName, doc.SPDXIdentifier) 62 | 63 | ci, err := genCreationInfo(m) 64 | if err != nil { 65 | return err 66 | } 67 | doc.CreationInfo = ci 68 | 69 | log.Debugf("generated creation with %d creators, created_at %s and license version %s", len(ci.Creators), ci.Created, ci.LicenseListVersion) 70 | 71 | doc.ExternalDocumentReferences = append(doc.ExternalDocumentReferences, externalDocumentRefs(m.in)...) 72 | 73 | log.Debugf("added %d external document references", len(doc.ExternalDocumentReferences)) 74 | 75 | primaryPkg, err := genPrimaryPackage(m) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | log.Debugf("generated primary package: %s, version: %s", primaryPkg.PackageName, primaryPkg.PackageVersion) 81 | 82 | pkgs, pkgMapper, err := genPackageList(m) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | files, fileMapper, err := genFileList(m) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | rels, err := genRelationships(m, pkgMapper, fileMapper) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | otherLicenses := genOtherLicenses(m.in) 98 | 99 | describedPkgs := getDescribedPkgs(m) 100 | 101 | // Add Packages to document 102 | doc.Packages = append(doc.Packages, primaryPkg) 103 | doc.Packages = append(doc.Packages, pkgs...) 104 | 105 | // Add Files to document 106 | doc.Files = append(doc.Files, files...) 107 | 108 | // Add OtherLicenses to document 109 | doc.OtherLicenses = append(doc.OtherLicenses, otherLicenses...) 110 | 111 | topLevelRels := []*spdx.Relationship{} 112 | 113 | // always add describes relationship between document and primary package 114 | topLevelRels = append(topLevelRels, &spdx.Relationship{ 115 | RefA: common.MakeDocElementID("", "DOCUMENT"), 116 | RefB: common.MakeDocElementID("", string(primaryPkg.PackageSPDXIdentifier)), 117 | Relationship: common.TypeRelationshipDescribe, 118 | RelationshipComment: "sbomasm created primary component relationship", 119 | }) 120 | 121 | if m.settings.Assemble.FlatMerge { 122 | log.Debugf("flat merge is applied") 123 | // we skip the contains relationship and remove all relationships except describes 124 | rels = []*spdx.Relationship{} 125 | } else if m.settings.Assemble.AssemblyMerge { 126 | log.Debugf("assembly merge is applied") 127 | // we retain all relationships but we will not add a contains relationship 128 | } else { 129 | log.Debugf("hierarchical merge is applied") 130 | // Default to hierarchical merge 131 | // Add relationships between primary package and described packages from merge sets 132 | for _, dp := range describedPkgs { 133 | currentPkgId := pkgMapper[dp] 134 | topLevelRels = append(topLevelRels, &spdx.Relationship{ 135 | RefA: common.MakeDocElementID("", string(primaryPkg.PackageSPDXIdentifier)), 136 | RefB: common.MakeDocElementID("", currentPkgId), 137 | Relationship: common.TypeRelationshipContains, 138 | RelationshipComment: "sbomasm created contains relationship to support hierarchical merge", 139 | }) 140 | } 141 | } 142 | 143 | // Add Relationships to document 144 | doc.Relationships = append(doc.Relationships, topLevelRels...) 145 | if len(rels) > 0 { 146 | doc.Relationships = append(doc.Relationships, rels...) 147 | } 148 | 149 | // Write the SBOM 150 | err = writeSBOM(doc, m) 151 | 152 | return err 153 | } 154 | -------------------------------------------------------------------------------- /pkg/assemble/util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package assemble 18 | 19 | import ( 20 | "os" 21 | 22 | "github.com/interlynk-io/sbomasm/pkg/detect" 23 | ) 24 | 25 | func detectSbom(path string) (string, string, error) { 26 | f, err := os.Open(path) 27 | if err != nil { 28 | return "", "", err 29 | } 30 | defer f.Close() 31 | 32 | spec, format, err := detect.Detect(f) 33 | if err != nil { 34 | return "", "", err 35 | } 36 | return string(spec), string(format), nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/detect/detect.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package detect 18 | 19 | import ( 20 | "bufio" 21 | "encoding/json" 22 | "encoding/xml" 23 | "fmt" 24 | "io" 25 | "strings" 26 | 27 | "gopkg.in/yaml.v2" 28 | ) 29 | 30 | type SBOMSpecFormat string 31 | 32 | const ( 33 | SBOMSpecSPDX SBOMSpecFormat = "spdx" 34 | SBOMSpecCDX SBOMSpecFormat = "cyclonedx" 35 | SBOMSpecUnknown SBOMSpecFormat = "unknown" 36 | ) 37 | 38 | type FileFormat string 39 | 40 | const ( 41 | FileFormatJSON FileFormat = "json" 42 | FileFormatRDF FileFormat = "rdf" 43 | FileFormatYAML FileFormat = "yaml" 44 | FileFormatTagValue FileFormat = "tag-value" 45 | FileFormatXML FileFormat = "xml" 46 | FileFormatUnknown FileFormat = "unknown" 47 | ) 48 | 49 | type spdxbasic struct { 50 | ID string `json:"SPDXID" yaml:"SPDXID"` 51 | } 52 | 53 | type cdxbasic struct { 54 | XMLNS string `json:"-" xml:"xmlns,attr"` 55 | BOMFormat string `json:"bomFormat" xml:"-"` 56 | } 57 | 58 | func Detect(f io.ReadSeeker) (SBOMSpecFormat, FileFormat, error) { 59 | defer f.Seek(0, io.SeekStart) 60 | 61 | f.Seek(0, io.SeekStart) 62 | 63 | var s spdxbasic 64 | if err := json.NewDecoder(f).Decode(&s); err == nil { 65 | if strings.HasPrefix(s.ID, "SPDX") { 66 | return SBOMSpecSPDX, FileFormatJSON, nil 67 | } 68 | } 69 | 70 | f.Seek(0, io.SeekStart) 71 | 72 | var cdx cdxbasic 73 | if err := json.NewDecoder(f).Decode(&cdx); err == nil { 74 | if cdx.BOMFormat == "CycloneDX" { 75 | return SBOMSpecCDX, FileFormatJSON, nil 76 | } 77 | } 78 | 79 | f.Seek(0, io.SeekStart) 80 | 81 | if err := xml.NewDecoder(f).Decode(&cdx); err == nil { 82 | if strings.HasPrefix(cdx.XMLNS, "http://cyclonedx.org") { 83 | return SBOMSpecCDX, FileFormatXML, nil 84 | } 85 | } 86 | f.Seek(0, io.SeekStart) 87 | 88 | if sc := bufio.NewScanner(f); sc.Scan() { 89 | if strings.HasPrefix(sc.Text(), "SPDX") { 90 | return SBOMSpecSPDX, FileFormatTagValue, nil 91 | } 92 | } 93 | 94 | f.Seek(0, io.SeekStart) 95 | 96 | var y spdxbasic 97 | if err := yaml.NewDecoder(f).Decode(&y); err == nil { 98 | if strings.HasPrefix(y.ID, "SPDX") { 99 | return SBOMSpecSPDX, FileFormatYAML, nil 100 | } 101 | } 102 | 103 | return "", "", fmt.Errorf("unknown spec or format") 104 | } 105 | -------------------------------------------------------------------------------- /pkg/dt/dt_interface.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package dt 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "os" 23 | 24 | dtrack "github.com/DependencyTrack/client-go" 25 | "github.com/google/uuid" 26 | "github.com/interlynk-io/sbomasm/pkg/logger" 27 | ) 28 | 29 | type Params struct { 30 | Url string 31 | ApiKey string 32 | ProjectIds []uuid.UUID 33 | UploadProjectID uuid.UUID 34 | 35 | Ctx *context.Context 36 | Input []string 37 | Output string 38 | Upload bool 39 | 40 | Name string 41 | Version string 42 | Type string 43 | 44 | FlatMerge bool 45 | HierMerge bool 46 | AssemblyMerge bool 47 | 48 | Xml bool 49 | Json bool 50 | 51 | OutputSpec string 52 | OutputSpecVersion string 53 | } 54 | 55 | func NewParams() *Params { 56 | return &Params{} 57 | } 58 | 59 | func (dtP *Params) PopulateInputField(ctx context.Context) { 60 | log := logger.FromContext(ctx) 61 | 62 | log.Debugf("Config: %+v", dtP) 63 | 64 | dTrackClient, err := dtrack.NewClient(dtP.Url, 65 | dtrack.WithAPIKey(dtP.ApiKey), dtrack.WithDebug(false)) 66 | if err != nil { 67 | log.Fatalf("Failed to create Dependency-Track client: %s", err) 68 | } 69 | 70 | for _, pid := range dtP.ProjectIds { 71 | log.Debugf("Processing project %s", pid) 72 | 73 | prj, err := dTrackClient.Project.Get(ctx, pid) 74 | if err != nil { 75 | log.Infof("Failed to get project, Check projectID or API port or Hostname.") 76 | log.Fatalf("Failed to get project: %s", err) 77 | } 78 | log.Debugf("ID: %s, Name: %s, Version: %s", prj.UUID, prj.Name, prj.Version) 79 | 80 | bom, err := dTrackClient.BOM.ExportProject(ctx, pid, dtrack.BOMFormatJSON, dtrack.BOMVariantInventory) 81 | if err != nil { 82 | log.Fatalln("Failed to export project: %s", err) 83 | } 84 | 85 | fname := fmt.Sprintf("tmpfile-%s", pid) 86 | f, err := os.CreateTemp("", fname) 87 | if err != nil { 88 | log.Fatal(err) 89 | } 90 | defer f.Close() 91 | 92 | f.WriteString(bom) 93 | dtP.Input = append(dtP.Input, f.Name()) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /pkg/dt/interface.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package dt 18 | 19 | type common interface { 20 | PopulateInputField() 21 | } 22 | -------------------------------------------------------------------------------- /pkg/edit/cdx.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package edit 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "fmt" 23 | "io" 24 | "os" 25 | "strings" 26 | 27 | cydx "github.com/CycloneDX/cyclonedx-go" 28 | "github.com/google/uuid" 29 | 30 | "github.com/interlynk-io/sbomasm/pkg/detect" 31 | liclib "github.com/interlynk-io/sbomasm/pkg/licenses" 32 | "github.com/interlynk-io/sbomasm/pkg/logger" 33 | ) 34 | 35 | var cdx_strings_to_types = map[string]cydx.ComponentType{ 36 | "application": cydx.ComponentTypeApplication, 37 | "container": cydx.ComponentTypeContainer, 38 | "device": cydx.ComponentTypeDevice, 39 | "file": cydx.ComponentTypeFile, 40 | "framework": cydx.ComponentTypeFramework, 41 | "library": cydx.ComponentTypeLibrary, 42 | "firmware": cydx.ComponentTypeFirmware, 43 | "operating-system": cydx.ComponentTypeOS, 44 | } 45 | 46 | var cdx_hash_algos = map[string]cydx.HashAlgorithm{ 47 | "MD5": cydx.HashAlgoMD5, 48 | "SHA-1": cydx.HashAlgoSHA1, 49 | "SHA-256": cydx.HashAlgoSHA256, 50 | "SHA-384": cydx.HashAlgoSHA384, 51 | "SHA-512": cydx.HashAlgoSHA512, 52 | "SHA3-256": cydx.HashAlgoSHA3_256, 53 | "SHA3-384": cydx.HashAlgoSHA3_384, 54 | "SHA3-512": cydx.HashAlgoSHA3_512, 55 | "BLAKE2b-256": cydx.HashAlgoBlake2b_256, 56 | "BLAKE2b-384": cydx.HashAlgoBlake2b_384, 57 | "BLAKE2b-512": cydx.HashAlgoBlake2b_512, 58 | "BLAKE3": cydx.HashAlgoBlake3, 59 | } 60 | 61 | var cdx_lifecycle_phases = map[string]cydx.LifecyclePhase{ 62 | "design": cydx.LifecyclePhaseDesign, 63 | "pre-build": cydx.LifecyclePhasePreBuild, 64 | "build": cydx.LifecyclePhaseBuild, 65 | "post-build": cydx.LifecyclePhasePostBuild, 66 | "operations": cydx.LifecyclePhaseOperations, 67 | "discovery": cydx.LifecyclePhaseDiscovery, 68 | "decommission": cydx.LifecyclePhaseDecommission, 69 | } 70 | 71 | func cdxEdit(c *configParams) error { 72 | log := logger.FromContext(*c.ctx) 73 | 74 | bom, err := loadCdxBom(*c.ctx, c.inputFilePath) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | doc, err := NewCdxEditDoc(bom, c) 80 | if doc == nil { 81 | return fmt.Errorf("failed to edit cdx document: %w", err) 82 | } 83 | 84 | if c.shouldSearch() && doc.comp == nil { 85 | return errors.New(fmt.Sprintf("component not found: %s, %s", c.search.name, c.search.version)) 86 | } 87 | 88 | if doc.comp != nil { 89 | log.Debugf("Component found %s, %s", doc.comp.Name, doc.comp.Version) 90 | } 91 | 92 | doc.update() 93 | 94 | return writeCdxBom(doc.bom, c) 95 | } 96 | 97 | func loadCdxBom(ctx context.Context, path string) (*cydx.BOM, error) { 98 | log := logger.FromContext(ctx) 99 | 100 | var err error 101 | var bom *cydx.BOM 102 | 103 | f, err := os.Open(path) 104 | if err != nil { 105 | return nil, err 106 | } 107 | defer f.Close() 108 | 109 | spec, format, err := detect.Detect(f) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | log.Debugf("loading bom:%s spec:%s format:%s", path, spec, format) 115 | 116 | switch format { 117 | case detect.FileFormatJSON: 118 | bom = new(cydx.BOM) 119 | decoder := cydx.NewBOMDecoder(f, cydx.BOMFileFormatJSON) 120 | if err = decoder.Decode(bom); err != nil { 121 | return nil, err 122 | } 123 | case detect.FileFormatXML: 124 | bom = new(cydx.BOM) 125 | decoder := cydx.NewBOMDecoder(f, cydx.BOMFileFormatXML) 126 | if err = decoder.Decode(bom); err != nil { 127 | return nil, err 128 | } 129 | default: 130 | panic("unsupported file format") // TODO: return error instead of panic 131 | } 132 | 133 | return bom, nil 134 | } 135 | 136 | func writeCdxBom(bom *cydx.BOM, c *configParams) error { 137 | var f io.Writer 138 | 139 | // Always generate a new serial number on edit 140 | bom.SerialNumber = newCdxSerialNumber() 141 | 142 | if c.outputFilePath == "" { 143 | f = os.Stdout 144 | } else { 145 | var err error 146 | f, err = os.Create(c.outputFilePath) 147 | if err != nil { 148 | return err 149 | } 150 | } 151 | 152 | inf, err := os.Open(c.inputFilePath) 153 | if err != nil { 154 | return err 155 | } 156 | defer inf.Close() 157 | 158 | _, format, err := detect.Detect(inf) 159 | if err != nil { 160 | return err 161 | } 162 | 163 | var encoder cydx.BOMEncoder 164 | 165 | switch format { 166 | case detect.FileFormatJSON: 167 | encoder = cydx.NewBOMEncoder(f, cydx.BOMFileFormatJSON) 168 | case detect.FileFormatXML: 169 | encoder = cydx.NewBOMEncoder(f, cydx.BOMFileFormatXML) 170 | } 171 | 172 | encoder.SetPretty(true) 173 | encoder.SetEscapeHTML(true) 174 | 175 | if err := encoder.Encode(bom); err != nil { 176 | return err 177 | } 178 | 179 | return nil 180 | } 181 | 182 | func cdxFindComponent(b *cydx.BOM, c *configParams) *cydx.Component { 183 | if c.search.subject != "component-name-version" { 184 | return nil 185 | } 186 | 187 | for i := range *b.Components { 188 | comp := &(*b.Components)[i] 189 | if comp.Name == c.search.name && comp.Version == c.search.version { 190 | return comp 191 | } 192 | } 193 | 194 | return nil 195 | } 196 | 197 | func cdxUniqTools(a *cydx.ToolsChoice, b *cydx.ToolsChoice) *cydx.ToolsChoice { 198 | choices := cydx.ToolsChoice{} 199 | 200 | if a == nil && b == nil { 201 | return &choices 202 | } 203 | 204 | if a == nil && b != nil { 205 | return b 206 | } 207 | 208 | if a != nil && b == nil { 209 | return a 210 | } 211 | 212 | if a.Tools != nil && b.Tools != nil { 213 | choices.Tools = new([]cydx.Tool) 214 | uniqTools := make(map[string]string) 215 | 216 | for _, tool := range *a.Tools { 217 | key := fmt.Sprintf("%s-%s", strings.ToLower(tool.Name), strings.ToLower(tool.Version)) 218 | 219 | if _, ok := uniqTools[key]; !ok { 220 | *choices.Tools = append(*choices.Tools, tool) 221 | uniqTools[key] = key 222 | } 223 | } 224 | 225 | for _, tool := range *b.Tools { 226 | key := fmt.Sprintf("%s-%s", strings.ToLower(tool.Name), strings.ToLower(tool.Version)) 227 | 228 | if _, ok := uniqTools[key]; !ok { 229 | *choices.Tools = append(*choices.Tools, tool) 230 | uniqTools[key] = key 231 | } 232 | } 233 | } 234 | 235 | if a.Components != nil && b.Components != nil { 236 | choices.Components = new([]cydx.Component) 237 | uniqTools := make(map[string]string) 238 | 239 | for _, tool := range *a.Components { 240 | key := fmt.Sprintf("%s-%s", strings.ToLower(tool.Name), strings.ToLower(tool.Version)) 241 | 242 | if _, ok := uniqTools[key]; !ok { 243 | *choices.Components = append(*choices.Components, tool) 244 | uniqTools[key] = key 245 | } 246 | } 247 | 248 | for _, tool := range *b.Components { 249 | key := fmt.Sprintf("%s-%s", strings.ToLower(tool.Name), strings.ToLower(tool.Version)) 250 | 251 | if _, ok := uniqTools[key]; !ok { 252 | *choices.Components = append(*choices.Components, tool) 253 | uniqTools[key] = key 254 | } 255 | } 256 | } 257 | 258 | if a.Services != nil && b.Services != nil { 259 | choices.Services = new([]cydx.Service) 260 | uniqTools := make(map[string]string) 261 | 262 | for _, tool := range *a.Services { 263 | key := fmt.Sprintf("%s-%s", strings.ToLower(tool.Name), strings.ToLower(tool.Version)) 264 | 265 | if _, ok := uniqTools[key]; !ok { 266 | *choices.Services = append(*choices.Services, tool) 267 | uniqTools[key] = key 268 | } 269 | } 270 | 271 | for _, tool := range *b.Services { 272 | key := fmt.Sprintf("%s-%s", strings.ToLower(tool.Name), strings.ToLower(tool.Version)) 273 | 274 | if _, ok := uniqTools[key]; !ok { 275 | *choices.Services = append(*choices.Services, tool) 276 | uniqTools[key] = key 277 | } 278 | } 279 | } 280 | 281 | return &choices 282 | } 283 | 284 | func cdxConstructTools(b *cydx.BOM, c *configParams) *cydx.ToolsChoice { 285 | choice := cydx.ToolsChoice{} 286 | 287 | if b.SpecVersion > cydx.SpecVersion1_4 { 288 | choice.Components = new([]cydx.Component) 289 | } else { 290 | choice.Tools = new([]cydx.Tool) 291 | } 292 | 293 | uniqTools := make(map[string]string) 294 | 295 | for _, tool := range c.tools { 296 | key := fmt.Sprintf("%s-%s", strings.ToLower(tool.name), strings.ToLower(tool.value)) 297 | 298 | if _, ok := uniqTools[key]; !ok { 299 | if b.SpecVersion > cydx.SpecVersion1_4 { 300 | *choice.Components = append(*choice.Components, cydx.Component{ 301 | Type: cydx.ComponentTypeApplication, 302 | Name: tool.name, 303 | Version: tool.value, 304 | }) 305 | } else { 306 | *choice.Tools = append(*choice.Tools, cydx.Tool{ 307 | Name: tool.name, 308 | Version: tool.value, 309 | }) 310 | } 311 | 312 | uniqTools[key] = key 313 | } 314 | } 315 | 316 | return &choice 317 | } 318 | 319 | func cdxConstructHashes(_ *cydx.BOM, c *configParams) *[]cydx.Hash { 320 | hashes := []cydx.Hash{} 321 | 322 | for _, hash := range c.hashes { 323 | hashes = append(hashes, cydx.Hash{ 324 | Algorithm: cydx.HashAlgorithm(hash.name), 325 | Value: hash.value, 326 | }) 327 | } 328 | 329 | return &hashes 330 | } 331 | 332 | func cdxConstructLicenses(_ *cydx.BOM, c *configParams) cydx.Licenses { 333 | licenses := cydx.Licenses{} 334 | 335 | for _, license := range c.licenses { 336 | if liclib.IsSpdxExpression(license.name) { 337 | licenses = append(licenses, cydx.LicenseChoice{ 338 | Expression: license.name, 339 | }) 340 | } else { 341 | lic, err := liclib.LookupSpdxLicense(license.name) 342 | if err != nil { 343 | licenses = append(licenses, cydx.LicenseChoice{ 344 | License: &cydx.License{ 345 | BOMRef: newBomRef(), 346 | Name: license.name, 347 | URL: license.value, 348 | }, 349 | }) 350 | } else { 351 | licenses = append(licenses, cydx.LicenseChoice{ 352 | License: &cydx.License{ 353 | BOMRef: newBomRef(), 354 | ID: lic.ShortID(), 355 | Name: lic.Name(), 356 | URL: license.value, 357 | }, 358 | }) 359 | } 360 | } 361 | } 362 | return licenses 363 | } 364 | 365 | func cdxConstructSupplier(_ *cydx.BOM, c *configParams) *cydx.OrganizationalEntity { 366 | entity := cydx.OrganizationalEntity{ 367 | BOMRef: newBomRef(), 368 | Name: c.supplier.name, 369 | URL: &[]string{ 370 | c.supplier.value, 371 | }, 372 | } 373 | return &entity 374 | } 375 | 376 | func cdxConstructAuthors(_ *cydx.BOM, c *configParams) *[]cydx.OrganizationalContact { 377 | authors := []cydx.OrganizationalContact{} 378 | 379 | for _, author := range c.authors { 380 | authors = append(authors, cydx.OrganizationalContact{ 381 | BOMRef: newBomRef(), 382 | Name: author.name, 383 | Email: author.value, 384 | }) 385 | } 386 | 387 | return &authors 388 | } 389 | 390 | func newCdxSerialNumber() string { 391 | u := uuid.New().String() 392 | 393 | return fmt.Sprintf("urn:uuid:%s", u) 394 | } 395 | 396 | func newBomRef() string { 397 | u := uuid.New().String() 398 | 399 | return fmt.Sprintf("sbomasm:%s", u) 400 | } 401 | -------------------------------------------------------------------------------- /pkg/edit/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package edit 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "os" 21 | "regexp" 22 | "strings" 23 | ) 24 | 25 | var supportedSubjects map[string]bool = map[string]bool{ 26 | "document": true, 27 | "primary-component": true, 28 | "component-name-version": true, 29 | } 30 | 31 | type SearchParams struct { 32 | subject string 33 | name string 34 | version string 35 | missing bool 36 | append bool 37 | } 38 | 39 | type paramTuple struct { 40 | name string 41 | value string 42 | } 43 | 44 | type configParams struct { 45 | ctx *context.Context 46 | 47 | inputFilePath string 48 | outputFilePath string 49 | 50 | search SearchParams 51 | 52 | name string 53 | version string 54 | supplier paramTuple 55 | authors []paramTuple 56 | purl string 57 | cpe string 58 | licenses []paramTuple 59 | hashes []paramTuple 60 | tools []paramTuple 61 | copyright string 62 | lifecycles []string 63 | description string 64 | repository string 65 | typ string 66 | 67 | timestamp bool 68 | } 69 | 70 | func (c *configParams) shouldTimeStamp() bool { 71 | return c.timestamp 72 | } 73 | 74 | func (c *configParams) shouldTyp() bool { 75 | return c.typ != "" 76 | } 77 | 78 | func (c *configParams) shouldRepository() bool { 79 | return c.repository != "" 80 | } 81 | 82 | func (c *configParams) shouldDescription() bool { 83 | return c.description != "" 84 | } 85 | 86 | func (c *configParams) shouldCopyRight() bool { 87 | return c.copyright != "" 88 | } 89 | 90 | func (c *configParams) shouldTools() bool { 91 | return len(c.tools) > 0 92 | } 93 | 94 | func (c *configParams) shouldHashes() bool { 95 | return len(c.hashes) > 0 96 | } 97 | 98 | func (c *configParams) shouldLicenses() bool { 99 | return len(c.licenses) > 0 100 | } 101 | 102 | func (c *configParams) shouldCpe() bool { 103 | return c.cpe != "" 104 | } 105 | 106 | func (c *configParams) shouldPurl() bool { 107 | return c.purl != "" 108 | } 109 | 110 | func (c *configParams) shouldAuthors() bool { 111 | return len(c.authors) > 0 112 | } 113 | 114 | func (c *configParams) shouldSupplier() bool { 115 | return c.supplier.value != "" 116 | } 117 | 118 | func (c *configParams) shouldVersion() bool { 119 | return c.version != "" 120 | } 121 | 122 | func (c *configParams) shouldName() bool { 123 | return c.name != "" 124 | } 125 | 126 | func (c *configParams) shouldOutput() bool { 127 | return c.outputFilePath != "" 128 | } 129 | 130 | func (c *configParams) shouldLifeCycle() bool { 131 | return len(c.lifecycles) > 0 132 | } 133 | 134 | func (c *configParams) onMissing() bool { 135 | return c.search.missing 136 | } 137 | 138 | func (c *configParams) onAppend() bool { 139 | return c.search.append 140 | } 141 | 142 | func (c *configParams) shouldSearch() bool { 143 | return c.search.subject == "component-name-version" 144 | } 145 | 146 | func (c *configParams) getFormattedAuthors() string { 147 | 148 | authors := []string{} 149 | for _, author := range c.authors { 150 | authors = append(authors, fmt.Sprintf("%s <%s>", author.name, author.value)) 151 | } 152 | 153 | return strings.Join(authors, ",") 154 | } 155 | 156 | func convertToConfigParams(eParams *EditParams) (*configParams, error) { 157 | p := &configParams{} 158 | 159 | //log := logger.FromContext(*eParams.Ctx) 160 | 161 | p.ctx = eParams.Ctx 162 | 163 | if err := validatePath(eParams.Input); err != nil { 164 | return nil, err 165 | } 166 | 167 | p.inputFilePath = eParams.Input 168 | 169 | if eParams.Output != "" { 170 | p.outputFilePath = eParams.Output 171 | } 172 | 173 | p.search = SearchParams{} 174 | 175 | if eParams.Subject != "" { 176 | p.search.subject = eParams.Subject 177 | } 178 | 179 | p.search = SearchParams{} 180 | 181 | if eParams.Subject != "" && supportedSubjects[strings.ToLower(eParams.Subject)] { 182 | p.search.subject = strings.ToLower(eParams.Subject) 183 | } else { 184 | return nil, fmt.Errorf("unsupported subject %s", eParams.Subject) 185 | } 186 | 187 | if p.search.subject == "component-name-version" { 188 | name, version := parseInputFormat(eParams.Search) 189 | if name == "" || version == "" { 190 | return nil, fmt.Errorf("invalid component-name-version format both name and version must be provided") 191 | } 192 | p.search.name = name 193 | p.search.version = version 194 | } 195 | 196 | p.search.missing = eParams.Missing 197 | p.search.append = eParams.Append 198 | 199 | p.name = eParams.Name 200 | p.version = eParams.Version 201 | 202 | if eParams.Supplier != "" { 203 | name, email := parseInputFormat(eParams.Supplier) 204 | 205 | p.supplier = paramTuple{ 206 | name: name, 207 | value: email, 208 | } 209 | } 210 | 211 | for _, author := range eParams.Authors { 212 | name, email := parseInputFormat(author) 213 | p.authors = append(p.authors, paramTuple{ 214 | name: name, 215 | value: email, 216 | }) 217 | } 218 | 219 | p.purl = eParams.Purl 220 | p.cpe = eParams.Cpe 221 | 222 | for _, license := range eParams.Licenses { 223 | name, url := parseInputFormat(license) 224 | p.licenses = append(p.licenses, paramTuple{ 225 | name: name, 226 | value: url, 227 | }) 228 | } 229 | 230 | for _, hash := range eParams.Hashes { 231 | algorithm, value := parseInputFormat(hash) 232 | p.hashes = append(p.hashes, paramTuple{ 233 | name: algorithm, 234 | value: value, 235 | }) 236 | } 237 | 238 | for _, tool := range eParams.Tools { 239 | name, version := parseInputFormat(tool) 240 | p.tools = append(p.tools, paramTuple{ 241 | name: name, 242 | value: version, 243 | }) 244 | } 245 | 246 | p.copyright = eParams.CopyRight 247 | p.lifecycles = eParams.Lifecycles 248 | p.description = eParams.Description 249 | p.repository = eParams.Repository 250 | p.typ = eParams.Type 251 | 252 | p.timestamp = eParams.Timestamp 253 | 254 | return p, nil 255 | } 256 | func parseInputFormat(s string) (name string, version string) { 257 | // Trim any leading/trailing whitespace 258 | s = strings.TrimSpace(s) 259 | 260 | // Regular expression to match the pattern 261 | re := regexp.MustCompile(`^(.+?)\s*(?:\(([^)]+)\))?$`) 262 | 263 | matches := re.FindStringSubmatch(s) 264 | if len(matches) > 1 { 265 | name = strings.TrimSpace(matches[1]) 266 | if len(matches) > 2 { 267 | version = strings.TrimSpace(matches[2]) 268 | } 269 | } else { 270 | name = s 271 | } 272 | 273 | return name, version 274 | } 275 | func validatePath(path string) error { 276 | stat, err := os.Stat(path) 277 | 278 | if err != nil { 279 | return err 280 | } 281 | 282 | if stat.IsDir() { 283 | return fmt.Errorf("path %s is a directory include only files", path) 284 | } 285 | 286 | return nil 287 | } 288 | -------------------------------------------------------------------------------- /pkg/edit/interface.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package edit 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/interlynk-io/sbomasm/pkg/logger" 21 | ) 22 | 23 | // EditParams represents the parameters for the edit command 24 | type EditParams struct { 25 | Ctx *context.Context 26 | 27 | Input string 28 | Output string 29 | 30 | Subject string 31 | Search string 32 | 33 | Append bool 34 | Missing bool 35 | 36 | Name string 37 | Version string 38 | Supplier string 39 | Timestamp bool 40 | Authors []string 41 | Purl string 42 | Cpe string 43 | Licenses []string 44 | Hashes []string 45 | Tools []string 46 | CopyRight string 47 | Lifecycles []string 48 | Description string 49 | Repository string 50 | Type string 51 | } 52 | 53 | func NewEditParams() *EditParams { 54 | return &EditParams{} 55 | } 56 | 57 | func Edit(eParams *EditParams) error { 58 | 59 | log := logger.FromContext(*eParams.Ctx) 60 | 61 | c, err := convertToConfigParams(eParams) 62 | if err != nil { 63 | return err 64 | } 65 | log.Debugf("config %+v", c) 66 | 67 | spec, format, err := detectSbom(eParams.Input) 68 | if err != nil { 69 | return err 70 | } 71 | log.Debugf("input sbom spec: %s format: %s", spec, format) 72 | 73 | if spec == "cyclonedx" { 74 | if err = cdxEdit(c); err != nil { 75 | return err 76 | } 77 | } 78 | 79 | if spec == "spdx" { 80 | if err = spdxEdit(c); err != nil { 81 | return err 82 | } 83 | } 84 | 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /pkg/edit/spdx.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package edit 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "fmt" 23 | "io" 24 | "os" 25 | "strings" 26 | 27 | "github.com/interlynk-io/sbomasm/pkg/detect" 28 | "github.com/interlynk-io/sbomasm/pkg/logger" 29 | "github.com/spdx/tools-golang/spdx" 30 | 31 | "github.com/samber/lo" 32 | spdx_json "github.com/spdx/tools-golang/json" 33 | spdx_rdf "github.com/spdx/tools-golang/rdf" 34 | "github.com/spdx/tools-golang/spdx/common" 35 | spdx_tv "github.com/spdx/tools-golang/tagvalue" 36 | spdx_yaml "github.com/spdx/tools-golang/yaml" 37 | ) 38 | 39 | var spdx_hash_algos = map[string]spdx.ChecksumAlgorithm{ 40 | "MD5": spdx.MD5, 41 | "SHA-1": spdx.SHA1, 42 | "SHA-256": spdx.SHA256, 43 | "SHA-384": spdx.SHA384, 44 | "SHA-512": spdx.SHA512, 45 | "SHA3-256": spdx.SHA256, 46 | "SHA3-384": spdx.SHA384, 47 | "SHA3-512": spdx.SHA512, 48 | "BLAKE2b-256": spdx.BLAKE2b_256, 49 | "BLAKE2b-384": spdx.BLAKE2b_384, 50 | "BLAKE2b-512": spdx.BLAKE2b_512, 51 | "BLAKE3": spdx.BLAKE3, 52 | } 53 | 54 | var spdx_strings_to_types = map[string]string{ 55 | "application": "APPLICATION", 56 | "framework": "FRAMEWORK", 57 | "library": "LIBRARY", 58 | "container": "CONTAINER", 59 | "operating-system": "OPERATING-SYSTEM", 60 | "device": "DEVICE", 61 | "firmware": "FIRMWARE", 62 | "source": "SOURCE", 63 | "archive": "ARCHIVE", 64 | "file": "FILE", 65 | "install": "INSTALL", 66 | "other": "OTHER", 67 | } 68 | 69 | func spdxEdit(c *configParams) error { 70 | // log := logger.FromContext(*c.ctx) 71 | 72 | bom, err := loadSpdxSbom(*c.ctx, c.inputFilePath) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | doc, err := NewSpdxEditDoc(bom, c) 78 | if doc == nil { 79 | return fmt.Errorf("failed to edit spdx document: %w", err) 80 | } 81 | 82 | doc.update() 83 | 84 | return writeSpdxSbom(doc.bom, c) 85 | } 86 | 87 | func loadSpdxSbom(ctx context.Context, path string) (*spdx.Document, error) { 88 | log := logger.FromContext(ctx) 89 | 90 | var d common.AnyDocument 91 | var err error 92 | 93 | f, err := os.Open(path) 94 | if err != nil { 95 | return nil, err 96 | } 97 | defer f.Close() 98 | 99 | spec, format, err := detect.Detect(f) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | log.Debugf("loading bom:%s spec:%s format:%s", path, spec, format) 105 | 106 | switch format { 107 | case detect.FileFormatJSON: 108 | d, err = spdx_json.Read(f) 109 | case detect.FileFormatTagValue: 110 | d, err = spdx_tv.Read(f) 111 | case detect.FileFormatYAML: 112 | d, err = spdx_yaml.Read(f) 113 | case detect.FileFormatRDF: 114 | d, err = spdx_rdf.Read(f) 115 | default: 116 | panic("unsupported spdx format") 117 | 118 | } 119 | 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | return d.(*spdx.Document), nil 125 | } 126 | 127 | func writeSpdxSbom(doc common.AnyDocument, m *configParams) error { 128 | var f io.Writer 129 | 130 | if m.outputFilePath == "" { 131 | f = os.Stdout 132 | } else { 133 | var err error 134 | f, err = os.Create(m.outputFilePath) 135 | if err != nil { 136 | return err 137 | } 138 | } 139 | 140 | inf, err := os.Open(m.inputFilePath) 141 | if err != nil { 142 | return err 143 | } 144 | defer inf.Close() 145 | 146 | _, format, err := detect.Detect(inf) 147 | if err != nil { 148 | return err 149 | } 150 | 151 | switch format { 152 | case detect.FileFormatJSON: 153 | var opt []spdx_json.WriteOption 154 | opt = append(opt, spdx_json.Indent(" ")) // to create multiline json 155 | opt = append(opt, spdx_json.EscapeHTML(true)) // to escape HTML characters 156 | spdx_json.Write(doc, f, opt...) 157 | case detect.FileFormatTagValue: 158 | spdx_tv.Write(doc, f) 159 | case detect.FileFormatYAML: 160 | spdx_yaml.Write(doc, f) 161 | case detect.FileFormatRDF: 162 | panic("write rdf format not supported") 163 | case detect.FileFormatXML: 164 | panic("write xml format not supported") 165 | } 166 | 167 | return nil 168 | } 169 | 170 | func spdxFindPkg(doc *spdx.Document, c *configParams, primaryPackage bool) (*spdx.Package, error) { 171 | pkgIDs := make(map[string]int) 172 | 173 | for index, pkg := range doc.Packages { 174 | pkgIDs[string(pkg.PackageSPDXIdentifier)] = index 175 | 176 | if primaryPackage == false { 177 | if pkg.PackageName == c.search.name && pkg.PackageVersion == c.search.version { 178 | return doc.Packages[index], nil 179 | } 180 | } 181 | } 182 | 183 | if primaryPackage { 184 | for _, r := range doc.Relationships { 185 | if strings.ToUpper(r.Relationship) == spdx.RelationshipDescribes { 186 | i, ok := pkgIDs[string(r.RefB.ElementRefID)] 187 | if ok { 188 | return doc.Packages[i], nil 189 | } 190 | } 191 | } 192 | } 193 | 194 | return nil, errors.New("package not found") 195 | } 196 | 197 | func spdxConstructLicenses(_ *spdx.Document, c *configParams) string { 198 | licenses := []string{} 199 | 200 | for _, l := range c.licenses { 201 | name := strings.ToLower(l.name) 202 | if name == "noassertion" || name == "none" { 203 | name = strings.ToUpper(l.name) 204 | } else { 205 | name = l.name 206 | } 207 | licenses = append(licenses, name) 208 | } 209 | 210 | return strings.Join(licenses, "OR") 211 | } 212 | 213 | func spdxConstructHashes(_ *spdx.Document, c *configParams) []spdx.Checksum { 214 | hashes := []spdx.Checksum{} 215 | 216 | for _, h := range c.hashes { 217 | hashes = append(hashes, spdx.Checksum{ 218 | Algorithm: spdx.ChecksumAlgorithm(h.name), 219 | Value: h.value, 220 | }) 221 | } 222 | 223 | return hashes 224 | } 225 | 226 | func spdxConstructTools(_ *spdx.Document, c *configParams) []spdx.Creator { 227 | tools := []spdx.Creator{} 228 | uniqTools := make(map[string]bool) 229 | 230 | for _, tool := range c.tools { 231 | parts := []string{tool.name, tool.value} 232 | key := fmt.Sprintf("%s-%s", strings.ToLower(tool.name), strings.ToLower(tool.value)) 233 | 234 | if _, ok := uniqTools[key]; !ok { 235 | tools = append(tools, spdx.Creator{ 236 | CreatorType: "Tool", 237 | Creator: strings.Join(lo.Compact(parts), "-"), 238 | }) 239 | 240 | uniqTools[key] = true 241 | } 242 | } 243 | return tools 244 | } 245 | 246 | func spdxUniqueTools(a []spdx.Creator, b []spdx.Creator) []spdx.Creator { 247 | tools := a 248 | uniqTools := make(map[string]bool) 249 | 250 | for _, tool := range b { 251 | key := fmt.Sprintf("%s-%s", strings.ToLower(tool.CreatorType), strings.ToLower(tool.Creator)) 252 | 253 | if _, ok := uniqTools[key]; !ok { 254 | tools = append(tools, tool) 255 | uniqTools[key] = true 256 | } 257 | } 258 | return tools 259 | } 260 | -------------------------------------------------------------------------------- /pkg/edit/spdx_edit.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package edit 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | 23 | "github.com/interlynk-io/sbomasm/internal/version" 24 | "github.com/interlynk-io/sbomasm/pkg/logger" 25 | "github.com/samber/lo" 26 | "github.com/spdx/tools-golang/spdx" 27 | ) 28 | 29 | const ( 30 | SBOMASM = "sbomasm" 31 | ) 32 | 33 | var SBOMASM_VERSION = version.Version 34 | 35 | type spdxEditDoc struct { 36 | bom *spdx.Document 37 | pkg *spdx.Package 38 | c *configParams 39 | } 40 | 41 | func NewSpdxEditDoc(bom *spdx.Document, c *configParams) (*spdxEditDoc, error) { 42 | doc := &spdxEditDoc{} 43 | 44 | doc.bom = bom 45 | doc.c = c 46 | 47 | if c.search.subject == "primary-component" { 48 | if doc.pkg == nil { 49 | return nil, fmt.Errorf("primary package is missing") 50 | } 51 | pkg, err := spdxFindPkg(bom, c, true) 52 | if err == nil { 53 | doc.pkg = pkg 54 | } 55 | } 56 | 57 | if c.search.subject == "component-name-version" { 58 | 59 | pkg, err := spdxFindPkg(bom, c, false) 60 | if err == nil { 61 | doc.pkg = pkg 62 | } 63 | if doc.pkg == nil { 64 | return nil, fmt.Errorf("package is missing") 65 | } 66 | } 67 | return doc, nil 68 | } 69 | 70 | func (d *spdxEditDoc) update() { 71 | log := logger.FromContext(*d.c.ctx) 72 | log.Debug("SPDX updating sbom") 73 | 74 | updateFuncs := []struct { 75 | name string 76 | f func() error 77 | }{ 78 | {"name", d.name}, 79 | {"version", d.version}, 80 | {"supplier", d.supplier}, 81 | {"authors", d.authors}, 82 | {"purl", d.purl}, 83 | {"cpe", d.cpe}, 84 | {"licenses", d.licenses}, 85 | {"hashes", d.hashes}, 86 | {"tools", d.tools}, 87 | {"copyright", d.copyright}, 88 | {"lifeCycles", d.lifeCycles}, 89 | {"description", d.description}, 90 | {"repository", d.repository}, 91 | {"type", d.typ}, 92 | {"timeStamp", d.timeStamp}, 93 | } 94 | 95 | for _, item := range updateFuncs { 96 | if err := item.f(); err != nil { 97 | if err == errNotSupported { 98 | log.Infof(fmt.Sprintf("SPDX error updating %s: %s", item.name, err)) 99 | } 100 | } 101 | } 102 | } 103 | 104 | func (d *spdxEditDoc) name() error { 105 | if !d.c.shouldName() { 106 | return errNoConfiguration 107 | } 108 | 109 | if d.c.search.subject == "document" { 110 | return errNotSupported 111 | } 112 | 113 | if d.c.onMissing() { 114 | if d.pkg.PackageName == "" { 115 | d.pkg.PackageName = d.c.name 116 | } 117 | } else { 118 | d.pkg.PackageName = d.c.name 119 | } 120 | 121 | return nil 122 | } 123 | 124 | func (d *spdxEditDoc) version() error { 125 | if !d.c.shouldVersion() { 126 | return errNoConfiguration 127 | } 128 | 129 | if d.c.search.subject == "document" { 130 | return errNotSupported 131 | } 132 | 133 | if d.c.onMissing() { 134 | if d.pkg.PackageVersion == "" { 135 | d.pkg.PackageVersion = d.c.version 136 | } 137 | } else { 138 | d.pkg.PackageVersion = d.c.version 139 | } 140 | return nil 141 | } 142 | 143 | func (d *spdxEditDoc) supplier() error { 144 | if !d.c.shouldSupplier() { 145 | return errNoConfiguration 146 | } 147 | 148 | if d.c.search.subject == "document" { 149 | return errNotSupported 150 | } 151 | 152 | supplier := spdx.Supplier{ 153 | SupplierType: "Organization", 154 | Supplier: fmt.Sprintf("%s (%s)", d.c.supplier.name, d.c.supplier.value), 155 | } 156 | 157 | if d.c.onMissing() { 158 | if d.pkg.PackageSupplier == nil { 159 | d.pkg.PackageSupplier = &supplier 160 | } 161 | } else { 162 | d.pkg.PackageSupplier = &supplier 163 | } 164 | 165 | return nil 166 | } 167 | 168 | func (d *spdxEditDoc) authors() error { 169 | if !d.c.shouldAuthors() { 170 | return errNoConfiguration 171 | } 172 | 173 | if d.c.search.subject != "document" { 174 | return errNotSupported 175 | } 176 | 177 | authors := []spdx.Creator{} 178 | 179 | for _, author := range d.c.authors { 180 | authors = append(authors, spdx.Creator{ 181 | CreatorType: "Person", 182 | Creator: fmt.Sprintf("%s (%s)", author.name, author.value), 183 | }) 184 | } 185 | 186 | if d.c.onMissing() { 187 | if d.bom.CreationInfo == nil { 188 | d.bom.CreationInfo = &spdx.CreationInfo{ 189 | Creators: authors, 190 | } 191 | } else if d.bom.CreationInfo.Creators == nil { 192 | d.bom.CreationInfo.Creators = authors 193 | } 194 | } else if d.c.onAppend() { 195 | if d.bom.CreationInfo == nil { 196 | d.bom.CreationInfo = &spdx.CreationInfo{ 197 | Creators: authors, 198 | } 199 | } else if d.bom.CreationInfo.Creators == nil { 200 | d.bom.CreationInfo.Creators = authors 201 | } else { 202 | d.bom.CreationInfo.Creators = append(d.bom.CreationInfo.Creators, authors...) 203 | } 204 | } else { 205 | if d.bom.CreationInfo == nil { 206 | d.bom.CreationInfo = &spdx.CreationInfo{ 207 | Creators: authors, 208 | } 209 | } else { 210 | d.bom.CreationInfo.Creators = authors 211 | } 212 | } 213 | return nil 214 | } 215 | 216 | func (d *spdxEditDoc) purl() error { 217 | if !d.c.shouldPurl() { 218 | return errNoConfiguration 219 | } 220 | 221 | if d.c.search.subject == "document" { 222 | return errNotSupported 223 | } 224 | 225 | purl := spdx.PackageExternalReference{ 226 | Category: "PACKAGE-MANAGER", 227 | RefType: "purl", 228 | Locator: d.c.purl, 229 | } 230 | 231 | foundPurl := false 232 | for _, ref := range d.pkg.PackageExternalReferences { 233 | if ref.RefType == "purl" { 234 | foundPurl = true 235 | } 236 | } 237 | 238 | if d.c.onMissing() { 239 | if !foundPurl { 240 | if d.pkg.PackageExternalReferences == nil { 241 | d.pkg.PackageExternalReferences = []*spdx.PackageExternalReference{} 242 | } 243 | d.pkg.PackageExternalReferences = append(d.pkg.PackageExternalReferences, &purl) 244 | } 245 | } else if d.c.onAppend() { 246 | if !foundPurl { 247 | if d.pkg.PackageExternalReferences == nil { 248 | d.pkg.PackageExternalReferences = []*spdx.PackageExternalReference{} 249 | } 250 | d.pkg.PackageExternalReferences = append(d.pkg.PackageExternalReferences, &purl) 251 | } else { 252 | d.pkg.PackageExternalReferences = append(d.pkg.PackageExternalReferences, &purl) 253 | } 254 | } else { 255 | if d.pkg.PackageExternalReferences == nil { 256 | d.pkg.PackageExternalReferences = []*spdx.PackageExternalReference{} 257 | d.pkg.PackageExternalReferences = append(d.pkg.PackageExternalReferences, &purl) 258 | } else { 259 | extRef := lo.Reject(d.pkg.PackageExternalReferences, func(x *spdx.PackageExternalReference, _ int) bool { 260 | return strings.ToLower(x.RefType) == "purl" 261 | }) 262 | 263 | if extRef == nil { 264 | extRef = []*spdx.PackageExternalReference{} 265 | } 266 | 267 | d.pkg.PackageExternalReferences = append(extRef, &purl) 268 | } 269 | } 270 | return nil 271 | } 272 | 273 | func (d *spdxEditDoc) cpe() error { 274 | if !d.c.shouldCpe() { 275 | return errNoConfiguration 276 | } 277 | 278 | if d.c.search.subject == "document" { 279 | return errNotSupported 280 | } 281 | 282 | cpe := spdx.PackageExternalReference{ 283 | Category: "SECURITY", 284 | RefType: "cpe23Type", 285 | Locator: d.c.cpe, 286 | } 287 | 288 | foundCpe := false 289 | for _, ref := range d.pkg.PackageExternalReferences { 290 | if ref.RefType == "cpe23Type" { 291 | foundCpe = true 292 | } 293 | } 294 | 295 | if d.c.onMissing() { 296 | if !foundCpe { 297 | if d.pkg.PackageExternalReferences == nil { 298 | d.pkg.PackageExternalReferences = []*spdx.PackageExternalReference{} 299 | } 300 | d.pkg.PackageExternalReferences = append(d.pkg.PackageExternalReferences, &cpe) 301 | } 302 | } else if d.c.onAppend() { 303 | if !foundCpe { 304 | if d.pkg.PackageExternalReferences == nil { 305 | d.pkg.PackageExternalReferences = []*spdx.PackageExternalReference{} 306 | } 307 | d.pkg.PackageExternalReferences = append(d.pkg.PackageExternalReferences, &cpe) 308 | } else { 309 | d.pkg.PackageExternalReferences = append(d.pkg.PackageExternalReferences, &cpe) 310 | } 311 | } else { 312 | if d.pkg.PackageExternalReferences == nil { 313 | d.pkg.PackageExternalReferences = []*spdx.PackageExternalReference{} 314 | d.pkg.PackageExternalReferences = append(d.pkg.PackageExternalReferences, &cpe) 315 | } else { 316 | extRef := lo.Reject(d.pkg.PackageExternalReferences, func(x *spdx.PackageExternalReference, _ int) bool { 317 | return strings.ToLower(x.RefType) == "cpe23Type" 318 | }) 319 | 320 | if extRef == nil { 321 | extRef = []*spdx.PackageExternalReference{} 322 | } 323 | 324 | d.pkg.PackageExternalReferences = append(extRef, &cpe) 325 | } 326 | } 327 | return nil 328 | } 329 | 330 | func (d *spdxEditDoc) licenses() error { 331 | if !d.c.shouldLicenses() { 332 | return errNoConfiguration 333 | } 334 | 335 | license := spdxConstructLicenses(d.bom, d.c) 336 | 337 | if d.c.onMissing() { 338 | if d.c.search.subject == "document" { 339 | if d.bom.DataLicense == "" { 340 | d.bom.DataLicense = license 341 | } 342 | } else { 343 | if d.pkg.PackageLicenseConcluded == "" { 344 | d.pkg.PackageLicenseConcluded = license 345 | } 346 | } 347 | } else { 348 | if d.c.search.subject == "document" { 349 | d.bom.DataLicense = license 350 | } else { 351 | d.pkg.PackageLicenseConcluded = license 352 | } 353 | } 354 | return nil 355 | } 356 | 357 | func (d *spdxEditDoc) hashes() error { 358 | if !d.c.shouldHashes() { 359 | return errNoConfiguration 360 | } 361 | 362 | if d.c.search.subject == "document" { 363 | return errNotSupported 364 | } 365 | 366 | hashes := spdxConstructHashes(d.bom, d.c) 367 | 368 | if d.c.onMissing() { 369 | if d.pkg.PackageChecksums == nil { 370 | d.pkg.PackageChecksums = hashes 371 | } 372 | } else if d.c.onAppend() { 373 | if d.pkg.PackageChecksums == nil { 374 | d.pkg.PackageChecksums = hashes 375 | } else { 376 | d.pkg.PackageChecksums = append(d.pkg.PackageChecksums, hashes...) 377 | } 378 | } else { 379 | d.pkg.PackageChecksums = hashes 380 | } 381 | 382 | return nil 383 | } 384 | 385 | func (d *spdxEditDoc) tools() error { 386 | // default sbomasm tool 387 | sbomasmTool := spdx.Creator{ 388 | Creator: fmt.Sprintf("%s-%s", SBOMASM, SBOMASM_VERSION), 389 | CreatorType: "Tool", 390 | } 391 | 392 | if d.bom.CreationInfo == nil { 393 | d.bom.CreationInfo = &spdx.CreationInfo{} 394 | } 395 | 396 | if d.bom.CreationInfo.Creators == nil { 397 | d.bom.CreationInfo.Creators = []spdx.Creator{} 398 | } 399 | 400 | newTools := spdxConstructTools(d.bom, d.c) 401 | 402 | explicitSbomasm := false 403 | for _, tool := range newTools { 404 | if strings.HasPrefix(tool.Creator, SBOMASM) { 405 | sbomasmTool = tool 406 | explicitSbomasm = true 407 | break 408 | } 409 | } 410 | 411 | if explicitSbomasm { 412 | d.bom.CreationInfo.Creators = removeCreator(d.bom.CreationInfo.Creators, SBOMASM) 413 | } 414 | 415 | if d.c.onMissing() { 416 | for _, tool := range newTools { 417 | if !creatorExists(d.bom.CreationInfo.Creators, tool) { 418 | d.bom.CreationInfo.Creators = spdxUniqueCreators(d.bom.CreationInfo.Creators, []spdx.Creator{tool}) 419 | } 420 | } 421 | if !creatorExists(d.bom.CreationInfo.Creators, sbomasmTool) { 422 | d.bom.CreationInfo.Creators = spdxUniqueCreators(d.bom.CreationInfo.Creators, []spdx.Creator{sbomasmTool}) 423 | } 424 | return nil 425 | } 426 | 427 | if d.c.onAppend() { 428 | d.bom.CreationInfo.Creators = spdxUniqueCreators(d.bom.CreationInfo.Creators, newTools) 429 | if !creatorExists(d.bom.CreationInfo.Creators, sbomasmTool) { 430 | d.bom.CreationInfo.Creators = spdxUniqueCreators(d.bom.CreationInfo.Creators, []spdx.Creator{sbomasmTool}) 431 | } 432 | return nil 433 | } 434 | 435 | d.bom.CreationInfo.Creators = spdxUniqueCreators(d.bom.CreationInfo.Creators, newTools) 436 | if !creatorExists(d.bom.CreationInfo.Creators, sbomasmTool) { 437 | d.bom.CreationInfo.Creators = spdxUniqueCreators(d.bom.CreationInfo.Creators, []spdx.Creator{sbomasmTool}) 438 | } 439 | 440 | return nil 441 | } 442 | 443 | // remove a creator by name 444 | func removeCreator(creators []spdx.Creator, creatorName string) []spdx.Creator { 445 | result := []spdx.Creator{} 446 | for _, c := range creators { 447 | if !strings.HasPrefix(c.Creator, creatorName) { 448 | result = append(result, c) 449 | } 450 | } 451 | return result 452 | } 453 | 454 | // ensure no duplicate creator 455 | func creatorExists(creators []spdx.Creator, creator spdx.Creator) bool { 456 | for _, c := range creators { 457 | if c.Creator == creator.Creator && c.CreatorType == creator.CreatorType { 458 | return true 459 | } 460 | } 461 | return false 462 | } 463 | 464 | // ensure unique creators 465 | func spdxUniqueCreators(existing, newCreators []spdx.Creator) []spdx.Creator { 466 | creatorSet := make(map[string]struct{}) 467 | for _, c := range existing { 468 | creatorSet[c.Creator] = struct{}{} 469 | } 470 | for _, c := range newCreators { 471 | if _, exists := creatorSet[c.Creator]; !exists { 472 | existing = append(existing, c) 473 | } 474 | } 475 | return existing 476 | } 477 | 478 | func (d *spdxEditDoc) copyright() error { 479 | if !d.c.shouldCopyRight() { 480 | return errNoConfiguration 481 | } 482 | 483 | if d.c.search.subject == "document" { 484 | return errNotSupported 485 | } 486 | 487 | if d.c.onMissing() { 488 | if d.pkg.PackageCopyrightText == "" { 489 | d.pkg.PackageCopyrightText = d.c.copyright 490 | } 491 | } else { 492 | d.pkg.PackageCopyrightText = d.c.copyright 493 | } 494 | 495 | return nil 496 | } 497 | 498 | func (d *spdxEditDoc) description() error { 499 | if !d.c.shouldDescription() { 500 | return errNoConfiguration 501 | } 502 | 503 | if d.c.onMissing() { 504 | if d.c.search.subject == "document" { 505 | if d.bom.DocumentComment == "" { 506 | d.bom.DocumentComment = d.c.description 507 | } 508 | } else { 509 | if d.pkg.PackageDescription == "" { 510 | d.pkg.PackageDescription = d.c.description 511 | } 512 | } 513 | } else { 514 | if d.c.search.subject == "document" { 515 | d.bom.DocumentComment = d.c.description 516 | } else { 517 | d.pkg.PackageDescription = d.c.description 518 | } 519 | } 520 | 521 | return nil 522 | } 523 | 524 | func (d *spdxEditDoc) repository() error { 525 | if !d.c.shouldRepository() { 526 | return errNoConfiguration 527 | } 528 | 529 | if d.c.search.subject == "document" { 530 | return errNotSupported 531 | } 532 | 533 | if d.c.onMissing() { 534 | if d.pkg.PackageDownloadLocation == "" { 535 | d.pkg.PackageDownloadLocation = d.c.repository 536 | } 537 | } else { 538 | d.pkg.PackageDownloadLocation = d.c.repository 539 | } 540 | 541 | return nil 542 | } 543 | 544 | func (d *spdxEditDoc) typ() error { 545 | if !d.c.shouldTyp() { 546 | return errNoConfiguration 547 | } 548 | 549 | if d.c.search.subject == "document" { 550 | return errNotSupported 551 | } 552 | 553 | purpose := spdx_strings_to_types[strings.ToLower(d.c.typ)] 554 | 555 | if purpose == "" { 556 | return errInvalidInput 557 | } 558 | 559 | if d.c.onMissing() { 560 | if d.pkg.PrimaryPackagePurpose == "" { 561 | d.pkg.PrimaryPackagePurpose = purpose 562 | } 563 | } else { 564 | d.pkg.PrimaryPackagePurpose = purpose 565 | } 566 | 567 | return nil 568 | } 569 | 570 | func (d *spdxEditDoc) timeStamp() error { 571 | if d.c.shouldTimeStamp() { 572 | return errNoConfiguration 573 | } 574 | 575 | if d.c.search.subject != "document" { 576 | return errNotSupported 577 | } 578 | 579 | if d.c.onMissing() { 580 | if d.bom.CreationInfo == nil { 581 | d.bom.CreationInfo = &spdx.CreationInfo{} 582 | } 583 | 584 | if d.bom.CreationInfo.Created == "" { 585 | d.bom.CreationInfo.Created = utcNowTime() 586 | } 587 | } else { 588 | if d.bom.CreationInfo == nil { 589 | d.bom.CreationInfo = &spdx.CreationInfo{} 590 | } 591 | 592 | d.bom.CreationInfo.Created = utcNowTime() 593 | } 594 | return nil 595 | } 596 | 597 | func (d *spdxEditDoc) lifeCycles() error { 598 | if !d.c.shouldLifeCycle() { 599 | return errNoConfiguration 600 | } 601 | 602 | if d.c.search.subject != "document" { 603 | return errNotSupported 604 | } 605 | 606 | lifecycles := fmt.Sprintf("lifecycle: %s", strings.Join(d.c.lifecycles, ",")) 607 | 608 | if d.c.onMissing() { 609 | if d.bom.CreationInfo == nil { 610 | d.bom.CreationInfo = &spdx.CreationInfo{} 611 | } 612 | if d.bom.CreationInfo.CreatorComment == "" { 613 | d.bom.CreationInfo.CreatorComment = lifecycles 614 | } 615 | } else { 616 | if d.bom.CreationInfo == nil { 617 | d.bom.CreationInfo = &spdx.CreationInfo{} 618 | } 619 | d.bom.CreationInfo.CreatorComment = lifecycles 620 | } 621 | return nil 622 | } 623 | -------------------------------------------------------------------------------- /pkg/edit/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package edit 18 | 19 | import ( 20 | "errors" 21 | "os" 22 | "time" 23 | 24 | "github.com/interlynk-io/sbomasm/pkg/detect" 25 | ) 26 | 27 | var errNoConfiguration = errors.New("no configuration provided") 28 | var errNotSupported = errors.New("not supported") 29 | var errInvalidInput = errors.New("invalid input data") 30 | 31 | func detectSbom(path string) (string, string, error) { 32 | f, err := os.Open(path) 33 | if err != nil { 34 | return "", "", err 35 | } 36 | defer f.Close() 37 | 38 | spec, format, err := detect.Detect(f) 39 | if err != nil { 40 | return "", "", err 41 | } 42 | return string(spec), string(format), nil 43 | } 44 | 45 | func utcNowTime() string { 46 | location, _ := time.LoadLocation("UTC") 47 | locationTime := time.Now().In(location) 48 | return locationTime.Format(time.RFC3339) 49 | } 50 | -------------------------------------------------------------------------------- /pkg/licenses/embed_licenses.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package licenses 18 | 19 | import ( 20 | "embed" 21 | "encoding/json" 22 | "fmt" 23 | "log" 24 | "strings" 25 | ) 26 | 27 | var ( 28 | //go:embed files 29 | res embed.FS 30 | 31 | licenses = map[string]string{ 32 | "spdx": "files/licenses_spdx.json", 33 | "spdxException": "files/licenses_spdx_exception.json", 34 | "aboutcode": "files/licenses_aboutcode.json", 35 | } 36 | ) 37 | 38 | type spdxLicense struct { 39 | Version string `json:"licenseListVersion"` 40 | Licenses []spdxLicenseDetail `json:"licenses"` 41 | Exceptions []spdxLicenseDetail `json:"exceptions"` 42 | } 43 | 44 | type spdxLicenseDetail struct { 45 | Reference string `json:"reference"` 46 | IsDeprecated bool `json:"isDeprecatedLicenseId"` 47 | DetailsURL string `json:"detailsUrl"` 48 | ReferenceNumber int `json:"referenceNumber"` 49 | Name string `json:"name"` 50 | LicenseID string `json:"licenseId"` 51 | LicenseExceptionID string `json:"licenseExceptionId"` 52 | SeeAlso []string `json:"seeAlso"` 53 | IsOsiApproved bool `json:"isOsiApproved"` 54 | IsFsfLibre bool `json:"isFsfLibre"` 55 | } 56 | 57 | type aboutCodeLicenseDetail struct { 58 | LicenseKey string `json:"license_key"` 59 | Category string `json:"category"` 60 | SpdxLicenseKey string `json:"spdx_license_key"` 61 | OtherSpdxLicenseKeys []string `json:"other_spdx_license_keys"` 62 | Exception bool `json:"is_exception"` 63 | Deprecated bool `json:"is_deprecated"` 64 | JSON string `json:"json"` 65 | Yaml string `json:"yaml"` 66 | HTML string `json:"html"` 67 | License string `json:"license"` 68 | } 69 | 70 | var ( 71 | licenseList = map[string]meta{} 72 | LicenseListAboutCode = map[string]meta{} 73 | ) 74 | 75 | func loadSpdxLicense() error { 76 | licData, err := res.ReadFile(licenses["spdx"]) 77 | if err != nil { 78 | fmt.Printf("error: %v\n", err) 79 | return err 80 | } 81 | 82 | var sl spdxLicense 83 | if err := json.Unmarshal(licData, &sl); err != nil { 84 | fmt.Printf("error: %v\n", err) 85 | return err 86 | } 87 | 88 | for _, l := range sl.Licenses { 89 | licenseList[l.LicenseID] = meta{ 90 | name: l.Name, 91 | short: l.LicenseID, 92 | deprecated: l.IsDeprecated, 93 | osiApproved: l.IsOsiApproved, 94 | fsfLibre: l.IsFsfLibre, 95 | restrictive: false, 96 | exception: false, 97 | freeAnyUse: false, 98 | source: "spdx", 99 | } 100 | } 101 | // fmt.Printf("loaded %d licenses\n", len(licenseList)) 102 | return nil 103 | } 104 | 105 | func loadSpdxExceptions() error { 106 | licData, err := res.ReadFile(licenses["spdxException"]) 107 | if err != nil { 108 | fmt.Printf("error: %v\n", err) 109 | return err 110 | } 111 | 112 | var sl spdxLicense 113 | if err := json.Unmarshal(licData, &sl); err != nil { 114 | fmt.Printf("error: %v\n", err) 115 | return err 116 | } 117 | 118 | for _, l := range sl.Exceptions { 119 | licenseList[l.LicenseExceptionID] = meta{ 120 | name: l.Name, 121 | short: l.LicenseExceptionID, 122 | deprecated: l.IsDeprecated, 123 | osiApproved: l.IsOsiApproved, 124 | fsfLibre: l.IsFsfLibre, 125 | restrictive: false, 126 | exception: true, 127 | freeAnyUse: false, 128 | source: "spdx", 129 | } 130 | } 131 | // fmt.Printf("loaded %d licenses\n", len(licenseList)) 132 | 133 | return nil 134 | } 135 | 136 | func loadAboutCodeLicense() error { 137 | licData, err := res.ReadFile(licenses["aboutcode"]) 138 | if err != nil { 139 | fmt.Printf("error: %v\n", err) 140 | return err 141 | } 142 | 143 | var acl []aboutCodeLicenseDetail 144 | 145 | if err := json.Unmarshal(licData, &acl); err != nil { 146 | fmt.Printf("error: %v\n", err) 147 | return err 148 | } 149 | 150 | isRestrictive := func(category string) bool { 151 | lowerCategory := strings.ToLower(category) 152 | 153 | if strings.Contains(lowerCategory, "copyleft") { 154 | return true 155 | } 156 | 157 | if strings.Contains(lowerCategory, "restricted") { 158 | return true 159 | } 160 | 161 | return false 162 | } 163 | 164 | isFreeAnyUse := func(category string) bool { 165 | lowerCategory := strings.ToLower(category) 166 | return strings.Contains(lowerCategory, "public") 167 | } 168 | 169 | for _, l := range acl { 170 | for _, otherKey := range l.OtherSpdxLicenseKeys { 171 | LicenseListAboutCode[otherKey] = meta{ 172 | name: l.LicenseKey, 173 | short: otherKey, 174 | deprecated: l.Deprecated, 175 | osiApproved: false, 176 | fsfLibre: false, 177 | restrictive: isRestrictive(l.Category), 178 | exception: l.Exception, 179 | freeAnyUse: isFreeAnyUse(l.Category), 180 | source: "aboutcode", 181 | } 182 | } 183 | 184 | LicenseListAboutCode[l.SpdxLicenseKey] = meta{ 185 | name: l.LicenseKey, 186 | short: l.SpdxLicenseKey, 187 | deprecated: l.Deprecated, 188 | osiApproved: false, 189 | fsfLibre: false, 190 | restrictive: isRestrictive(l.Category), 191 | exception: l.Exception, 192 | freeAnyUse: isFreeAnyUse(l.Category), 193 | source: "aboutcode", 194 | } 195 | } 196 | // fmt.Printf("loaded %d licenses\n", len(LicenseListAboutCode)) 197 | 198 | return nil 199 | } 200 | 201 | func init() { 202 | err := loadSpdxLicense() 203 | if err != nil { 204 | log.Printf("Failed to load spdx license: %v", err) 205 | } 206 | err = loadSpdxExceptions() 207 | if err != nil { 208 | log.Printf("Failed to load spdx exceptions: %v", err) 209 | } 210 | err = loadAboutCodeLicense() 211 | if err != nil { 212 | log.Printf("Failed to load about code license: %v", err) 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /pkg/licenses/license.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package licenses 16 | 17 | import ( 18 | "errors" 19 | "strings" 20 | 21 | go_spdx "github.com/github/go-spdx/v2/spdxexp" 22 | ) 23 | 24 | type License interface { 25 | Name() string 26 | ShortID() string 27 | Deprecated() bool 28 | OsiApproved() bool 29 | FsfLibre() bool 30 | FreeAnyUse() bool 31 | Restrictive() bool 32 | Exception() bool 33 | Source() string 34 | } 35 | 36 | type meta struct { 37 | name string 38 | short string 39 | deprecated bool 40 | osiApproved bool 41 | fsfLibre bool 42 | freeAnyUse bool 43 | restrictive bool 44 | exception bool 45 | source string 46 | } 47 | 48 | func (m meta) Name() string { 49 | return m.name 50 | } 51 | 52 | func (m meta) ShortID() string { 53 | return m.short 54 | } 55 | 56 | func (m meta) Deprecated() bool { 57 | return m.deprecated 58 | } 59 | 60 | func (m meta) OsiApproved() bool { 61 | return m.osiApproved 62 | } 63 | 64 | func (m meta) FsfLibre() bool { 65 | return m.fsfLibre 66 | } 67 | 68 | func (m meta) FreeAnyUse() bool { 69 | return m.freeAnyUse 70 | } 71 | 72 | func (m meta) Restrictive() bool { 73 | return m.restrictive 74 | } 75 | 76 | func (m meta) Exception() bool { 77 | return m.exception 78 | } 79 | 80 | func (m meta) Source() string { 81 | return m.source 82 | } 83 | 84 | func IsSpdxExpression(licenseKey string) bool { 85 | licenses, err := go_spdx.ExtractLicenses(licenseKey) 86 | if err != nil { 87 | return false 88 | } 89 | 90 | if len(licenses) <= 1 { 91 | return false 92 | } 93 | 94 | return true 95 | } 96 | 97 | func LookupSpdxLicense(licenseKey string) (License, error) { 98 | if licenseKey == "" { 99 | return nil, errors.New("license not found") 100 | } 101 | 102 | lowerKey := strings.ToLower(licenseKey) 103 | 104 | if lowerKey == "none" || lowerKey == "noassertion" { 105 | return nil, errors.New("license not found") 106 | } 107 | 108 | tLicKey := strings.TrimRight(licenseKey, "+") 109 | 110 | license, lok := licenseList[tLicKey] 111 | if lok { 112 | return license, nil 113 | } 114 | 115 | return nil, errors.New("license not found") 116 | } 117 | 118 | func LookupAdoutCodeLicense(licenseKey string) (License, error) { 119 | if licenseKey == "" { 120 | return nil, errors.New("license not found") 121 | } 122 | 123 | lowerKey := strings.ToLower(licenseKey) 124 | 125 | if lowerKey == "none" || lowerKey == "noassertion" { 126 | return nil, errors.New("license not found") 127 | } 128 | 129 | tLicKey := strings.TrimRight(licenseKey, "+") 130 | 131 | abouLicense, aok := LicenseListAboutCode[tLicKey] 132 | 133 | if aok { 134 | return abouLicense, nil 135 | } 136 | return nil, errors.New("license not found") 137 | } 138 | 139 | func LookupLicense(licenseKey string) (License, error) { 140 | spdxL, err := LookupSpdxLicense(licenseKey) 141 | abcL, err2 := LookupAdoutCodeLicense(licenseKey) 142 | 143 | if err != nil && err2 != nil { 144 | return nil, errors.New("license not found") 145 | } 146 | 147 | if err == nil { 148 | return spdxL, nil 149 | } 150 | 151 | if err2 == nil { 152 | return abcL, nil 153 | } 154 | 155 | return nil, errors.New("license not found") 156 | } 157 | 158 | func LookupExpression(expression string, customLicense []License) []License { 159 | customLookup := func(licenseKey string) (License, error) { 160 | if len(customLicense) == 0 { 161 | return nil, errors.New("license not found") 162 | } 163 | 164 | for _, l := range customLicense { 165 | if l.ShortID() == licenseKey { 166 | return l, nil 167 | } 168 | } 169 | return nil, errors.New("license not found") 170 | } 171 | 172 | if expression == "" || strings.ToLower(expression) == "none" || strings.ToLower(expression) == "noassertion" { 173 | return []License{} 174 | } 175 | 176 | licenses, err := go_spdx.ExtractLicenses(expression) 177 | if err != nil { 178 | return []License{CreateCustomLicense(expression, expression)} 179 | } 180 | 181 | ls := []License{} 182 | 183 | for _, l := range licenses { 184 | tLicKey := strings.TrimRight(l, "+") 185 | lic, err := LookupLicense(tLicKey) 186 | if err != nil { 187 | custLic, err2 := customLookup(tLicKey) 188 | if err2 != nil { 189 | ls = append(ls, CreateCustomLicense(tLicKey, tLicKey)) 190 | continue 191 | } 192 | ls = append(ls, custLic) 193 | } 194 | 195 | if lic != nil { 196 | ls = append(ls, lic) 197 | } 198 | } 199 | 200 | return ls 201 | } 202 | 203 | func CreateCustomLicense(id, name string) License { 204 | return meta{ 205 | name: name, 206 | short: id, 207 | deprecated: false, 208 | osiApproved: false, 209 | fsfLibre: false, 210 | freeAnyUse: false, 211 | restrictive: false, 212 | exception: false, 213 | source: "custom", 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /pkg/logger/log.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Interlynk.io 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package logger 18 | 19 | import ( 20 | "context" 21 | 22 | "go.uber.org/zap" 23 | ) 24 | 25 | var logger *zap.SugaredLogger 26 | 27 | type logKey struct{} 28 | 29 | func InitProdLogger() { 30 | l, _ := zap.NewProduction() 31 | //l, _ := zap.NewDevelopment() 32 | defer l.Sync() 33 | if logger != nil { 34 | panic("logger already initialized") 35 | } 36 | logger = l.Sugar() 37 | } 38 | 39 | func InitDebugLogger() { 40 | l, _ := zap.NewDevelopment() 41 | defer l.Sync() 42 | if logger != nil { 43 | panic("logger already initialized") 44 | } 45 | logger = l.Sugar() 46 | } 47 | 48 | func WithLogger(ctx context.Context) context.Context { 49 | return context.WithValue(ctx, logKey{}, logger) 50 | } 51 | 52 | func WithLoggerAndCancel(ctx context.Context) (context.Context, context.CancelFunc) { 53 | return context.WithCancel(context.WithValue(ctx, logKey{}, logger)) 54 | } 55 | 56 | func FromContext(ctx context.Context) *zap.SugaredLogger { 57 | if logger, ok := ctx.Value(logKey{}).(*zap.SugaredLogger); ok { 58 | return logger 59 | } 60 | 61 | return zap.NewNop().Sugar() 62 | } 63 | -------------------------------------------------------------------------------- /samples/spdx/issue-56/example6-bin.spdx: -------------------------------------------------------------------------------- 1 | SPDXVersion: SPDX-2.2 2 | DataLicense: CC0-1.0 3 | SPDXID: SPDXRef-DOCUMENT 4 | DocumentName: hello-go-bin 5 | DocumentNamespace: https://swinslow.net/spdx-examples/example6/hello-go-bin-v2 6 | ExternalDocumentRef:DocumentRef-hello-go-src https://swinslow.net/spdx-examples/example6/hello-go-src-v2 SHA1: b3018ddb18802a56b60ad839c98d279687b60bd6 7 | ExternalDocumentRef:DocumentRef-go-lib https://swinslow.net/spdx-examples/example6/go-lib-v2 SHA1: 58e4a6d5745f032b9788142e49edee1b508c7ac5 8 | Creator: Person: Steve Winslow (steve@swinslow.net) 9 | Creator: Tool: github.com/spdx/tools-golang/builder 10 | Creator: Tool: github.com/spdx/tools-golang/idsearcher 11 | Created: 2021-08-26T01:56:00Z 12 | 13 | ##### Package: hello-go-bin 14 | 15 | PackageName: hello-go-bin 16 | SPDXID: SPDXRef-Package-hello-go-bin 17 | PackageDownloadLocation: git+https://github.com/swinslow/spdx-examples.git#example6/content/build 18 | FilesAnalyzed: true 19 | PackageVerificationCode: 41acac4b846ee388cb6c1234f04489ccd5daa5a5 20 | PackageLicenseConcluded: GPL-3.0-or-later AND LicenseRef-Golang-BSD-plus-Patents 21 | PackageLicenseInfoFromFiles: NOASSERTION 22 | PackageLicenseDeclared: NOASSERTION 23 | PackageCopyrightText: NOASSERTION 24 | 25 | Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-Package-hello-go-bin 26 | 27 | FileName: ./hello 28 | SPDXID: SPDXRef-hello-go-binary 29 | FileChecksum: SHA1: 78ed46e8e6f86f19d3a6782979029be5f918235f 30 | FileChecksum: SHA256: 3d51cb6c9a38d437e8ee20a1902a15875ea1d3771a215622e14739532be14949 31 | FileChecksum: MD5: 9ec63d68bdceb2922548e3faa377e7d0 32 | LicenseConcluded: GPL-3.0-or-later AND LicenseRef-Golang-BSD-plus-Patents 33 | LicenseInfoInFile: NOASSERTION 34 | FileCopyrightText: NOASSERTION 35 | 36 | ##### Relationships 37 | 38 | Relationship: SPDXRef-hello-go-binary GENERATED_FROM DocumentRef-hello-go-src:SPDXRef-hello-go-src 39 | Relationship: SPDXRef-hello-go-binary GENERATED_FROM DocumentRef-hello-go-src:SPDXRef-Makefile 40 | 41 | Relationship: DocumentRef-go-lib:SPDXRef-Package-go-compiler BUILD_TOOL_OF SPDXRef-Package-hello-go-bin 42 | 43 | Relationship: DocumentRef-go-lib:SPDXRef-Package-go.fmt RUNTIME_DEPENDENCY_OF SPDXRef-Package-hello-go-bin 44 | Relationship: DocumentRef-go-lib:SPDXRef-Package-go.fmt STATIC_LINK SPDXRef-Package-hello-go-bin 45 | 46 | Relationship: DocumentRef-go-lib:SPDXRef-Package-go.reflect STATIC_LINK SPDXRef-Package-hello-go-bin 47 | Relationship: DocumentRef-go-lib:SPDXRef-Package-go.strconv STATIC_LINK SPDXRef-Package-hello-go-bin 48 | 49 | ##### Non-standard license 50 | 51 | LicenseID: LicenseRef-Golang-BSD-plus-Patents 52 | ExtractedText: 53 | Copyright (c) 2009 The Go Authors. All rights reserved. 54 | 55 | Redistribution and use in source and binary forms, with or without 56 | modification, are permitted provided that the following conditions are 57 | met: 58 | 59 | * Redistributions of source code must retain the above copyright 60 | notice, this list of conditions and the following disclaimer. 61 | * Redistributions in binary form must reproduce the above 62 | copyright notice, this list of conditions and the following disclaimer 63 | in the documentation and/or other materials provided with the 64 | distribution. 65 | * Neither the name of Google Inc. nor the names of its 66 | contributors may be used to endorse or promote products derived from 67 | this software without specific prior written permission. 68 | 69 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 70 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 71 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 72 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 73 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 74 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 75 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 76 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 77 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 78 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 79 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 80 | 81 | Additional IP Rights Grant (Patents) 82 | 83 | "This implementation" means the copyrightable works distributed by 84 | Google as part of the Go project. 85 | 86 | Google hereby grants to You a perpetual, worldwide, non-exclusive, 87 | no-charge, royalty-free, irrevocable (except as stated in this section) 88 | patent license to make, have made, use, offer to sell, sell, import, 89 | transfer and otherwise run, modify and propagate the contents of this 90 | implementation of Go, where such license applies only to those patent 91 | claims, both currently owned or controlled by Google and acquired in 92 | the future, licensable by Google that are necessarily infringed by this 93 | implementation of Go. This grant does not include claims that would be 94 | infringed only as a consequence of further modification of this 95 | implementation. If you or your agent or exclusive licensee institute or 96 | order or agree to the institution of patent litigation against any 97 | entity (including a cross-claim or counterclaim in a lawsuit) alleging 98 | that this implementation of Go or any code incorporated within this 99 | implementation of Go constitutes direct or contributory patent 100 | infringement, or inducement of patent infringement, then any patent 101 | rights granted to you under this License for this implementation of Go 102 | shall terminate as of the date such litigation is filed. 103 | LicenseName: Golang BSD-plus-PATENTS 104 | LicenseCrossReference: https://github.com/golang/go/blob/master/LICENSE 105 | LicenseCrossReference: https://github.com/golang/go/blob/master/PATENTS 106 | LicenseComment: The Golang license text is split across two files, with the BSD-3-Clause content in LICENSE and the Additional IP Rights Grant in PATENTS. 107 | -------------------------------------------------------------------------------- /samples/spdx/issue-56/example6-lib.spdx: -------------------------------------------------------------------------------- 1 | SPDXVersion: SPDX-2.2 2 | DataLicense: CC0-1.0 3 | SPDXID: SPDXRef-DOCUMENT 4 | DocumentName: go-lib 5 | DocumentNamespace: https://swinslow.net/spdx-examples/example6/go-lib-v2 6 | Creator: Person: Steve Winslow (steve@swinslow.net) 7 | Created: 2021-08-26T01:55:00Z 8 | 9 | ##### Package representing the Go distribution 10 | 11 | PackageName: go-1.15 12 | SPDXID: SPDXRef-Package-godist 13 | PackageFileName: go_6715.snap 14 | PackageVersion: 1.15.4 15 | PackageSupplier: Organization: Canonical Ltd. 16 | PackageOriginator: Organization: Google LLC 17 | PackageDownloadLocation: NOASSERTION 18 | FilesAnalyzed: false 19 | PackageChecksum: SHA256: 0d6e1420facd978e532eae7bd5cb6378b65522c12fa9dcf682129e698c34d1b2 20 | PackageHomePage: https://golang.org/ 21 | PackageSourceInfo: installed using Ubuntu snap file, see https://snapcraft.io/go 22 | PackageLicenseConcluded: NOASSERTION 23 | PackageLicenseDeclared: LicenseRef-Golang-BSD-plus-Patents 24 | PackageCopyrightText: Copyright (c) 2009 The Go Authors. All rights reserved. 25 | PackageSummary: Ubuntu snap distribution of Golang v1.15.4 linux/amd64 26 | 27 | Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-Package-godist 28 | 29 | ##### Package representing the Go compiler 30 | 31 | PackageName: go 32 | SPDXID: SPDXRef-Package-go-compiler 33 | PackageFileName: go 34 | PackageVersion: 1.15.4 35 | PackageDownloadLocation: NOASSERTION 36 | FilesAnalyzed: false 37 | PackageChecksum: SHA256: 4448b9e62b3c364a073808e22643985d9f49c4ad44aa11d8e4a17c49600a7c3a 38 | PackageLicenseConcluded: NOASSERTION 39 | PackageLicenseDeclared: NOASSERTION 40 | PackageCopyrightText: NOASSERTION 41 | 42 | Relationship: SPDXRef-Package-godist CONTAINS SPDXRef-Package-go-compiler 43 | 44 | ##### Packages representing Go standard library packages 45 | 46 | PackageName: go.fmt 47 | SPDXID: SPDXRef-Package-go.fmt 48 | PackageVersion: 1.15.4 49 | PackageDownloadLocation: NOASSERTION 50 | FilesAnalyzed: false 51 | PackageLicenseConcluded: NOASSERTION 52 | PackageLicenseDeclared: NOASSERTION 53 | PackageCopyrightText: NOASSERTION 54 | PackageComment: This represents the fmt standard library, not the gofmt command. 55 | 56 | Relationship: SPDXRef-Package-godist CONTAINS SPDXRef-Package-go.fmt 57 | 58 | PackageName: go.reflect 59 | SPDXID: SPDXRef-Package-go.reflect 60 | PackageVersion: 1.15.4 61 | PackageDownloadLocation: NOASSERTION 62 | FilesAnalyzed: false 63 | PackageLicenseConcluded: NOASSERTION 64 | PackageLicenseDeclared: NOASSERTION 65 | PackageCopyrightText: NOASSERTION 66 | 67 | Relationship: SPDXRef-Package-godist CONTAINS SPDXRef-Package-go.reflect 68 | 69 | PackageName: go.strconv 70 | SPDXID: SPDXRef-Package-go.strconv 71 | PackageVersion: 1.15.4 72 | PackageDownloadLocation: NOASSERTION 73 | FilesAnalyzed: false 74 | PackageLicenseConcluded: NOASSERTION 75 | PackageLicenseDeclared: NOASSERTION 76 | PackageCopyrightText: NOASSERTION 77 | 78 | Relationship: SPDXRef-Package-godist CONTAINS SPDXRef-Package-go.strconv 79 | 80 | ##### Non-standard license 81 | 82 | LicenseID: LicenseRef-Golang-BSD-plus-Patents 83 | ExtractedText: 84 | Copyright (c) 2009 The Go Authors. All rights reserved. 85 | 86 | Redistribution and use in source and binary forms, with or without 87 | modification, are permitted provided that the following conditions are 88 | met: 89 | 90 | * Redistributions of source code must retain the above copyright 91 | notice, this list of conditions and the following disclaimer. 92 | * Redistributions in binary form must reproduce the above 93 | copyright notice, this list of conditions and the following disclaimer 94 | in the documentation and/or other materials provided with the 95 | distribution. 96 | * Neither the name of Google Inc. nor the names of its 97 | contributors may be used to endorse or promote products derived from 98 | this software without specific prior written permission. 99 | 100 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 101 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 102 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 103 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 104 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 105 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 106 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 107 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 108 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 109 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 110 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 111 | 112 | Additional IP Rights Grant (Patents) 113 | 114 | "This implementation" means the copyrightable works distributed by 115 | Google as part of the Go project. 116 | 117 | Google hereby grants to You a perpetual, worldwide, non-exclusive, 118 | no-charge, royalty-free, irrevocable (except as stated in this section) 119 | patent license to make, have made, use, offer to sell, sell, import, 120 | transfer and otherwise run, modify and propagate the contents of this 121 | implementation of Go, where such license applies only to those patent 122 | claims, both currently owned or controlled by Google and acquired in 123 | the future, licensable by Google that are necessarily infringed by this 124 | implementation of Go. This grant does not include claims that would be 125 | infringed only as a consequence of further modification of this 126 | implementation. If you or your agent or exclusive licensee institute or 127 | order or agree to the institution of patent litigation against any 128 | entity (including a cross-claim or counterclaim in a lawsuit) alleging 129 | that this implementation of Go or any code incorporated within this 130 | implementation of Go constitutes direct or contributory patent 131 | infringement, or inducement of patent infringement, then any patent 132 | rights granted to you under this License for this implementation of Go 133 | shall terminate as of the date such litigation is filed. 134 | LicenseName: Golang BSD-plus-PATENTS 135 | LicenseCrossReference: https://github.com/golang/go/blob/master/LICENSE 136 | LicenseCrossReference: https://github.com/golang/go/blob/master/PATENTS 137 | LicenseComment: The Golang license text is split across two files, with the BSD-3-Clause content in LICENSE and the Additional IP Rights Grant in PATENTS. 138 | -------------------------------------------------------------------------------- /samples/spdx/issue-56/example6-src.spdx: -------------------------------------------------------------------------------- 1 | SPDXVersion: SPDX-2.2 2 | DataLicense: CC0-1.0 3 | SPDXID: SPDXRef-DOCUMENT 4 | DocumentName: hello-go-src 5 | DocumentNamespace: https://swinslow.net/spdx-examples/example6/hello-go-src-v2 6 | Creator: Person: Steve Winslow (steve@swinslow.net) 7 | Creator: Tool: github.com/spdx/tools-golang/builder 8 | Creator: Tool: github.com/spdx/tools-golang/idsearcher 9 | Created: 2021-08-26T01:55:30Z 10 | 11 | ##### Package: hello-go-src 12 | 13 | PackageName: hello-go-src 14 | SPDXID: SPDXRef-Package-hello-go-src 15 | PackageDownloadLocation: git+https://github.com/swinslow/spdx-examples.git#example6/content/src 16 | FilesAnalyzed: true 17 | PackageVerificationCode: 6486e016b01e9ec8a76998cefd0705144d869234 18 | PackageLicenseConcluded: NOASSERTION 19 | PackageLicenseInfoFromFiles: GPL-3.0-or-later 20 | PackageLicenseDeclared: GPL-3.0-or-later 21 | PackageCopyrightText: NOASSERTION 22 | 23 | Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-Package-hello-go-src 24 | 25 | FileName: ./Makefile 26 | SPDXID: SPDXRef-Makefile 27 | FileChecksum: SHA1: 5cb1c1c76bd0694fe5be2774c7df8166f52498a0 28 | FileChecksum: SHA256: 23ffc10f988297282e29b32e9c520fd33b4122a487ccaa74c979d225181aa8bf 29 | FileChecksum: MD5: 7c1236d86a868a5762ba16274339c0f8 30 | LicenseConcluded: GPL-3.0-or-later 31 | LicenseInfoInFile: GPL-3.0-or-later 32 | FileCopyrightText: NOASSERTION 33 | 34 | FileName: ./hello.go 35 | SPDXID: SPDXRef-hello-go-src 36 | FileChecksum: SHA1: bb5ae27c76cd4332edd0da834eb4bd8a7c31ca93 37 | FileChecksum: SHA256: 1ce078bb915470348fcf481198b8ab1cdb7d36481564959387153e8d4cd1bbf2 38 | FileChecksum: MD5: 7f4170f33ec5c81492785e1147dfd3af 39 | LicenseConcluded: GPL-3.0-or-later 40 | LicenseInfoInFile: GPL-3.0-or-later 41 | FileCopyrightText: NOASSERTION 42 | 43 | ##### Relationships 44 | 45 | Relationship: SPDXRef-Makefile BUILD_TOOL_OF SPDXRef-Package-hello-go-src 46 | 47 | -------------------------------------------------------------------------------- /samples/spdx/issue-67/example6-bin.spdx: -------------------------------------------------------------------------------- 1 | SPDXVersion: SPDX-2.2 2 | DataLicense: CC0-1.0 3 | SPDXID: SPDXRef-DOCUMENT 4 | DocumentName: hello-go-bin 5 | DocumentNamespace: https://swinslow.net/spdx-examples/example6/hello-go-bin-v2 6 | ExternalDocumentRef:DocumentRef-hello-go-src https://swinslow.net/spdx-examples/example6/hello-go-src-v2 SHA1: b3018ddb18802a56b60ad839c98d279687b60bd6 7 | ExternalDocumentRef:DocumentRef-go-lib https://swinslow.net/spdx-examples/example6/go-lib-v2 SHA1: 58e4a6d5745f032b9788142e49edee1b508c7ac5 8 | Creator: Person: Steve Winslow (steve@swinslow.net) 9 | Creator: Tool: github.com/spdx/tools-golang/builder 10 | Creator: Tool: github.com/spdx/tools-golang/idsearcher 11 | Created: 2021-08-26T01:56:00Z 12 | LicenseListVersion: 3.18 13 | 14 | ##### Package: hello-go-bin 15 | 16 | PackageName: hello-go-bin 17 | SPDXID: SPDXRef-Package-hello-go-bin 18 | PackageDownloadLocation: git+https://github.com/swinslow/spdx-examples.git#example6/content/build 19 | FilesAnalyzed: true 20 | PackageVerificationCode: 41acac4b846ee388cb6c1234f04489ccd5daa5a5 21 | PackageLicenseConcluded: GPL-3.0-or-later AND LicenseRef-Golang-BSD-plus-Patents 22 | PackageLicenseInfoFromFiles: NOASSERTION 23 | PackageLicenseDeclared: NOASSERTION 24 | PackageCopyrightText: NOASSERTION 25 | 26 | Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-Package-hello-go-bin 27 | 28 | FileName: ./hello 29 | SPDXID: SPDXRef-hello-go-binary 30 | FileChecksum: SHA1: 78ed46e8e6f86f19d3a6782979029be5f918235f 31 | FileChecksum: SHA256: 3d51cb6c9a38d437e8ee20a1902a15875ea1d3771a215622e14739532be14949 32 | FileChecksum: MD5: 9ec63d68bdceb2922548e3faa377e7d0 33 | LicenseConcluded: GPL-3.0-or-later AND LicenseRef-Golang-BSD-plus-Patents 34 | LicenseInfoInFile: NOASSERTION 35 | FileCopyrightText: NOASSERTION 36 | 37 | ##### Relationships 38 | 39 | Relationship: SPDXRef-hello-go-binary GENERATED_FROM DocumentRef-hello-go-src:SPDXRef-hello-go-src 40 | Relationship: SPDXRef-hello-go-binary GENERATED_FROM DocumentRef-hello-go-src:SPDXRef-Makefile 41 | 42 | Relationship: DocumentRef-go-lib:SPDXRef-Package-go-compiler BUILD_TOOL_OF SPDXRef-Package-hello-go-bin 43 | 44 | Relationship: DocumentRef-go-lib:SPDXRef-Package-go.fmt RUNTIME_DEPENDENCY_OF SPDXRef-Package-hello-go-bin 45 | Relationship: DocumentRef-go-lib:SPDXRef-Package-go.fmt STATIC_LINK SPDXRef-Package-hello-go-bin 46 | 47 | Relationship: DocumentRef-go-lib:SPDXRef-Package-go.reflect STATIC_LINK SPDXRef-Package-hello-go-bin 48 | Relationship: DocumentRef-go-lib:SPDXRef-Package-go.strconv STATIC_LINK SPDXRef-Package-hello-go-bin 49 | 50 | ##### Non-standard license 51 | 52 | LicenseID: LicenseRef-Golang-BSD-plus-Patents 53 | ExtractedText: 54 | Copyright (c) 2009 The Go Authors. All rights reserved. 55 | 56 | Redistribution and use in source and binary forms, with or without 57 | modification, are permitted provided that the following conditions are 58 | met: 59 | 60 | * Redistributions of source code must retain the above copyright 61 | notice, this list of conditions and the following disclaimer. 62 | * Redistributions in binary form must reproduce the above 63 | copyright notice, this list of conditions and the following disclaimer 64 | in the documentation and/or other materials provided with the 65 | distribution. 66 | * Neither the name of Google Inc. nor the names of its 67 | contributors may be used to endorse or promote products derived from 68 | this software without specific prior written permission. 69 | 70 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 71 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 72 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 73 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 74 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 75 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 76 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 77 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 78 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 79 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 80 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 81 | 82 | Additional IP Rights Grant (Patents) 83 | 84 | "This implementation" means the copyrightable works distributed by 85 | Google as part of the Go project. 86 | 87 | Google hereby grants to You a perpetual, worldwide, non-exclusive, 88 | no-charge, royalty-free, irrevocable (except as stated in this section) 89 | patent license to make, have made, use, offer to sell, sell, import, 90 | transfer and otherwise run, modify and propagate the contents of this 91 | implementation of Go, where such license applies only to those patent 92 | claims, both currently owned or controlled by Google and acquired in 93 | the future, licensable by Google that are necessarily infringed by this 94 | implementation of Go. This grant does not include claims that would be 95 | infringed only as a consequence of further modification of this 96 | implementation. If you or your agent or exclusive licensee institute or 97 | order or agree to the institution of patent litigation against any 98 | entity (including a cross-claim or counterclaim in a lawsuit) alleging 99 | that this implementation of Go or any code incorporated within this 100 | implementation of Go constitutes direct or contributory patent 101 | infringement, or inducement of patent infringement, then any patent 102 | rights granted to you under this License for this implementation of Go 103 | shall terminate as of the date such litigation is filed. 104 | LicenseName: Golang BSD-plus-PATENTS 105 | LicenseCrossReference: https://github.com/golang/go/blob/master/LICENSE 106 | LicenseCrossReference: https://github.com/golang/go/blob/master/PATENTS 107 | LicenseComment: The Golang license text is split across two files, with the BSD-3-Clause content in LICENSE and the Additional IP Rights Grant in PATENTS. 108 | -------------------------------------------------------------------------------- /samples/spdx/issue-67/example6-lib.spdx: -------------------------------------------------------------------------------- 1 | SPDXVersion: SPDX-2.2 2 | DataLicense: CC0-1.0 3 | SPDXID: SPDXRef-DOCUMENT 4 | DocumentName: go-lib 5 | DocumentNamespace: https://swinslow.net/spdx-examples/example6/go-lib-v2 6 | Creator: Person: Steve Winslow (steve@swinslow.net) 7 | Created: 2021-08-26T01:55:00Z 8 | LicenseListVersion: 3.20 9 | 10 | ##### Package representing the Go distribution 11 | 12 | PackageName: go-1.15 13 | SPDXID: SPDXRef-Package-godist 14 | PackageFileName: go_6715.snap 15 | PackageVersion: 1.15.4 16 | PackageSupplier: Organization: Canonical Ltd. 17 | PackageOriginator: Organization: Google LLC 18 | PackageDownloadLocation: NOASSERTION 19 | FilesAnalyzed: false 20 | PackageChecksum: SHA256: 0d6e1420facd978e532eae7bd5cb6378b65522c12fa9dcf682129e698c34d1b2 21 | PackageHomePage: https://golang.org/ 22 | PackageSourceInfo: installed using Ubuntu snap file, see https://snapcraft.io/go 23 | PackageLicenseConcluded: NOASSERTION 24 | PackageLicenseDeclared: LicenseRef-Golang-BSD-plus-Patents 25 | PackageCopyrightText: Copyright (c) 2009 The Go Authors. All rights reserved. 26 | PackageSummary: Ubuntu snap distribution of Golang v1.15.4 linux/amd64 27 | 28 | Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-Package-godist 29 | 30 | ##### Package representing the Go compiler 31 | 32 | PackageName: go 33 | SPDXID: SPDXRef-Package-go-compiler 34 | PackageFileName: go 35 | PackageVersion: 1.15.4 36 | PackageDownloadLocation: NOASSERTION 37 | FilesAnalyzed: false 38 | PackageChecksum: SHA256: 4448b9e62b3c364a073808e22643985d9f49c4ad44aa11d8e4a17c49600a7c3a 39 | PackageLicenseConcluded: NOASSERTION 40 | PackageLicenseDeclared: NOASSERTION 41 | PackageCopyrightText: NOASSERTION 42 | 43 | Relationship: SPDXRef-Package-godist CONTAINS SPDXRef-Package-go-compiler 44 | 45 | ##### Packages representing Go standard library packages 46 | 47 | PackageName: go.fmt 48 | SPDXID: SPDXRef-Package-go.fmt 49 | PackageVersion: 1.15.4 50 | PackageDownloadLocation: NOASSERTION 51 | FilesAnalyzed: false 52 | PackageLicenseConcluded: NOASSERTION 53 | PackageLicenseDeclared: NOASSERTION 54 | PackageCopyrightText: NOASSERTION 55 | PackageComment: This represents the fmt standard library, not the gofmt command. 56 | 57 | Relationship: SPDXRef-Package-godist CONTAINS SPDXRef-Package-go.fmt 58 | 59 | PackageName: go.reflect 60 | SPDXID: SPDXRef-Package-go.reflect 61 | PackageVersion: 1.15.4 62 | PackageDownloadLocation: NOASSERTION 63 | FilesAnalyzed: false 64 | PackageLicenseConcluded: NOASSERTION 65 | PackageLicenseDeclared: NOASSERTION 66 | PackageCopyrightText: NOASSERTION 67 | 68 | Relationship: SPDXRef-Package-godist CONTAINS SPDXRef-Package-go.reflect 69 | 70 | PackageName: go.strconv 71 | SPDXID: SPDXRef-Package-go.strconv 72 | PackageVersion: 1.15.4 73 | PackageDownloadLocation: NOASSERTION 74 | FilesAnalyzed: false 75 | PackageLicenseConcluded: NOASSERTION 76 | PackageLicenseDeclared: NOASSERTION 77 | PackageCopyrightText: NOASSERTION 78 | 79 | Relationship: SPDXRef-Package-godist CONTAINS SPDXRef-Package-go.strconv 80 | 81 | ##### Non-standard license 82 | 83 | LicenseID: LicenseRef-Golang-BSD-plus-Patents 84 | ExtractedText: 85 | Copyright (c) 2009 The Go Authors. All rights reserved. 86 | 87 | Redistribution and use in source and binary forms, with or without 88 | modification, are permitted provided that the following conditions are 89 | met: 90 | 91 | * Redistributions of source code must retain the above copyright 92 | notice, this list of conditions and the following disclaimer. 93 | * Redistributions in binary form must reproduce the above 94 | copyright notice, this list of conditions and the following disclaimer 95 | in the documentation and/or other materials provided with the 96 | distribution. 97 | * Neither the name of Google Inc. nor the names of its 98 | contributors may be used to endorse or promote products derived from 99 | this software without specific prior written permission. 100 | 101 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 102 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 103 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 104 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 105 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 106 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 107 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 108 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 109 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 110 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 111 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 112 | 113 | Additional IP Rights Grant (Patents) 114 | 115 | "This implementation" means the copyrightable works distributed by 116 | Google as part of the Go project. 117 | 118 | Google hereby grants to You a perpetual, worldwide, non-exclusive, 119 | no-charge, royalty-free, irrevocable (except as stated in this section) 120 | patent license to make, have made, use, offer to sell, sell, import, 121 | transfer and otherwise run, modify and propagate the contents of this 122 | implementation of Go, where such license applies only to those patent 123 | claims, both currently owned or controlled by Google and acquired in 124 | the future, licensable by Google that are necessarily infringed by this 125 | implementation of Go. This grant does not include claims that would be 126 | infringed only as a consequence of further modification of this 127 | implementation. If you or your agent or exclusive licensee institute or 128 | order or agree to the institution of patent litigation against any 129 | entity (including a cross-claim or counterclaim in a lawsuit) alleging 130 | that this implementation of Go or any code incorporated within this 131 | implementation of Go constitutes direct or contributory patent 132 | infringement, or inducement of patent infringement, then any patent 133 | rights granted to you under this License for this implementation of Go 134 | shall terminate as of the date such litigation is filed. 135 | LicenseName: Golang BSD-plus-PATENTS 136 | LicenseCrossReference: https://github.com/golang/go/blob/master/LICENSE 137 | LicenseCrossReference: https://github.com/golang/go/blob/master/PATENTS 138 | LicenseComment: The Golang license text is split across two files, with the BSD-3-Clause content in LICENSE and the Additional IP Rights Grant in PATENTS. 139 | -------------------------------------------------------------------------------- /samples/spdx/issue-67/example6-src.spdx: -------------------------------------------------------------------------------- 1 | SPDXVersion: SPDX-2.2 2 | DataLicense: CC0-1.0 3 | SPDXID: SPDXRef-DOCUMENT 4 | DocumentName: hello-go-src 5 | DocumentNamespace: https://swinslow.net/spdx-examples/example6/hello-go-src-v2 6 | Creator: Person: Steve Winslow (steve@swinslow.net) 7 | Creator: Tool: github.com/spdx/tools-golang/builder 8 | Creator: Tool: github.com/spdx/tools-golang/idsearcher 9 | Created: 2021-08-26T01:55:30Z 10 | LicenseListVersion: 3.21 11 | 12 | ##### Package: hello-go-src 13 | 14 | PackageName: hello-go-src 15 | SPDXID: SPDXRef-Package-hello-go-src 16 | PackageDownloadLocation: git+https://github.com/swinslow/spdx-examples.git#example6/content/src 17 | FilesAnalyzed: true 18 | PackageVerificationCode: 6486e016b01e9ec8a76998cefd0705144d869234 19 | PackageLicenseConcluded: NOASSERTION 20 | PackageLicenseInfoFromFiles: GPL-3.0-or-later 21 | PackageLicenseDeclared: GPL-3.0-or-later 22 | PackageCopyrightText: NOASSERTION 23 | 24 | Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-Package-hello-go-src 25 | 26 | FileName: ./Makefile 27 | SPDXID: SPDXRef-Makefile 28 | FileChecksum: SHA1: 5cb1c1c76bd0694fe5be2774c7df8166f52498a0 29 | FileChecksum: SHA256: 23ffc10f988297282e29b32e9c520fd33b4122a487ccaa74c979d225181aa8bf 30 | FileChecksum: MD5: 7c1236d86a868a5762ba16274339c0f8 31 | LicenseConcluded: GPL-3.0-or-later 32 | LicenseInfoInFile: GPL-3.0-or-later 33 | FileCopyrightText: NOASSERTION 34 | 35 | FileName: ./hello.go 36 | SPDXID: SPDXRef-hello-go-src 37 | FileChecksum: SHA1: bb5ae27c76cd4332edd0da834eb4bd8a7c31ca93 38 | FileChecksum: SHA256: 1ce078bb915470348fcf481198b8ab1cdb7d36481564959387153e8d4cd1bbf2 39 | FileChecksum: MD5: 7f4170f33ec5c81492785e1147dfd3af 40 | LicenseConcluded: GPL-3.0-or-later 41 | LicenseInfoInFile: GPL-3.0-or-later 42 | FileCopyrightText: NOASSERTION 43 | 44 | ##### Relationships 45 | 46 | Relationship: SPDXRef-Makefile BUILD_TOOL_OF SPDXRef-Package-hello-go-src 47 | 48 | -------------------------------------------------------------------------------- /samples/spdx/issue-76/duplicate1.spdx: -------------------------------------------------------------------------------- 1 | SPDXVersion: SPDX-2.2 2 | DataLicense: CC0-1.0 3 | SPDXID: SPDXRef-DOCUMENT 4 | DocumentName: hello-go-bin 5 | DocumentNamespace: https://swinslow.net/spdx-examples/example6/hello-go-bin-v2 6 | Creator: Person: Steve Winslow (steve@swinslow.net) 7 | Creator: Tool: github.com/spdx/tools-golang/builder 8 | Created: 2021-08-26T01:56:00Z 9 | 10 | ##### Package: hello-go-bin 11 | 12 | PackageName: hello-go-bin 13 | SPDXID: SPDXRef-Package-hello-go-bin 14 | PackageDownloadLocation: git+https://github.com/swinslow/spdx-examples.git#example6/content/build 15 | FilesAnalyzed: true 16 | PackageVerificationCode: 41acac4b846ee388cb6c1234f04489ccd5daa5a5 17 | PackageLicenseConcluded: GPL-3.0-or-later 18 | PackageLicenseInfoFromFiles: NOASSERTION 19 | PackageLicenseDeclared: NOASSERTION 20 | PackageCopyrightText: NOASSERTION 21 | 22 | Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-Package-hello-go-bin 23 | 24 | Relationship: SPDXRef-Package-hello-go-bin CONTAINS SPDXRef-Package-crypto-js 25 | 26 | ##### Package: crypto-js 27 | 28 | PackageName: crypto-js 29 | SPDXID: SPDXRef-Package-crypto-js 30 | PackageSupplier: Organization: ACME 31 | PackageVersion: 4.1.1 32 | PackageDownloadLocation: https://github.com/brix/crypto-js/archive/refs/tags/4.1.1.zip 33 | FilesAnalyzed: false 34 | PackageLicenseConcluded: MIT 35 | PackageLicenseDeclared: MIT 36 | PackageCopyrightText: NOASSERTION 37 | PackageSummary: 38 | -------------------------------------------------------------------------------- /samples/spdx/issue-76/duplicate2.spdx: -------------------------------------------------------------------------------- 1 | SPDXVersion: SPDX-2.2 2 | DataLicense: CC0-1.0 3 | SPDXID: SPDXRef-DOCUMENT 4 | DocumentName: hello-go-src 5 | DocumentNamespace: https://swinslow.net/spdx-examples/example6/hello-go-src-v2 6 | Creator: Person: Steve Winslow (steve@swinslow.net) 7 | Creator: Tool: github.com/spdx/tools-golang/builder 8 | Created: 2021-08-26T01:55:30Z 9 | 10 | ##### Package: hello-go-src 11 | 12 | PackageName: hello-go-src 13 | SPDXID: SPDXRef-Package-hello-go-src 14 | PackageDownloadLocation: git+https://github.com/swinslow/spdx-examples.git#example6/content/src 15 | FilesAnalyzed: true 16 | PackageVerificationCode: 6486e016b01e9ec8a76998cefd0705144d869234 17 | PackageLicenseConcluded: NOASSERTION 18 | PackageLicenseInfoFromFiles: GPL-3.0-or-later 19 | PackageLicenseDeclared: GPL-3.0-or-later 20 | PackageCopyrightText: NOASSERTION 21 | 22 | Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-Package-hello-go-src 23 | 24 | Relationship: SPDXRef-Package-hello-go-src CONTAINS SPDXRef-Package-crypto-js 25 | 26 | ##### Package: crypto-js 27 | 28 | PackageName: crypto-js 29 | SPDXID: SPDXRef-Package-crypto-js 30 | PackageSupplier: Organization: ACME 31 | PackageVersion: 4.1.1 32 | PackageDownloadLocation: https://github.com/brix/crypto-js/archive/refs/tags/4.1.1.zip 33 | FilesAnalyzed: false 34 | PackageLicenseConcluded: MIT 35 | PackageLicenseDeclared: MIT 36 | PackageCopyrightText: NOASSERTION 37 | PackageSummary: 38 | -------------------------------------------------------------------------------- /samples/spdx/issue-77/main.spdx: -------------------------------------------------------------------------------- 1 | SPDXVersion: SPDX-2.2 2 | DataLicense: CC0-1.0 3 | SPDXID: SPDXRef-DOCUMENT 4 | DocumentName: main 5 | DocumentNamespace: https://example.com/spdx/main 6 | ExternalDocumentRef: DocumentRef-other https://example.com/spdx/other SHA1: 084de3898580bbf1bae9c66d347c25f9a33b1a5e 7 | Creator: Tool: Compliance Tool 8 | Created: 2024-06-03T16:53:26Z 9 | 10 | #### Relationships 11 | 12 | Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-Package-main 13 | 14 | Relationship: SPDXRef-Package-main CONTAINS DocumentRef-other:SPDXRef-Package-other 15 | Relationship: SPDXRef-Package-main CONTAINS SPDXRef-Package-rtty 16 | 17 | ##### Package: main 18 | 19 | PackageName: main 20 | SPDXID: SPDXRef-Package-main 21 | PackageSupplier: Organization: ACME 22 | PackageDownloadLocation: NOASSERTION 23 | FilesAnalyzed: false 24 | PackageLicenseConcluded: LGPL-2.1-only 25 | PackageLicenseDeclared: LGPL-2.1-only 26 | PackageCopyrightText: NOASSERTION 27 | 28 | ##### Package: rtty 29 | 30 | PackageName: rtty 31 | SPDXID: SPDXRef-Package-rtty 32 | PackageSupplier: Organization: ACME 33 | PackageVersion: 6.6.1 34 | PackageDownloadLocation: https://github.com/zhaojh329/rtty/releases 35 | FilesAnalyzed: false 36 | PackageLicenseConcluded: LGPL-2.1-only 37 | PackageLicenseDeclared: LGPL-2.1-only 38 | PackageCopyrightText: NOASSERTION 39 | PackageSummary:remote debug client 40 | -------------------------------------------------------------------------------- /samples/spdx/issue-77/other.spdx: -------------------------------------------------------------------------------- 1 | SPDXVersion: SPDX-2.2 2 | DataLicense: CC0-1.0 3 | SPDXID: SPDXRef-DOCUMENT 4 | DocumentName: other 5 | DocumentNamespace: https://example.com/spdx/other 6 | Creator: Tool: Compliance Tool 7 | Created: 2024-04-26T12:23:57Z 8 | 9 | #### Relationships 10 | 11 | Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-Package-other 12 | 13 | Relationship: SPDXRef-Package-other CONTAINS SPDXRef-Package-libuv 14 | 15 | ##### Package: other 16 | 17 | PackageName: other 18 | SPDXID: SPDXRef-Package-other 19 | PackageSupplier: Organization: ACME 20 | PackageDownloadLocation: NOASSERTION 21 | FilesAnalyzed: false 22 | PackageLicenseConcluded: MIT 23 | PackageLicenseDeclared: MIT 24 | PackageCopyrightText: NOASSERTION 25 | 26 | ##### Package: libuv 27 | 28 | PackageName: libuv 29 | SPDXID: SPDXRef-Package-libuv 30 | PackageSupplier: Organization: ACME 31 | PackageVersion: v1.19.2 32 | PackageDownloadLocation: https://github.com 33 | FilesAnalyzed: false 34 | PackageLicenseConcluded: MIT AND BSD-3-Clause 35 | PackageLicenseDeclared: MIT AND BSD-3-Clause 36 | PackageCopyrightText: NOASSERTION 37 | PackageSummary: 38 | -------------------------------------------------------------------------------- /samples/spdx/zephyr/96b_avenger96-shell_module-app.spdx: -------------------------------------------------------------------------------- 1 | SPDXVersion: SPDX-2.2 2 | DataLicense: CC0-1.0 3 | SPDXID: SPDXRef-DOCUMENT 4 | DocumentName: app-sources 5 | DocumentNamespace: http://spdx.org/spdxdocs/zephyr-65f2599b-fc54-419f-93af-dc98db9bb6f4/app 6 | Creator: Tool: Zephyr SPDX builder 7 | Created: 2023-04-27T18:01:21Z 8 | 9 | Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-app-sources 10 | 11 | ##### Package: app-sources 12 | 13 | PackageName: app-sources 14 | SPDXID: SPDXRef-app-sources 15 | PackageDownloadLocation: NOASSERTION 16 | PackageLicenseConcluded: Apache-2.0 17 | PackageLicenseDeclared: NOASSERTION 18 | PackageCopyrightText: NOASSERTION 19 | PackageLicenseInfoFromFiles: Apache-2.0 20 | FilesAnalyzed: true 21 | PackageVerificationCode: 6815ae4336a9a2aca9c706e60a1c66a95dfa6531 22 | 23 | FileName: ./src/dynamic_cmd.c 24 | SPDXID: SPDXRef-File-dynamic-cmd.c 25 | FileChecksum: SHA1: af27bfbe15814f357351839be3707d10ec064a53 26 | FileChecksum: SHA256: 954127f14852edadd84a1efa30167700a006aeef3168fbe2ac815e115084fc7c 27 | LicenseConcluded: Apache-2.0 28 | LicenseInfoInFile: Apache-2.0 29 | FileCopyrightText: NOASSERTION 30 | 31 | FileName: ./src/main.c 32 | SPDXID: SPDXRef-File-main.c 33 | FileChecksum: SHA1: b06663196c79be76ee9b54bd2b22c0bc10b990c0 34 | FileChecksum: SHA256: 7b41fe722b0c939a029433fc7ed41f0e1776f06a4275388df286e44f6bb3688d 35 | LicenseConcluded: Apache-2.0 36 | LicenseInfoInFile: Apache-2.0 37 | FileCopyrightText: NOASSERTION 38 | 39 | FileName: ./src/test_module.c 40 | SPDXID: SPDXRef-File-test-module.c 41 | FileChecksum: SHA1: 419247ddadeebc4a5676481ae9fbae7044cf5410 42 | FileChecksum: SHA256: 642d1dd39f811284365355d816718f34994451c62b860d25b0142c0b8df8c251 43 | LicenseConcluded: Apache-2.0 44 | LicenseInfoInFile: Apache-2.0 45 | FileCopyrightText: NOASSERTION 46 | 47 | FileName: ./src/uart_reinit.c 48 | SPDXID: SPDXRef-File-uart-reinit.c 49 | FileChecksum: SHA1: 57b906b837061cfa0c97dc40383f0ad5e687e9d7 50 | FileChecksum: SHA256: 82c46db5741714ff6fc70f06ce971e2b616fadb6f997198ddd302c1c15733d1d 51 | LicenseConcluded: Apache-2.0 52 | LicenseInfoInFile: Apache-2.0 53 | FileCopyrightText: NOASSERTION 54 | 55 | --------------------------------------------------------------------------------