├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ ├── ci.yml │ ├── golangci-lint.yml │ ├── release.yml │ ├── sbom_dev.yml │ └── sbom_release.yml ├── .gitignore ├── .goreleaser.yaml ├── .tool-versions ├── CODEOWNERS ├── Compliance.md ├── Dockerfile ├── Features.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── cmd ├── compliance.go ├── dtrackScore.go ├── generate.go ├── list.go ├── root.go ├── score.go ├── share.go └── version.go ├── docs └── list.md ├── go.mod ├── go.sum ├── golangci.yml ├── images └── dt.png ├── main.go ├── pkg ├── compliance │ ├── bsi.go │ ├── bsiV2.go │ ├── bsiV2_test.go │ ├── bsi_report.go │ ├── bsi_score.go │ ├── bsi_test.go │ ├── bsi_v2_report.go │ ├── common │ │ ├── common.go │ │ └── sig.go │ ├── compliance.go │ ├── db │ │ ├── db.go │ │ ├── db_test.go │ │ └── record.go │ ├── fsct │ │ ├── fsct.go │ │ ├── fsct_report.go │ │ ├── fsct_score.go │ │ └── fsct_test.go │ ├── ntia.go │ ├── ntia_report.go │ ├── ntia_score.go │ ├── ntia_test.go │ ├── oct.go │ ├── oct_report.go │ ├── oct_score.go │ └── oct_test.go ├── cpe │ ├── cpe.go │ └── cpe_test.go ├── engine │ ├── compliance.go │ ├── dtrack.go │ ├── list.go │ ├── score.go │ ├── score_test.go │ └── share.go ├── licenses │ ├── embed_licenses.go │ ├── files │ │ ├── licenses_aboutcode.json │ │ ├── licenses_spdx.json │ │ └── licenses_spdx_exception.json │ └── license.go ├── list │ ├── list.go │ ├── report.go │ └── types.go ├── logger │ └── log.go ├── omniborid │ ├── omniborid.go │ └── omniborid_test.go ├── purl │ ├── purl.go │ └── purl_test.go ├── reporter │ ├── basic.go │ ├── detailed.go │ ├── json.go │ └── report.go ├── sbom │ ├── author.go │ ├── cdx.go │ ├── checksum.go │ ├── component.go │ ├── component_test.go │ ├── contact.go │ ├── document.go │ ├── externalReference.go │ ├── manufacturer.go │ ├── primarycomp.go │ ├── relation.go │ ├── sbom.go │ ├── sbomfakes │ │ └── fake_author.go │ ├── signature.go │ ├── spdx.go │ ├── spec.go │ ├── supplier.go │ ├── tool.go │ └── vulnerabilities.go ├── scorer │ ├── config.go │ ├── criteria.go │ ├── ntia.go │ ├── quality.go │ ├── score.go │ ├── scorer.go │ ├── scores.go │ ├── semantic.go │ ├── sharing.go │ ├── structural.go │ └── tools.go ├── share │ └── share.go ├── swhid │ ├── swhid.go │ └── swhid_test.go └── swid │ ├── swid.go │ └── swid_test.go └── samples ├── photon.spdx.json ├── sbomqs-cdx-cgomod.json ├── sbomqs-dummy-bomlinks-data.cdx.json ├── sbomqs-dummy-bomlinks-data.spdx.json ├── sbomqs-dummy-licenses.cdx-1.6.json ├── sbomqs-sbomsh-with-vuln.cdx.json ├── sbomqs-spdx-sbtool.json ├── sbomqs-spdx-syft.json ├── signature-test-data ├── SPDXJSONExample-v2.3.spdx.json ├── public_key.pem ├── sbom.sig └── stree-cdxgen-signed-sbom.cdx.json ├── stree-cdxgen-signed-sbom.cdx.json ├── stree-cdxgen.cdx.json └── test-license.json /.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/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: ">=1.20" 20 | 21 | - name: Install dependencies 22 | run: go mod download 23 | 24 | - name: Run tests 25 | run: go test ./... -v 26 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | 3 | on: 4 | pull_request: 5 | branches: main 6 | types: 7 | - opened 8 | - reopened 9 | - synchronize 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | golangci: 16 | name: lint 17 | runs-on: ubuntu-latest 18 | 19 | permissions: 20 | contents: read 21 | pull-requests: read 22 | 23 | steps: 24 | - name: Checkout mode 25 | uses: actions/checkout@v4 26 | 27 | - name: Set up Go 28 | uses: actions/setup-go@v5 29 | with: 30 | go-version-file: go.mod 31 | cache: false 32 | 33 | - name: Run golangci-lint 34 | uses: golangci/golangci-lint-action@v6 35 | with: 36 | args: --timeout=5m 37 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release | Build Binary 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | workflow_dispatch: 8 | 9 | jobs: 10 | releaser: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | id-token: write 14 | contents: write 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - run: git fetch --force --tags 20 | - uses: actions/setup-go@v5 21 | with: 22 | go-version: ">=1.20" 23 | cache: true 24 | - name: Download syft binary 25 | run: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin 26 | - name: Run syft 27 | run: syft version 28 | - name: Goreleaser 29 | uses: goreleaser/goreleaser-action@v6 30 | with: 31 | install-only: true 32 | - run: go version 33 | - run: goreleaser -v 34 | - name: Releaser 35 | run: make release 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | -------------------------------------------------------------------------------- /.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@v4 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@v4 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 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | vendor/ 16 | build/ 17 | 18 | version.txt 19 | 20 | dist/ 21 | 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: sbomqs 2 | version: 2 3 | 4 | env: 5 | - GO111MODULE=on 6 | 7 | before: 8 | hooks: 9 | - go mod tidy 10 | - /bin/bash -c 'if [ -n "$(git --no-pager diff --exit-code go.mod go.sum)" ]; then exit 1; fi' 11 | 12 | gomod: 13 | proxy: true 14 | 15 | builds: 16 | - id: binaries 17 | binary: sbomqs-{{ .Os }}-{{ .Arch }} 18 | no_unique_dist_dir: true 19 | main: . 20 | flags: 21 | - -trimpath 22 | mod_timestamp: '{{ .CommitTimestamp }}' 23 | goos: 24 | - linux 25 | - darwin 26 | - windows 27 | goarch: 28 | - amd64 29 | - arm64 30 | ldflags: 31 | - "{{ .Env.LDFLAGS }}" 32 | env: 33 | - CGO_ENABLED=0 34 | 35 | nfpms: 36 | - id: sbomqs 37 | package_name: sbomqs 38 | file_name_template: "{{ .ConventionalFileName }}" 39 | vendor: Interlynk 40 | homepage: https://interlynk.io 41 | maintainer: Interlynk Authors hello@interlynk.io 42 | builds: 43 | - binaries 44 | description: SBOM quality score - Quality metrics for your sboms. 45 | license: "Apache License 2.0" 46 | formats: 47 | - apk 48 | - deb 49 | - rpm 50 | contents: 51 | - src: /usr/bin/sbomqs-{{ .Os }}-{{ .Arch }} 52 | dst: /usr/bin/sbomqs 53 | type: "symlink" 54 | 55 | archives: 56 | - format: binary 57 | name_template: "{{ .Binary }}" 58 | allow_different_binary_count: true 59 | 60 | snapshot: 61 | name_template: SNAPSHOT-{{ .ShortCommit }} 62 | 63 | release: 64 | prerelease: allow 65 | draft: true 66 | 67 | sboms: 68 | - 69 | artifacts: binary 70 | documents: 71 | - "${artifact}.spdx.sbom" 72 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | golang 1.23.6 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | @riteshnoronha 2 | @surendrapathak 3 | -------------------------------------------------------------------------------- /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/sbomqs" 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 CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -a -o sbomqs . 15 | 16 | RUN chmod +x sbomqs 17 | 18 | # Final stage 19 | FROM alpine:3.19 20 | LABEL org.opencontainers.image.source="https://github.com/interlynk-io/sbomqs" 21 | LABEL org.opencontainers.image.description="Quality & Compliance metrics for your sboms" 22 | LABEL org.opencontainers.image.licenses=Apache-2.0 23 | 24 | COPY --from=builder /app/sbomqs /app/sbomqs 25 | 26 | # Disable version check 27 | ENV INTERLYNK_DISABLE_VERSION_CHECK=true 28 | 29 | ENTRYPOINT ["/app/sbomqs"] 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2023 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 | #inspired by https://github.com/pinterb/go-semver/blob/master/Makefile 16 | 17 | 18 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 19 | ifeq (,$(shell go env GOBIN)) 20 | GOBIN=$(shell go env GOPATH)/bin 21 | else 22 | GOBIN=$(shell go env GOBIN) 23 | endif 24 | 25 | GIT_VERSION ?= $(shell git describe --tags --always --dirty) 26 | GIT_HASH ?= $(shell git rev-parse HEAD) 27 | DATE_FMT = +'%Y-%m-%dT%H:%M:%SZ' 28 | SOURCE_DATE_EPOCH ?= $(shell git log -1 --pretty=%ct) 29 | ifdef SOURCE_DATE_EPOCH 30 | 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)") 31 | else 32 | BUILD_DATE ?= $(shell date "$(DATE_FMT)") 33 | endif 34 | GIT_TREESTATE = "clean" 35 | DIFF = $(shell git diff --quiet >/dev/null 2>&1; if [ $$? -eq 1 ]; then echo "1"; fi) 36 | ifeq ($(DIFF), 1) 37 | GIT_TREESTATE = "dirty" 38 | endif 39 | 40 | PKG ?= sigs.k8s.io/release-utils/version 41 | LDFLAGS=-buildid= -X $(PKG).gitVersion=$(GIT_VERSION) \ 42 | -X $(PKG).gitCommit=$(GIT_HASH) \ 43 | -X $(PKG).gitTreeState=$(GIT_TREESTATE) \ 44 | -X $(PKG).buildDate=$(BUILD_DATE) 45 | 46 | 47 | BUILD_DIR = ./build 48 | 49 | .PHONY: dep 50 | dep: 51 | go mod vendor 52 | go mod tidy 53 | 54 | .PHONY: generate 55 | generate: 56 | go generate ./... 57 | 58 | .PHONY: test 59 | test: generate 60 | go test -cover -race ./... 61 | 62 | .PHONY: build 63 | build: 64 | CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/sbomqs main.go 65 | 66 | .PHONY: clean 67 | clean: 68 | \rm -rf $(BUILD_DIR) 69 | 70 | .PHONY: snapshot 71 | snapshot: 72 | LDFLAGS="$(LDFLAGS)" \goreleaser release --clean --snapshot --timeout 120m 73 | 74 | .PHONY: release 75 | release: 76 | LDFLAGS="$(LDFLAGS)" \goreleaser release --clean --timeout 120m 77 | 78 | .PHONY: updatedeps 79 | updatedeps: 80 | go get -u all 81 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | To report a security issue, please email 4 | [hello@interlynk.io](mailto:hello@interlynk.io) 5 | with a description of the issue, the steps you took to create the issue, 6 | affected versions, and, if known, mitigations for the issue. 7 | -------------------------------------------------------------------------------- /cmd/compliance.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 | package cmd 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | 20 | "github.com/interlynk-io/sbomqs/pkg/engine" 21 | "github.com/interlynk-io/sbomqs/pkg/logger" 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | var complianceCmd = &cobra.Command{ 26 | Use: "compliance [flags]", 27 | Short: "compliance command checks an SBOM for compliance with SBOM standards", 28 | Long: ` 29 | Check if our SBOM meets compliance requirements for various standards, such as NTIA minimum elements, 30 | BSI TR-03183-2, Framing Software Component Transparency (v3) and OpenChain Telco. 31 | `, 32 | Example: ` sbomqs compliance < --ntia | --bsi | --bsi-v2 | --fsct | --oct > [--basic | --json] 33 | 34 | # Check a NTIA minimum elements compliance against a SBOM in a table output 35 | sbomqs compliance --ntia samples/sbomqs-spdx-syft.json 36 | 37 | # Check a BSI TR-03183-2 v1.1 compliance against a SBOM in a table output 38 | sbomqs compliance --bsi samples/sbomqs-spdx-syft.json 39 | 40 | # Check a BSI TR-03183-2 v2.0.0 compliance against a SBOM in a table output 41 | sbomqs compliance --bsi-v2 samples/sbomqs-spdx-syft.json 42 | 43 | # Check a Framing Software Component Transparency (v3) compliance against a SBOM in a table output 44 | sbomqs compliance --fsct samples/sbomqs-spdx-syft.json 45 | 46 | # Check a OpenChain Telco compliance against a SBOM in a JSON output 47 | sbomqs compliance --oct --json samples/sbomqs-spdx-syft.json 48 | 49 | # Check a Framing Software Component Transparency (v3) compliance against a SBOM in a table colorful output 50 | sbomqs compliance --fsct --color samples/sbomqs-spdx-syft.json 51 | 52 | `, 53 | Args: func(cmd *cobra.Command, args []string) error { 54 | if err := cobra.ExactArgs(1)(cmd, args); err != nil { 55 | return fmt.Errorf("compliance requires a single argument, the path to an SBOM file") 56 | } 57 | 58 | return nil 59 | }, 60 | RunE: func(cmd *cobra.Command, args []string) error { 61 | debug, _ := cmd.Flags().GetBool("debug") 62 | if debug { 63 | logger.InitDebugLogger() 64 | } else { 65 | logger.InitProdLogger() 66 | } 67 | 68 | ctx := logger.WithLogger(context.Background()) 69 | 70 | engParams := setupEngineParams(cmd, args) 71 | return engine.ComplianceRun(ctx, engParams) 72 | }, 73 | } 74 | 75 | func setupEngineParams(cmd *cobra.Command, args []string) *engine.Params { 76 | engParams := &engine.Params{} 77 | 78 | engParams.Basic, _ = cmd.Flags().GetBool("basic") 79 | engParams.Detailed, _ = cmd.Flags().GetBool("detailed") 80 | engParams.JSON, _ = cmd.Flags().GetBool("json") 81 | engParams.Color, _ = cmd.Flags().GetBool("color") 82 | 83 | engParams.Ntia, _ = cmd.Flags().GetBool("ntia") 84 | engParams.Bsi, _ = cmd.Flags().GetBool("bsi") 85 | engParams.BsiV2, _ = cmd.Flags().GetBool("bsi-v2") 86 | engParams.Oct, _ = cmd.Flags().GetBool("oct") 87 | engParams.Fsct, _ = cmd.Flags().GetBool("fsct") 88 | 89 | engParams.Debug, _ = cmd.Flags().GetBool("debug") 90 | 91 | engParams.Signature, _ = cmd.Flags().GetString("sig") 92 | engParams.PublicKey, _ = cmd.Flags().GetString("pub") 93 | 94 | engParams.Path = append(engParams.Path, args[0]) 95 | engParams.Blob = args[0] 96 | 97 | return engParams 98 | } 99 | 100 | func init() { 101 | rootCmd.AddCommand(complianceCmd) 102 | 103 | // Debug control 104 | complianceCmd.Flags().BoolP("debug", "D", false, "debug logging") 105 | 106 | // Output control 107 | complianceCmd.Flags().BoolP("json", "j", false, "output in json format") 108 | complianceCmd.Flags().BoolP("basic", "b", false, "output in basic format") 109 | complianceCmd.Flags().BoolP("detailed", "d", false, "output in detailed format(default)") 110 | complianceCmd.Flags().BoolP("color", "l", false, "output in colorful") 111 | 112 | // complianceCmd.Flags().BoolP("pdf", "p", false, "output in pdf format") 113 | complianceCmd.MarkFlagsMutuallyExclusive("json", "basic", "detailed") 114 | 115 | // Standards control 116 | complianceCmd.Flags().BoolP("ntia", "n", false, "NTIA minimum elements (July 12, 2021)") 117 | complianceCmd.Flags().BoolP("bsi", "c", false, "BSI TR-03183-2 (v1.1)") 118 | complianceCmd.Flags().BoolP("bsi-v2", "s", false, "BSI TR-03183-2 (v2.0.0)") 119 | complianceCmd.Flags().BoolP("oct", "t", false, "OpenChain Telco SBOM (v1.0)") 120 | complianceCmd.Flags().BoolP("fsct", "f", false, "Framing Software Component Transparency (v3)") 121 | 122 | complianceCmd.Flags().StringP("sig", "v", "", "signature of sbom") 123 | complianceCmd.Flags().StringP("pub", "p", "", "public key of sbom") 124 | } 125 | -------------------------------------------------------------------------------- /cmd/dtrackScore.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 cmd 16 | 17 | import ( 18 | "context" 19 | "log" 20 | 21 | "github.com/google/uuid" 22 | "github.com/interlynk-io/sbomqs/pkg/engine" 23 | "github.com/interlynk-io/sbomqs/pkg/logger" 24 | "github.com/spf13/cobra" 25 | ) 26 | 27 | // dtrackScoreCmd represents the dtrackScore command 28 | var dtrackScoreCmd = &cobra.Command{ 29 | Use: "dtrackScore ", 30 | Short: "generate an sbom quality score for a given project id from dependency track", 31 | Long: `dtrackScore allows your to score the sbom quality of a project from dependency track.`, 32 | SilenceUsage: true, 33 | Args: cobra.MinimumNArgs(1), 34 | RunE: func(cmd *cobra.Command, args []string) error { 35 | debug, _ := cmd.Flags().GetBool("debug") 36 | if debug { 37 | logger.InitDebugLogger() 38 | } else { 39 | logger.InitProdLogger() 40 | } 41 | ctx := logger.WithLogger(context.Background()) 42 | 43 | dtParams, err := extractArgs(cmd, args) 44 | if err != nil { 45 | log.Fatalf("failed to extract args: %v", err) 46 | } 47 | 48 | return engine.DtrackScore(ctx, dtParams) 49 | }, 50 | } 51 | 52 | func extractArgs(cmd *cobra.Command, args []string) (*engine.DtParams, error) { 53 | params := &engine.DtParams{} 54 | 55 | url, err := cmd.Flags().GetString("url") 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | apiKey, err := cmd.Flags().GetString("api-key") 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | json, _ := cmd.Flags().GetBool("json") 66 | basic, _ := cmd.Flags().GetBool("basic") 67 | detailed, _ := cmd.Flags().GetBool("detailed") 68 | 69 | params.URL = url 70 | params.APIKey = apiKey 71 | 72 | params.JSON = json 73 | params.Basic = basic 74 | params.Detailed = detailed 75 | 76 | params.TagProjectWithScore, _ = cmd.Flags().GetBool("tag-project-with-score") 77 | 78 | for _, arg := range args { 79 | argID, err := uuid.Parse(arg) 80 | if err != nil { 81 | return nil, err 82 | } 83 | params.ProjectIDs = append(params.ProjectIDs, argID) 84 | } 85 | 86 | params.Timeout, _ = cmd.Flags().GetInt("timeout") 87 | 88 | return params, nil 89 | } 90 | 91 | func init() { 92 | rootCmd.AddCommand(dtrackScoreCmd) 93 | dtrackScoreCmd.Flags().StringP("url", "u", "", "dependency track url https://localhost:8080/") 94 | dtrackScoreCmd.Flags().StringP("api-key", "k", "", "dependency track api key, requires VIEW_PORTFOLIO for scoring and PORTFOLIO_MANAGEMENT for tagging") 95 | err := dtrackScoreCmd.MarkFlagRequired("url") 96 | if err != nil { 97 | // Handle the error appropriately, such as logging it or returning it 98 | log.Fatalf("Failed to mark flag as deprecated: %v", err) 99 | } 100 | err = dtrackScoreCmd.MarkFlagRequired("api-key") 101 | if err != nil { 102 | // Handle the error appropriately, such as logging it or returning it 103 | log.Fatalf("Failed to mark flag as deprecated: %v", err) 104 | } 105 | 106 | dtrackScoreCmd.Flags().BoolP("debug", "D", false, "enable debug logging") 107 | 108 | dtrackScoreCmd.Flags().BoolP("json", "j", false, "results in json") 109 | dtrackScoreCmd.Flags().BoolP("detailed", "d", false, "results in table format, default") 110 | dtrackScoreCmd.Flags().BoolP("basic", "b", false, "results in single line format") 111 | 112 | dtrackScoreCmd.Flags().BoolP("tag-project-with-score", "t", false, "tag project with sbomqs score") 113 | dtrackScoreCmd.Flags().IntP("timeout", "i", 60, "Timeout in seconds for Dependency-Track API requests") 114 | } 115 | -------------------------------------------------------------------------------- /cmd/generate.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 | package cmd 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "os" 20 | 21 | "github.com/interlynk-io/sbomqs/pkg/logger" 22 | "github.com/interlynk-io/sbomqs/pkg/scorer" 23 | "github.com/spf13/cobra" 24 | ) 25 | 26 | const ( 27 | featuresFileName = "features.yaml" 28 | features = "features" 29 | ) 30 | 31 | // generateCmd represents the generate command 32 | var generateCmd = &cobra.Command{ 33 | Use: "generate", 34 | Short: "provides a comprehensive config generate for your sbom to get specific criteria", 35 | RunE: func(_ *cobra.Command, args []string) error { 36 | ctx := logger.WithLogger(context.Background()) 37 | 38 | if len(args) > 0 { 39 | if args[0] == features { 40 | return generateYaml(ctx) 41 | } 42 | } else { 43 | return fmt.Errorf("arguments missing%s", "list of valid command eg. features") 44 | } 45 | return fmt.Errorf("invalid arguments%s", "list of valid command eg. features") 46 | }, 47 | } 48 | 49 | func init() { 50 | rootCmd.AddCommand(generateCmd) 51 | } 52 | 53 | func generateYaml(_ context.Context) error { 54 | return os.WriteFile(featuresFileName, []byte(scorer.DefaultConfig()), 0o600) 55 | } 56 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 | package cmd 15 | 16 | import ( 17 | "os" 18 | 19 | "github.com/spf13/cobra" 20 | ) 21 | 22 | // rootCmd represents the base command when called without any subcommands 23 | var rootCmd = &cobra.Command{ 24 | Use: "sbomqs", 25 | Short: "sbomqs application provides sbom quality scores.", 26 | Long: `SBOM Quality Score (sbomqs) is a standardized metric to 27 | produce a calculated score that represents a level of “quality” 28 | when using an SBOM. The sbomqs is intended to help customers make 29 | an assessment of a SBOM acceptance risk based on their personal risk tolerance. 30 | `, 31 | } 32 | 33 | // Execute adds all child commands to the root command and sets flags appropriately. 34 | // This is called by main.main(). It only needs to happen once to the rootCmd. 35 | func Execute() { 36 | err := rootCmd.Execute() 37 | if err != nil { 38 | os.Exit(1) 39 | } 40 | } 41 | 42 | func init() { 43 | rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 44 | } 45 | -------------------------------------------------------------------------------- /cmd/share.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 | package cmd 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | 20 | "github.com/interlynk-io/sbomqs/pkg/engine" 21 | "github.com/interlynk-io/sbomqs/pkg/logger" 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | // shareCmd represents the share command 26 | var shareCmd = &cobra.Command{ 27 | Use: "share ", 28 | Short: "share your sbom quality score with others", 29 | Long: `share command creates a permanent link to the score result from an easy-to-understand web page. 30 | 31 | Due to privacy considerations, the SBOM never leaves your environment, and only 32 | the score report (includes filename) is sent to https://sbombenchmark.dev (exact JSON form is used). 33 | 34 | For more information, please visit https://sbombenchmark.dev 35 | `, 36 | SilenceUsage: true, 37 | Args: func(cmd *cobra.Command, args []string) error { 38 | if err := cobra.ExactArgs(1)(cmd, args); err != nil { 39 | return fmt.Errorf("share requires a single argument, the path to the sbom file") 40 | } 41 | 42 | return nil 43 | }, 44 | 45 | RunE: func(cmd *cobra.Command, args []string) error { 46 | debug, _ := cmd.Flags().GetBool("debug") 47 | if debug { 48 | logger.InitDebugLogger() 49 | } else { 50 | logger.InitProdLogger() 51 | } 52 | 53 | ctx := logger.WithLogger(context.Background()) 54 | sbomFileName := args[0] 55 | 56 | engParams := &engine.Params{Basic: true} 57 | engParams.Path = append(engParams.Path, sbomFileName) 58 | return engine.ShareRun(ctx, engParams) 59 | }, 60 | } 61 | 62 | func init() { 63 | rootCmd.AddCommand(shareCmd) 64 | 65 | //Debug Control 66 | shareCmd.Flags().BoolP("debug", "D", false, "enable debug logging") 67 | } 68 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 cmd 16 | 17 | import ( 18 | version "sigs.k8s.io/release-utils/version" 19 | ) 20 | 21 | func init() { 22 | rootCmd.AddCommand(version.Version()) 23 | } 24 | -------------------------------------------------------------------------------- /docs/list.md: -------------------------------------------------------------------------------- 1 | # `sbomqs list` Command 2 | 3 | The `sbomqs list` command allows users to list components or SBOM fileds based on a specified feature, making it easier to identify which components or properties(SBOM metadata) meet (or fail to meet) certain criteria. This command is particularly useful for pinpointing missing fields in SBOM components (e.g., suppliers, licenses) or verifying SBOM metadata (e.g., authors, creation timestamp). 4 | 5 | ## Usage 6 | 7 | ```bash 8 | sbomqs list [flags] 9 | ``` 10 | 11 | ### Autocompletion for `--feature` Flag 12 | 13 | - **For Bash**: 14 | 15 | ```bash 16 | sbomqs completion bash > sbomqs_completion.sh 17 | ``` 18 | 19 | - **For Zsh**: 20 | 21 | ```bash 22 | sbomqs completion zsh > sbomqs_completion.sh 23 | ``` 24 | 25 | This creates a file (`sbomqs_completion.sh`) with the completion logic. 26 | 27 | To enable autocompletion, source the script in your shell session: 28 | 29 | - **Temporary (Current Session)**: 30 | 31 | ```bash 32 | source sbomqs_completion.sh 33 | ``` 34 | 35 | - **Permanent (All Sessions)**: 36 | 37 | - Move the script to a directory in your shell’s path: 38 | 39 | ```bash 40 | mv sbomqs_completion.sh ~/.zsh/ # For Zsh, or ~/.bash/ for Bash 41 | ``` 42 | 43 | - Add it to your shell configuration: 44 | - **Bash**: Edit `~/.bashrc` or `~/.bash_profile`: 45 | 46 | ```bash 47 | echo "source ~/.bash/sbomqs_completion.sh" >> ~/.bashrc 48 | source ~/.bashrc 49 | ``` 50 | 51 | - **Zsh**: Edit `~/.zshrc`: 52 | 53 | ```bash 54 | echo "source ~/.zsh/sbomqs_completion.sh" >> ~/.zshrc 55 | source ~/.zshrc 56 | ``` 57 | 58 | For Zsh, ensure completion is initialized by adding `autoload -Uz compinit && compinit` to `~/.zshrc` if not already present. 59 | 60 | Run the following command and press ``: 61 | 62 | ```bash 63 | sbomqs list --feature= 64 | ``` 65 | 66 | ### Flags 67 | 68 | - `--features, -f `: Specifies the feature to list (required). See supported features below. 69 | - `--missing, -m`: Lists components or properties that do not have the specified feature (default: false). 70 | - `--basic, -b`: Outputs results in a single-line format (default: false). 71 | - `--detailed, -d`: Outputs results in a detailed table format (default: true). 72 | - `--json, -j`: Outputs results in JSON format (default: false). 73 | - `--color, -l`: Enables colored output for the detailed format (default: false). 74 | - `--debug, -D`: Enables debug logging (default: false). 75 | 76 | ### Supported Features 77 | 78 | The list command supports the following features, categorized into **component-based** (`comp_`) and **SBOM-based** (`sbom_`) features: 79 | 80 | #### Component-Based Features (comp_) 81 | 82 | These features evaluate individual components in the SBOM: 83 | 84 | - `comp_with_name`: Lists components with a name. 85 | - `comp_with_version`: Lists components with a version. 86 | - `comp_with_supplier`: Lists components with a supplier. 87 | - `comp_with_uniq_ids`: Lists components with unique IDs. 88 | - `comp_valid_licenses`: Lists components with at least one valid SPDX license. 89 | - `comp_with_any_vuln_lookup_id`: Lists components with any vulnerability lookup ID (CPE or PURL). 90 | - `comp_with_deprecated_licenses`: Lists components with deprecated licenses. 91 | - `comp_with_multi_vuln_lookup_id`: Lists components with both CPE and PURL (multiple vulnerability lookup IDs). 92 | - `comp_with_primary_purpose`: Lists components with a supported primary purpose. 93 | - `comp_with_restrictive_licenses`: Lists components with restrictive licenses. 94 | - `comp_with_checksums`: Lists components with checksums. 95 | - `comp_with_licenses`: Lists components with licenses. 96 | 97 | #### SBOM-Based Features (sbom_) 98 | 99 | These features evaluate document-level properties of the SBOM: 100 | 101 | - `sbom_creation_timestamp`: Lists the SBOM’s creation timestamp. 102 | - `sbom_authors`: Lists the SBOM’s authors. 103 | - `sbom_with_creator_and_version`: Lists the creator tool and its version. 104 | - `sbom_with_primary_component`: Lists the primary component of the SBOM. 105 | - `sbom_dependencies`: Lists the dependencies of the primary component. 106 | - `sbom_sharable`: Lists whether the SBOM has a sharable license (all licenses must be free for any use). 107 | - `sbom_parsable`: Lists whether the SBOM is parsable. 108 | - `sbom_spec`: Lists the SBOM specification (e.g., SPDX, CycloneDX). 109 | - `sbom_spec_file_format`: Lists the SBOM file format (e.g., JSON, YAML). 110 | - `sbom_spec_version`: Lists the SBOM specification version (e.g., SPDX-2.2). 111 | 112 | ## Examples 113 | 114 | ### 1. List Components with Suppliers (Basic Format) 115 | 116 | ```bash 117 | $ sbomqs list --features comp_with_supplier --basic samples/photon.spdx.json 118 | 119 | samples/photon.spdx.json: comp_with_supplier (present): 0/39 components 120 | ``` 121 | 122 | ### 2. List Components Missing Suppliers (Detailed Format) 123 | 124 | ```bash 125 | $ sbomqs list --features comp_with_supplier --missing samples/photon.spdx.json 126 | 127 | File: samples/photon.spdx.json 128 | Feature: comp_with_supplier (missing) 129 | +----------------------------+-----------------+-----------------+ 130 | | Feature | Component Name | Version | 131 | +----------------------------+-----------------+-----------------+ 132 | | comp_with_supplier (39/39) | abc | v1 | 133 | | | abe | v2 | 134 | | | abf | v3 | 135 | | | abg | v4 | 136 | | | abh | v5 | 137 | | | abi | v6 | 138 | | | abz | v26 | 139 | +----------------------------+-----------------+-----------------+ 140 | ``` 141 | 142 | ### 3. List SBOM Authors (JSON Format) 143 | 144 | ```bash 145 | $ sbomqs list --features sbom_authors --json samples/photon.spdx.json 146 | 147 | { 148 | "run_id": "8af142e2-822f-4005-9612-42ddeb9394bf", 149 | "timestamp": "2025-04-03T10:00:00Z", 150 | "creation_info": { 151 | "name": "sbomqs", 152 | "version": "v1.0.0", 153 | "vendor": "Interlynk (support@interlynk.io)" 154 | }, 155 | "files": [ 156 | { 157 | "file_name": "samples/photon.spdx.json", 158 | "feature": "sbom_authors", 159 | "missing": false, 160 | "document_property": { 161 | "property": "Authors", 162 | "value": "John Doe", 163 | "present": true 164 | }, 165 | "errors": [] 166 | } 167 | ] 168 | } 169 | ``` 170 | 171 | ## Notes 172 | 173 | - The `--missing` flag is particularly useful for identifying gaps in your SBOM, such as components missing suppliers or licenses, helping you improve compliance and quality. 174 | - The `list` command supports the same input sources as the score command: local files, directories, and GitHub URLs. 175 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/interlynk-io/sbomqs 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/DependencyTrack/client-go v0.16.0 10 | github.com/github/go-spdx/v2 v2.3.3 11 | github.com/google/uuid v1.6.0 12 | github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2 13 | github.com/olekukonko/tablewriter v0.0.5 14 | github.com/package-url/packageurl-go v0.1.3 15 | github.com/samber/lo v1.50.0 16 | github.com/spdx/tools-golang v0.5.5 17 | github.com/spf13/cobra v1.9.1 18 | github.com/stretchr/testify v1.10.0 19 | go.uber.org/zap v1.27.0 20 | gopkg.in/yaml.v2 v2.4.0 21 | sigs.k8s.io/release-utils v0.11.1 22 | ) 23 | 24 | require ( 25 | github.com/davecgh/go-spew v1.1.1 // indirect 26 | github.com/google/go-cmp v0.7.0 // indirect 27 | github.com/mattn/go-colorable v0.1.14 // indirect 28 | github.com/mattn/go-isatty v0.0.20 // indirect 29 | github.com/pkg/errors v0.9.1 // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | github.com/tidwall/gjson v1.18.0 // indirect 32 | github.com/tidwall/match v1.1.1 // indirect 33 | github.com/tidwall/pretty v1.2.1 // indirect 34 | gopkg.in/yaml.v3 v3.0.1 // indirect 35 | ) 36 | 37 | require ( 38 | github.com/anchore/go-struct-converter v0.0.0-20250211213226-cce56d595160 // indirect 39 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect 40 | github.com/fatih/color v1.18.0 41 | github.com/go-git/go-billy/v5 v5.6.2 42 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 43 | github.com/mattn/go-runewidth v0.0.16 // indirect 44 | github.com/rivo/uniseg v0.4.7 // indirect 45 | github.com/spdx/gordf v0.0.0-20250128162952-000978ccd6fb // indirect 46 | github.com/spf13/afero v1.14.0 47 | github.com/spf13/pflag v1.0.6 // indirect 48 | github.com/tidwall/sjson v1.2.5 49 | go.uber.org/multierr v1.11.0 // indirect 50 | golang.org/x/mod v0.24.0 // indirect 51 | golang.org/x/sync v0.13.0 // indirect 52 | golang.org/x/sys v0.32.0 // indirect 53 | golang.org/x/text v0.24.0 // indirect 54 | golang.org/x/tools v0.32.0 // indirect 55 | gotest.tools v2.2.0+incompatible 56 | sigs.k8s.io/yaml v1.4.0 // indirect 57 | ) 58 | -------------------------------------------------------------------------------- /golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | disable-all: true 3 | enable: 4 | - asciicheck 5 | - unused 6 | - errcheck 7 | - errorlint 8 | - gofmt 9 | - goimports 10 | - gosec 11 | - revive 12 | - misspell 13 | - stylecheck 14 | - staticcheck 15 | - unconvert 16 | 17 | linters-settings: 18 | unparam: 19 | exclude: 20 | - 'setIgnore' 21 | 22 | run: 23 | issues-exit-code: 1 24 | timeout: 10m 25 | -------------------------------------------------------------------------------- /images/dt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlynk-io/sbomqs/7fb5cece50e89250a90515e0401d9348b3c5c11e/images/dt.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 main 16 | 17 | import "github.com/interlynk-io/sbomqs/cmd" 18 | 19 | func main() { 20 | cmd.Execute() 21 | } 22 | -------------------------------------------------------------------------------- /pkg/compliance/bsi_score.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 compliance 16 | 17 | import "github.com/interlynk-io/sbomqs/pkg/compliance/db" 18 | 19 | type bsiScoreResult struct { 20 | id string 21 | requiredScore float64 22 | optionalScore float64 23 | requiredRecords int 24 | optionalRecords int 25 | } 26 | 27 | func newBsiScoreResult(id string) *bsiScoreResult { 28 | return &bsiScoreResult{id: id} 29 | } 30 | 31 | func (r *bsiScoreResult) totalScore() float64 { 32 | if r.requiredRecords == 0 && r.optionalRecords == 0 { 33 | return 0.0 34 | } 35 | 36 | if r.requiredRecords != 0 && r.optionalRecords != 0 { 37 | return (r.totalRequiredScore() + r.totalOptionalScore()) / 2 38 | } 39 | 40 | if r.requiredRecords == 0 && r.optionalRecords != 0 { 41 | return r.totalOptionalScore() 42 | } 43 | 44 | return r.totalRequiredScore() 45 | } 46 | 47 | func (r *bsiScoreResult) totalRequiredScore() float64 { 48 | if r.requiredRecords == 0 { 49 | return 0.0 50 | } 51 | 52 | return r.requiredScore / float64(r.requiredRecords) 53 | } 54 | 55 | func (r *bsiScoreResult) totalOptionalScore() float64 { 56 | if r.optionalRecords == 0 { 57 | return 0.0 58 | } 59 | 60 | return r.optionalScore / float64(r.optionalRecords) 61 | } 62 | 63 | func bsiKeyIDScore(dtb *db.DB, key int, id string) *bsiScoreResult { 64 | records := dtb.GetRecordsByKeyID(key, id) 65 | 66 | if len(records) == 0 { 67 | return newBsiScoreResult(id) 68 | } 69 | 70 | requiredScore := 0.0 71 | optionalScore := 0.0 72 | 73 | requiredRecs := 0 74 | optionalRecs := 0 75 | 76 | for _, r := range records { 77 | if r.Required { 78 | requiredScore += r.Score 79 | requiredRecs++ 80 | } else { 81 | optionalScore += r.Score 82 | optionalRecs++ 83 | } 84 | } 85 | 86 | return &bsiScoreResult{ 87 | id: id, 88 | requiredScore: requiredScore, 89 | optionalScore: optionalScore, 90 | requiredRecords: requiredRecs, 91 | optionalRecords: optionalRecs, 92 | } 93 | } 94 | 95 | func bsiIDScore(dtb *db.DB, id string) *bsiScoreResult { 96 | records := dtb.GetRecordsByID(id) 97 | 98 | if len(records) == 0 { 99 | return newBsiScoreResult(id) 100 | } 101 | 102 | requiredScore := 0.0 103 | optionalScore := 0.0 104 | 105 | requiredRecs := 0 106 | optionalRecs := 0 107 | 108 | for _, r := range records { 109 | if r.Required { 110 | requiredScore += r.Score 111 | requiredRecs++ 112 | } else { 113 | optionalScore += r.Score 114 | optionalRecs++ 115 | } 116 | } 117 | 118 | return &bsiScoreResult{ 119 | id: id, 120 | requiredScore: requiredScore, 121 | optionalScore: optionalScore, 122 | requiredRecords: requiredRecs, 123 | optionalRecords: optionalRecs, 124 | } 125 | } 126 | 127 | func bsiAggregateScore(dtb *db.DB) *bsiScoreResult { 128 | var results []bsiScoreResult 129 | var finalResult bsiScoreResult 130 | 131 | ids := dtb.GetAllIDs() 132 | for _, id := range ids { 133 | results = append(results, *bsiIDScore(dtb, id)) 134 | } 135 | 136 | for _, r := range results { 137 | finalResult.requiredScore += r.requiredScore 138 | finalResult.optionalScore += r.optionalScore 139 | finalResult.requiredRecords += r.requiredRecords 140 | finalResult.optionalRecords += r.optionalRecords 141 | } 142 | 143 | return &finalResult 144 | } 145 | -------------------------------------------------------------------------------- /pkg/compliance/bsi_v2_report.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 compliance 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "os" 21 | 22 | db "github.com/interlynk-io/sbomqs/pkg/compliance/db" 23 | "github.com/olekukonko/tablewriter" 24 | ) 25 | 26 | func bsiV2JSONReport(dtb *db.DB, fileName string) { 27 | name := "BSI TR-03183-2 v2.0.0 Compliance Report" 28 | revision := "TR-03183-2 (2.0.0)" 29 | jr := newJSONReport(name, revision) 30 | jr.Run.FileName = fileName 31 | 32 | score := bsiAggregateScore(dtb) 33 | summary := Summary{} 34 | summary.MaxScore = 10.0 35 | summary.TotalScore = score.totalScore() 36 | summary.TotalRequiredScore = score.totalRequiredScore() 37 | summary.TotalOptionalScore = score.totalOptionalScore() 38 | 39 | jr.Summary = summary 40 | jr.Sections = constructSections(dtb) 41 | 42 | o, _ := json.MarshalIndent(jr, "", " ") 43 | fmt.Println(string(o)) 44 | } 45 | 46 | func bsiV2DetailedReport(dtb *db.DB, fileName string) { 47 | table := tablewriter.NewWriter(os.Stdout) 48 | score := bsiAggregateScore(dtb) 49 | 50 | fmt.Printf("BSI TR-03183-2 v2.0.0 Compliance Report \n") 51 | fmt.Printf("Compliance score by Interlynk Score:%0.1f RequiredScore:%0.1f OptionalScore:%0.1f for %s\n", score.totalScore(), score.totalRequiredScore(), score.totalOptionalScore(), fileName) 52 | fmt.Printf("* indicates optional fields\n") 53 | table.SetHeader([]string{"ElementId", "Section", "Datafield", "Element Result", "Score"}) 54 | table.SetRowLine(true) 55 | table.SetAutoMergeCellsByColumnIndex([]int{0}) 56 | 57 | sections := constructSections(dtb) 58 | 59 | for _, section := range sections { 60 | sectionID := section.ID 61 | if !section.Required { 62 | sectionID = sectionID + "*" 63 | } 64 | table.Append([]string{section.ElementID, sectionID, section.DataField, section.ElementResult, fmt.Sprintf("%0.1f", section.Score)}) 65 | } 66 | table.Render() 67 | } 68 | 69 | func bsiV2BasicReport(dtb *db.DB, fileName string) { 70 | score := bsiAggregateScore(dtb) 71 | fmt.Printf("BSI TR-03183-2 v2.0.0 Compliance Report\n") 72 | fmt.Printf("Score:%0.1f RequiredScore:%0.1f OptionalScore:%0.1f for %s\n", score.totalScore(), score.totalRequiredScore(), score.totalOptionalScore(), fileName) 73 | } 74 | -------------------------------------------------------------------------------- /pkg/compliance/common/sig.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 common 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "crypto/rsa" 21 | "crypto/x509" 22 | "encoding/base64" 23 | "encoding/json" 24 | "encoding/pem" 25 | "fmt" 26 | "math/big" 27 | "os" 28 | 29 | "github.com/interlynk-io/sbomqs/pkg/logger" 30 | "github.com/tidwall/sjson" 31 | ) 32 | 33 | type SBOM struct { 34 | Signature *Signature `json:"signature"` 35 | OtherData map[string]interface{} `json:"-"` // Holds the remaining SBOM data 36 | } 37 | 38 | type Signature struct { 39 | Algorithm string `json:"algorithm"` 40 | Value string `json:"value"` 41 | PublicKey *PublicKey `json:"publicKey"` 42 | } 43 | 44 | type PublicKey struct { 45 | Kty string `json:"kty"` 46 | N string `json:"n"` 47 | E string `json:"e"` 48 | } 49 | 50 | func RetrieveSignatureFromSBOM(ctx context.Context, sbomFile string) (string, string, string, error) { 51 | log := logger.FromContext(ctx) 52 | log.Debugf("common.RetrieveSignatureFromSBOM()") 53 | var err error 54 | 55 | data, err := os.ReadFile(sbomFile) 56 | if err != nil { 57 | log.Debug("error reading SBOM file: %w", err) 58 | return "", "", "", fmt.Errorf("error reading SBOM file: %w", err) 59 | } 60 | 61 | var sbom SBOM 62 | 63 | // nolint 64 | extracted_signature := "extracted_signature.bin" 65 | 66 | // nolint 67 | extracted_publick_key := "extracted_public_key.pem" 68 | 69 | if err := json.Unmarshal(data, &sbom); err != nil { 70 | log.Debug("Error parsing SBOM JSON: %w", err) 71 | return "", "", "", fmt.Errorf("error unmarshalling SBOM JSON: %w", err) 72 | } 73 | 74 | if sbom.Signature == nil { 75 | log.Debug("signature and public key are not present in the SBOM") 76 | return sbomFile, "", "", nil 77 | } 78 | log.Debug("signature and public key are present in the SBOM") 79 | 80 | signatureValue, err := base64.StdEncoding.DecodeString(sbom.Signature.Value) 81 | if err != nil { 82 | log.Debug("error decoding signature: %w", err) 83 | return "", "", "", fmt.Errorf("error decoding signature: %w", err) 84 | } 85 | 86 | if err := os.WriteFile(extracted_signature, signatureValue, 0o600); err != nil { 87 | log.Debug("Error writing signature to file:", err) 88 | } 89 | log.Debug("Signature written to file: extracted_signature.bin") 90 | 91 | // extract the public key modulus and exponent 92 | modulus, err := base64.StdEncoding.DecodeString(sbom.Signature.PublicKey.N) 93 | if err != nil { 94 | return "", "", "", fmt.Errorf("error decoding public key modulus: %w", err) 95 | } 96 | exponent := DecodeBase64URLEncodingToInt(sbom.Signature.PublicKey.E) 97 | if exponent == 0 { 98 | log.Debug("Invalid public key exponent.") 99 | } 100 | 101 | // create the RSA public key 102 | pubKey := &rsa.PublicKey{ 103 | N: DecodeBigInt(modulus), 104 | E: exponent, 105 | } 106 | 107 | pubKeyPEM := PublicKeyToPEM(pubKey) 108 | if err := os.WriteFile(extracted_publick_key, pubKeyPEM, 0o600); err != nil { 109 | log.Debug("error writing public key to file: %w", err) 110 | } 111 | 112 | // remove the "signature" section 113 | modifiedSBOM, err := sjson.DeleteBytes(data, "signature") 114 | if err != nil { 115 | log.Debug("Error removing signature section: %w", err) 116 | } 117 | 118 | var normalizedSBOM bytes.Buffer 119 | if err := json.Indent(&normalizedSBOM, modifiedSBOM, "", " "); err != nil { 120 | log.Debug("Error normalizing SBOM JSON: %w", err) 121 | } 122 | 123 | // save the modified SBOM to a new file without a trailing newline 124 | standaloneSBOMFile := "standalone_sbom.json" 125 | if err := os.WriteFile(standaloneSBOMFile, bytes.TrimSuffix(normalizedSBOM.Bytes(), []byte("\n")), 0o600); err != nil { 126 | return "", "", "", fmt.Errorf("error writing standalone SBOM file: %w", err) 127 | } 128 | 129 | log.Debug("Standalone SBOM saved to:", standaloneSBOMFile) 130 | return standaloneSBOMFile, extracted_signature, extracted_publick_key, nil 131 | } 132 | 133 | func DecodeBase64URLEncodingToInt(input string) int { 134 | bytes, err := base64.StdEncoding.DecodeString(input) 135 | if err != nil { 136 | return 0 137 | } 138 | if len(bytes) == 0 { 139 | return 0 140 | } 141 | result := 0 142 | for _, b := range bytes { 143 | result = result<<8 + int(b) 144 | } 145 | return result 146 | } 147 | 148 | func DecodeBigInt(input []byte) *big.Int { 149 | result := new(big.Int) 150 | result.SetBytes(input) 151 | return result 152 | } 153 | 154 | func PublicKeyToPEM(pub *rsa.PublicKey) []byte { 155 | pubASN1, err := x509.MarshalPKIXPublicKey(pub) 156 | if err != nil { 157 | fmt.Println("Error marshaling public key:", err) 158 | return nil 159 | } 160 | pubPEM := pem.EncodeToMemory(&pem.Block{ 161 | Type: "PUBLIC KEY", 162 | Bytes: pubASN1, 163 | }) 164 | return pubPEM 165 | } 166 | -------------------------------------------------------------------------------- /pkg/compliance/compliance.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 compliance 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "fmt" 21 | 22 | "github.com/interlynk-io/sbomqs/pkg/compliance/fsct" 23 | "github.com/interlynk-io/sbomqs/pkg/logger" 24 | "github.com/interlynk-io/sbomqs/pkg/sbom" 25 | ) 26 | 27 | //nolint:revive,stylecheck 28 | const ( 29 | BSI_REPORT = "BSI" 30 | BSI_V2_REPORT = "BSI-V2" 31 | NTIA_REPORT = "NTIA" 32 | OCT_TELCO = "OCT" 33 | FSCT_V3 = "FSCT" 34 | ) 35 | 36 | func validReportTypes() map[string]bool { 37 | return map[string]bool{ 38 | BSI_REPORT: true, 39 | BSI_V2_REPORT: true, 40 | NTIA_REPORT: true, 41 | OCT_TELCO: true, 42 | FSCT_V3: true, 43 | } 44 | } 45 | 46 | //nolint:revive,stylecheck 47 | func ComplianceResult(ctx context.Context, doc sbom.Document, reportType, fileName, outFormat string, coloredOutput bool) error { 48 | log := logger.FromContext(ctx) 49 | log.Debug("compliance.ComplianceResult()") 50 | 51 | if !validReportTypes()[reportType] { 52 | log.Debugf("Invalid report type: %s\n", reportType) 53 | return errors.New("invalid report type") 54 | } 55 | 56 | if doc == nil { 57 | log.Debugf("sbom document is nil\n") 58 | return errors.New("sbom document is nil") 59 | } 60 | 61 | if fileName == "" { 62 | log.Debugf("file name is empty\n") 63 | return errors.New("file name is empty") 64 | } 65 | 66 | if outFormat == "" { 67 | log.Debugf("output format is empty\n") 68 | return errors.New("output format is empty") 69 | } 70 | 71 | switch { 72 | case reportType == BSI_REPORT: 73 | bsiResult(ctx, doc, fileName, outFormat, coloredOutput) 74 | 75 | case reportType == BSI_V2_REPORT: 76 | bsiV2Result(ctx, doc, fileName, outFormat) 77 | 78 | case reportType == NTIA_REPORT: 79 | ntiaResult(ctx, doc, fileName, outFormat, coloredOutput) 80 | 81 | case reportType == OCT_TELCO: 82 | if doc.Spec().GetSpecType() != "spdx" { 83 | fmt.Println("The Provided SBOM spec is other than SPDX. Open Chain Telco only support SPDX specs SBOMs.") 84 | return nil 85 | } 86 | octResult(ctx, doc, fileName, outFormat, coloredOutput) 87 | 88 | case reportType == FSCT_V3: 89 | fsct.Result(ctx, doc, fileName, outFormat, coloredOutput) 90 | 91 | default: 92 | fmt.Println("No compliance type is provided") 93 | 94 | } 95 | 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /pkg/compliance/db/db.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 db 16 | 17 | import ( 18 | "fmt" 19 | ) 20 | 21 | type DB struct { 22 | keyRecords map[int][]*Record // store record as a value of a Map with a key as a "check_key" 23 | idRecords map[string][]*Record // store record as a value of a Map with a key as a "id" 24 | idKeyRecords map[string]map[int][]*Record // store record as a value of a Map with a key as a "check_key an id" 25 | allIDs map[string]struct{} // Set of all unique ids 26 | } 27 | 28 | // newDB initializes and returns a new database instance. 29 | func NewDB() *DB { 30 | return &DB{ 31 | keyRecords: make(map[int][]*Record), 32 | idRecords: make(map[string][]*Record), 33 | idKeyRecords: make(map[string]map[int][]*Record), 34 | allIDs: make(map[string]struct{}), 35 | } 36 | } 37 | 38 | // addRecord adds a single record to the database 39 | func (d *DB) AddRecord(r *Record) { 40 | // store record using a key 41 | d.keyRecords[r.CheckKey] = append(d.keyRecords[r.CheckKey], r) 42 | 43 | // store record using a id 44 | d.idRecords[r.ID] = append(d.idRecords[r.ID], r) 45 | if d.idKeyRecords[r.ID] == nil { 46 | d.idKeyRecords[r.ID] = make(map[int][]*Record) 47 | } 48 | 49 | // store record using a key and id 50 | d.idKeyRecords[r.ID][r.CheckKey] = append(d.idKeyRecords[r.ID][r.CheckKey], r) 51 | 52 | d.allIDs[r.ID] = struct{}{} 53 | } 54 | 55 | // addRecords adds multiple records to the database 56 | func (d *DB) AddRecords(rs []*Record) { 57 | for _, r := range rs { 58 | d.AddRecord(r) 59 | } 60 | } 61 | 62 | // getRecords retrieves records by the given "check_key" 63 | func (d *DB) GetRecords(key int) []*Record { 64 | return d.keyRecords[key] 65 | } 66 | 67 | // getAllIDs retrieves all unique ids in the database 68 | func (d *DB) GetAllIDs() []string { 69 | ids := make([]string, 0, len(d.allIDs)) 70 | for id := range d.allIDs { 71 | ids = append(ids, id) 72 | } 73 | return ids 74 | } 75 | 76 | // getRecordsByID retrieves records by the given "id" 77 | func (d *DB) GetRecordsByID(id string) []*Record { 78 | return d.idRecords[id] 79 | } 80 | 81 | // getRecordsByKeyID retrieves records by the given "check_key" and "id" 82 | func (d *DB) GetRecordsByKeyID(key int, id string) []*Record { 83 | return d.idKeyRecords[id][key] 84 | } 85 | 86 | // dumpAll prints all records, optionally filtered by the given keys 87 | // nolint 88 | func (d *DB) dumpAll(keys []int) { 89 | for _, records := range d.keyRecords { 90 | for _, r := range records { 91 | if len(keys) == 0 { 92 | fmt.Printf("id: %s, key: %d, value: %s\n", r.ID, r.CheckKey, r.CheckValue) 93 | continue 94 | } 95 | for _, k := range keys { 96 | if r.CheckKey == k { 97 | fmt.Printf("id: %s, key: %d, value: %s\n", r.ID, r.CheckKey, r.CheckValue) 98 | } 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /pkg/compliance/db/db_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 db 16 | 17 | import ( 18 | "fmt" 19 | "math/rand" 20 | "testing" 21 | ) 22 | 23 | const ( 24 | numRecords = 1000000 // Number of records to test with 25 | ) 26 | 27 | // Generate large data set 28 | func generateRecords(n int) []*Record { 29 | var records []*Record 30 | for i := 0; i < n; i++ { 31 | records = append(records, &Record{ 32 | CheckKey: rand.Intn(1000), // #nosec 33 | CheckValue: fmt.Sprintf("value_%d", i), 34 | ID: fmt.Sprintf("id_%d", rand.Intn(1000)), // #nosec 35 | Score: rand.Float64() * 100, // #nosec 36 | Required: rand.Intn(2) == 0, // #nosec 37 | }) 38 | } 39 | return records 40 | } 41 | 42 | // Benchmark original db implementation 43 | func BenchmarkOriginalDB(b *testing.B) { 44 | records := generateRecords(numRecords) 45 | db := NewDB() 46 | 47 | // Benchmark insertion 48 | b.Run("Insert", func(b *testing.B) { 49 | for i := 0; i < b.N; i++ { 50 | db.AddRecords(records) 51 | } 52 | }) 53 | 54 | // Benchmark retrieval by key 55 | b.Run("GetByKey", func(b *testing.B) { 56 | for i := 0; i < b.N; i++ { 57 | db.GetRecords(rand.Intn(1000)) // #nosec 58 | } 59 | }) 60 | 61 | // Benchmark retrieval by ID 62 | b.Run("GetByID", func(b *testing.B) { 63 | for i := 0; i < b.N; i++ { 64 | db.GetRecordsByID(fmt.Sprintf("id_%d", rand.Intn(1000))) // #nosec 65 | } 66 | }) 67 | 68 | // Benchmark for combined retrieval by key and ID case 69 | b.Run("GetByKeyAndIDTogether", func(b *testing.B) { 70 | for i := 0; i < b.N; i++ { 71 | key := rand.Intn(1000) // #nosec 72 | id := fmt.Sprintf("id_%d", rand.Intn(1000)) // #nosec 73 | db.GetRecordsByKeyID(key, id) 74 | } 75 | }) 76 | 77 | // Benchmark for retrieval of all IDs case 78 | b.Run("GetAllIDs", func(b *testing.B) { 79 | for i := 0; i < b.N; i++ { 80 | db.GetAllIDs() 81 | } 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /pkg/compliance/db/record.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 db 16 | 17 | type Record struct { 18 | CheckKey int 19 | CheckValue string 20 | ID string 21 | Score float64 22 | Required bool 23 | Maturity string 24 | } 25 | 26 | func NewRecord() *Record { 27 | return &Record{} 28 | } 29 | 30 | func NewRecordStmt(key int, id, value string, score float64, maturity string) *Record { 31 | r := NewRecord() 32 | r.CheckKey = key 33 | r.CheckValue = value 34 | r.ID = id 35 | r.Score = score 36 | r.Required = true 37 | r.Maturity = maturity 38 | return r 39 | } 40 | 41 | func NewRecordStmtOptional(key int, id, value string, score float64) *Record { 42 | r := NewRecord() 43 | r.CheckKey = key 44 | r.CheckValue = value 45 | r.ID = id 46 | r.Score = score 47 | r.Required = false 48 | return r 49 | } 50 | -------------------------------------------------------------------------------- /pkg/compliance/fsct/fsct_score.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 fsct 16 | 17 | import "github.com/interlynk-io/sbomqs/pkg/compliance/db" 18 | 19 | type fsctScoreResult struct { 20 | id string 21 | requiredScore float64 22 | optionalScore float64 23 | requiredRecords int 24 | optionalRecords int 25 | } 26 | 27 | func newFsctScoreResult(id string) *fsctScoreResult { 28 | return &fsctScoreResult{id: id} 29 | } 30 | 31 | func (r *fsctScoreResult) totalScore() float64 { 32 | if r.requiredRecords == 0 && r.optionalRecords == 0 { 33 | return 0.0 34 | } 35 | 36 | if r.requiredRecords != 0 && r.optionalRecords != 0 { 37 | return (r.totalRequiredScore() + r.totalOptionalScore()) / 2 38 | } 39 | 40 | if r.requiredRecords == 0 && r.optionalRecords != 0 { 41 | return r.totalOptionalScore() 42 | } 43 | 44 | return r.totalRequiredScore() 45 | } 46 | 47 | func (r *fsctScoreResult) totalRequiredScore() float64 { 48 | if r.requiredRecords == 0 { 49 | return 0.0 50 | } 51 | 52 | return r.requiredScore / float64(r.requiredRecords) 53 | } 54 | 55 | func (r *fsctScoreResult) totalOptionalScore() float64 { 56 | if r.optionalRecords == 0 { 57 | return 0.0 58 | } 59 | 60 | return r.optionalScore / float64(r.optionalRecords) 61 | } 62 | 63 | func fsctKeyIDScore(db *db.DB, key int, id string) *fsctScoreResult { 64 | records := db.GetRecordsByKeyID(key, id) 65 | 66 | if len(records) == 0 { 67 | return newFsctScoreResult(id) 68 | } 69 | 70 | requiredScore := 0.0 71 | optionalScore := 0.0 72 | 73 | requiredRecs := 0 74 | optionalRecs := 0 75 | 76 | for _, r := range records { 77 | if r.Required { 78 | requiredScore += r.Score 79 | requiredRecs++ 80 | } else { 81 | optionalScore += r.Score 82 | optionalRecs++ 83 | } 84 | } 85 | 86 | return &fsctScoreResult{ 87 | id: id, 88 | requiredScore: requiredScore, 89 | optionalScore: optionalScore, 90 | requiredRecords: requiredRecs, 91 | optionalRecords: optionalRecs, 92 | } 93 | } 94 | 95 | func fsctIDScore(db *db.DB, id string) *fsctScoreResult { 96 | records := db.GetRecordsByID(id) 97 | 98 | if len(records) == 0 { 99 | return newFsctScoreResult(id) 100 | } 101 | 102 | requiredScore := 0.0 103 | optionalScore := 0.0 104 | 105 | requiredRecs := 0 106 | optionalRecs := 0 107 | 108 | for _, r := range records { 109 | if r.Required { 110 | requiredScore += r.Score 111 | requiredRecs++ 112 | } else { 113 | optionalScore += r.Score 114 | optionalRecs++ 115 | } 116 | } 117 | 118 | return &fsctScoreResult{ 119 | id: id, 120 | requiredScore: requiredScore, 121 | optionalScore: optionalScore, 122 | requiredRecords: requiredRecs, 123 | optionalRecords: optionalRecs, 124 | } 125 | } 126 | 127 | func fsctAggregateScore(db *db.DB) *fsctScoreResult { 128 | var results []fsctScoreResult 129 | var finalResult fsctScoreResult 130 | 131 | ids := db.GetAllIDs() 132 | for _, id := range ids { 133 | results = append(results, *fsctIDScore(db, id)) 134 | } 135 | 136 | for _, r := range results { 137 | finalResult.requiredScore += r.requiredScore 138 | finalResult.optionalScore += r.optionalScore 139 | finalResult.requiredRecords += r.requiredRecords 140 | finalResult.optionalRecords += r.optionalRecords 141 | } 142 | 143 | return &finalResult 144 | } 145 | -------------------------------------------------------------------------------- /pkg/compliance/ntia_report.go: -------------------------------------------------------------------------------- 1 | package compliance 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "sort" 8 | "time" 9 | 10 | "github.com/google/uuid" 11 | "github.com/interlynk-io/sbomqs/pkg/compliance/common" 12 | "github.com/interlynk-io/sbomqs/pkg/compliance/db" 13 | "github.com/olekukonko/tablewriter" 14 | "sigs.k8s.io/release-utils/version" 15 | ) 16 | 17 | var ntiaSectionDetails = map[int]ntiaSection{ 18 | SBOM_MACHINE_FORMAT: {Title: "Automation Support", ID: "1.1", Required: true, DataField: "Machine-Readable Formats"}, 19 | SBOM_CREATOR: {Title: "Required fields sboms ", ID: "2.1", Required: true, DataField: "Author"}, 20 | SBOM_TIMESTAMP: {Title: "Required fields sboms", ID: "2.2", Required: true, DataField: "Timestamp"}, 21 | SBOM_DEPENDENCY: {Title: "Required fields sboms", ID: "2.3", Required: true, DataField: "Dependencies"}, 22 | COMP_NAME: {Title: "Required fields components", ID: "2.4", Required: true, DataField: "Package Name"}, 23 | COMP_DEPTH: {Title: "Required fields components", ID: "2.5", Required: true, DataField: "Dependencies on other components"}, 24 | COMP_CREATOR: {Title: "Required fields component", ID: "2.6", Required: true, DataField: "Package Supplier"}, 25 | PACK_SUPPLIER: {Title: "Required fields component", ID: "2.6", Required: true, DataField: "Package Supplier"}, 26 | COMP_VERSION: {Title: "Required fields components", ID: "2.7", Required: true, DataField: "Package Version"}, 27 | COMP_OTHER_UNIQ_IDS: {Title: "Required fields component", ID: "2.8", Required: true, DataField: "Other Uniq IDs"}, 28 | } 29 | 30 | type ntiaSection struct { 31 | Title string `json:"section_title"` 32 | ID string `json:"section_id"` 33 | DataField string `json:"section_data_field"` 34 | Required bool `json:"required"` 35 | ElementID string `json:"element_id"` 36 | ElementResult string `json:"element_result"` 37 | Score float64 `json:"score"` 38 | } 39 | 40 | type ntiaComplianceReport struct { 41 | Name string `json:"report_name"` 42 | Subtitle string `json:"subtitle"` 43 | Revision string `json:"revision"` 44 | Run run `json:"run"` 45 | Tool tool `json:"tool"` 46 | Summary Summary `json:"summary"` 47 | Sections []ntiaSection `json:"sections"` 48 | } 49 | 50 | func newNtiaJSONReport() *ntiaComplianceReport { 51 | return &ntiaComplianceReport{ 52 | Name: "NTIA-minimum elements Compliance Report", 53 | Subtitle: "Part 2: Software Bill of Materials (SBOM)", 54 | Revision: "", 55 | Run: run{ 56 | ID: uuid.New().String(), 57 | GeneratedAt: time.Now().UTC().Format(time.RFC3339), 58 | FileName: "", 59 | EngineVersion: "1", 60 | }, 61 | Tool: tool{ 62 | Name: "sbomqs", 63 | Version: version.GetVersionInfo().GitVersion, 64 | Vendor: "Interlynk (support@interlynk.io)", 65 | }, 66 | } 67 | } 68 | 69 | func ntiaJSONReport(db *db.DB, fileName string) { 70 | jr := newNtiaJSONReport() 71 | jr.Run.FileName = fileName 72 | 73 | score := ntiaAggregateScore(db) 74 | summary := Summary{} 75 | summary.MaxScore = 10.0 76 | summary.TotalScore = score.totalScore() 77 | summary.TotalRequiredScore = score.totalRequiredScore() 78 | summary.TotalOptionalScore = score.totalOptionalScore() 79 | 80 | jr.Summary = summary 81 | jr.Sections = ntiaConstructSections(db) 82 | 83 | o, _ := json.MarshalIndent(jr, "", " ") 84 | fmt.Println(string(o)) 85 | } 86 | 87 | func ntiaConstructSections(db *db.DB) []ntiaSection { 88 | var sections []ntiaSection 89 | allIDs := db.GetAllIDs() 90 | for _, id := range allIDs { 91 | records := db.GetRecordsByID(id) 92 | 93 | for _, r := range records { 94 | section := ntiaSectionDetails[r.CheckKey] 95 | newSection := ntiaSection{ 96 | Title: section.Title, 97 | ID: section.ID, 98 | DataField: section.DataField, 99 | Required: section.Required, 100 | } 101 | score := ntiaKeyIDScore(db, r.CheckKey, r.ID) 102 | newSection.Score = score.totalScore() 103 | if r.ID == "doc" { 104 | newSection.ElementID = "sbom" 105 | } else { 106 | newSection.ElementID = r.ID 107 | } 108 | 109 | newSection.ElementResult = r.CheckValue 110 | 111 | sections = append(sections, newSection) 112 | } 113 | } 114 | // Group sections by ElementID 115 | sectionsByElementID := make(map[string][]ntiaSection) 116 | for _, section := range sections { 117 | sectionsByElementID[section.ElementID] = append(sectionsByElementID[section.ElementID], section) 118 | } 119 | 120 | // Sort each group of sections by section.ID and ensure "SBOM Data Fields" comes first within its group if it exists 121 | var sortedSections []ntiaSection 122 | var sbomLevelSections []ntiaSection 123 | for elementID, group := range sectionsByElementID { 124 | sort.Slice(group, func(i, j int) bool { 125 | return group[i].ID < group[j].ID 126 | }) 127 | if elementID == "SBOM Level" { 128 | sbomLevelSections = group 129 | } else { 130 | sortedSections = append(sortedSections, group...) 131 | } 132 | } 133 | 134 | // Place "SBOM Level" sections at the top 135 | sortedSections = append(sbomLevelSections, sortedSections...) 136 | 137 | return sortedSections 138 | } 139 | 140 | func ntiaDetailedReport(db *db.DB, fileName string, colorOutput bool) { 141 | table := tablewriter.NewWriter(os.Stdout) 142 | score := ntiaAggregateScore(db) 143 | 144 | fmt.Printf("NTIA Report\n") 145 | fmt.Printf("Compliance score by Interlynk Score:%0.1f RequiredScore:%0.1f OptionalScore:%0.1f for %s\n", score.totalScore(), score.totalRequiredScore(), score.totalOptionalScore(), fileName) 146 | fmt.Printf("* indicates optional fields\n") 147 | table.SetHeader([]string{"ELEMENT ID", "Section ID", "NTIA minimum elements", "Result", "Score"}) 148 | table.SetRowLine(true) 149 | table.SetAutoMergeCellsByColumnIndex([]int{0}) 150 | 151 | sections := ntiaConstructSections(db) 152 | 153 | // Sort sections by ElementId and then by SectionId 154 | sort.Slice(sections, func(i, j int) bool { 155 | if sections[i].ElementID == sections[j].ElementID { 156 | return sections[i].ID < sections[j].ID 157 | } 158 | return sections[i].ElementID < sections[j].ElementID 159 | }) 160 | 161 | for _, section := range sections { 162 | sectionID := section.ID 163 | if !section.Required { 164 | sectionID = sectionID + "*" 165 | } 166 | 167 | if colorOutput { 168 | // disable tablewriter's auto-wrapping 169 | table.SetAutoWrapText(false) 170 | columnWidth := 30 171 | common.SetHeaderColor(table, 5) 172 | 173 | table = common.ColorTable(table, 174 | section.ElementID, 175 | section.ID, 176 | section.ElementResult, 177 | section.DataField, 178 | section.Score, 179 | columnWidth) 180 | } else { 181 | table.Append([]string{section.ElementID, sectionID, section.DataField, section.ElementResult, fmt.Sprintf("%0.1f", section.Score)}) 182 | } 183 | } 184 | table.Render() 185 | } 186 | 187 | func ntiaBasicReport(db *db.DB, fileName string) { 188 | score := ntiaAggregateScore(db) 189 | fmt.Printf("NTIA Report\n") 190 | fmt.Printf("Score:%0.1f RequiredScore:%0.1f OptionalScore:%0.1f for %s\n", score.totalScore(), score.totalRequiredScore(), score.totalOptionalScore(), fileName) 191 | } 192 | -------------------------------------------------------------------------------- /pkg/compliance/ntia_score.go: -------------------------------------------------------------------------------- 1 | package compliance 2 | 3 | import "github.com/interlynk-io/sbomqs/pkg/compliance/db" 4 | 5 | type ntiaScoreResult struct { 6 | id string 7 | requiredScore float64 8 | optionalScore float64 9 | requiredRecords int 10 | optionalRecords int 11 | } 12 | 13 | func newNtiaScoreResult(id string) *ntiaScoreResult { 14 | return &ntiaScoreResult{id: id} 15 | } 16 | 17 | func (r *ntiaScoreResult) totalScore() float64 { 18 | if r.requiredRecords == 0 && r.optionalRecords == 0 { 19 | return 0.0 20 | } 21 | 22 | if r.requiredRecords != 0 && r.optionalRecords != 0 { 23 | return (r.totalRequiredScore() + r.totalOptionalScore()) / 2 24 | } 25 | 26 | if r.requiredRecords == 0 && r.optionalRecords != 0 { 27 | return r.totalOptionalScore() 28 | } 29 | 30 | return r.totalRequiredScore() 31 | } 32 | 33 | func (r *ntiaScoreResult) totalRequiredScore() float64 { 34 | if r.requiredRecords == 0 { 35 | return 0.0 36 | } 37 | 38 | return r.requiredScore / float64(r.requiredRecords) 39 | } 40 | 41 | func (r *ntiaScoreResult) totalOptionalScore() float64 { 42 | if r.optionalRecords == 0 { 43 | return 0.0 44 | } 45 | 46 | return r.optionalScore / float64(r.optionalRecords) 47 | } 48 | 49 | func ntiaKeyIDScore(db *db.DB, key int, id string) *ntiaScoreResult { 50 | records := db.GetRecordsByKeyID(key, id) 51 | 52 | if len(records) == 0 { 53 | return newNtiaScoreResult(id) 54 | } 55 | 56 | requiredScore := 0.0 57 | optionalScore := 0.0 58 | 59 | requiredRecs := 0 60 | optionalRecs := 0 61 | 62 | for _, r := range records { 63 | if r.Required { 64 | requiredScore += r.Score 65 | requiredRecs++ 66 | } else { 67 | optionalScore += r.Score 68 | optionalRecs++ 69 | } 70 | } 71 | 72 | return &ntiaScoreResult{ 73 | id: id, 74 | requiredScore: requiredScore, 75 | optionalScore: optionalScore, 76 | requiredRecords: requiredRecs, 77 | optionalRecords: optionalRecs, 78 | } 79 | } 80 | 81 | func ntiaAggregateScore(db *db.DB) *ntiaScoreResult { 82 | var results []ntiaScoreResult 83 | var finalResult ntiaScoreResult 84 | 85 | ids := db.GetAllIDs() 86 | for _, id := range ids { 87 | results = append(results, *ntiaIDScore(db, id)) 88 | } 89 | 90 | for _, r := range results { 91 | finalResult.requiredScore += r.requiredScore 92 | finalResult.optionalScore += r.optionalScore 93 | finalResult.requiredRecords += r.requiredRecords 94 | finalResult.optionalRecords += r.optionalRecords 95 | } 96 | 97 | return &finalResult 98 | } 99 | 100 | func ntiaIDScore(db *db.DB, id string) *ntiaScoreResult { 101 | records := db.GetRecordsByID(id) 102 | 103 | if len(records) == 0 { 104 | return newNtiaScoreResult(id) 105 | } 106 | 107 | requiredScore := 0.0 108 | optionalScore := 0.0 109 | 110 | requiredRecs := 0 111 | optionalRecs := 0 112 | 113 | for _, r := range records { 114 | if r.Required { 115 | requiredScore += r.Score 116 | requiredRecs++ 117 | } else { 118 | optionalScore += r.Score 119 | optionalRecs++ 120 | } 121 | } 122 | 123 | return &ntiaScoreResult{ 124 | id: id, 125 | requiredScore: requiredScore, 126 | optionalScore: optionalScore, 127 | requiredRecords: requiredRecs, 128 | optionalRecords: optionalRecs, 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /pkg/compliance/oct_score.go: -------------------------------------------------------------------------------- 1 | package compliance 2 | 3 | import "github.com/interlynk-io/sbomqs/pkg/compliance/db" 4 | 5 | type octScoreResult struct { 6 | id string 7 | requiredScore float64 8 | optionalScore float64 9 | requiredRecords int 10 | optionalRecords int 11 | } 12 | 13 | func newOctScoreResult(id string) *octScoreResult { 14 | return &octScoreResult{id: id} 15 | } 16 | 17 | func (r *octScoreResult) totalScore() float64 { 18 | if r.requiredRecords == 0 && r.optionalRecords == 0 { 19 | return 0.0 20 | } 21 | 22 | if r.requiredRecords != 0 && r.optionalRecords != 0 { 23 | return (r.totalRequiredScore() + r.totalOptionalScore()) / 2 24 | } 25 | 26 | if r.requiredRecords == 0 && r.optionalRecords != 0 { 27 | return r.totalOptionalScore() 28 | } 29 | 30 | return r.totalRequiredScore() 31 | } 32 | 33 | func (r *octScoreResult) totalRequiredScore() float64 { 34 | if r.requiredRecords == 0 { 35 | return 0.0 36 | } 37 | 38 | return r.requiredScore / float64(r.requiredRecords) 39 | } 40 | 41 | func (r *octScoreResult) totalOptionalScore() float64 { 42 | if r.optionalRecords == 0 { 43 | return 0.0 44 | } 45 | 46 | return r.optionalScore / float64(r.optionalRecords) 47 | } 48 | 49 | func octKeyIDScore(dtb *db.DB, key int, id string) *octScoreResult { 50 | records := dtb.GetRecordsByKeyID(key, id) 51 | 52 | if len(records) == 0 { 53 | return newOctScoreResult(id) 54 | } 55 | 56 | requiredScore := 0.0 57 | optionalScore := 0.0 58 | 59 | requiredRecs := 0 60 | optionalRecs := 0 61 | 62 | for _, r := range records { 63 | if r.Required { 64 | requiredScore += r.Score 65 | requiredRecs++ 66 | } else { 67 | optionalScore += r.Score 68 | optionalRecs++ 69 | } 70 | } 71 | 72 | return &octScoreResult{ 73 | id: id, 74 | requiredScore: requiredScore, 75 | optionalScore: optionalScore, 76 | requiredRecords: requiredRecs, 77 | optionalRecords: optionalRecs, 78 | } 79 | } 80 | 81 | func octAggregateScore(dtb *db.DB) *octScoreResult { 82 | var results []octScoreResult 83 | var finalResult octScoreResult 84 | 85 | ids := dtb.GetAllIDs() 86 | for _, id := range ids { 87 | results = append(results, *octIDScore(dtb, id)) 88 | } 89 | 90 | for _, r := range results { 91 | finalResult.requiredScore += r.requiredScore 92 | finalResult.optionalScore += r.optionalScore 93 | finalResult.requiredRecords += r.requiredRecords 94 | finalResult.optionalRecords += r.optionalRecords 95 | } 96 | 97 | return &finalResult 98 | } 99 | 100 | func octIDScore(dtb *db.DB, id string) *octScoreResult { 101 | records := dtb.GetRecordsByID(id) 102 | 103 | if len(records) == 0 { 104 | return newOctScoreResult(id) 105 | } 106 | 107 | requiredScore := 0.0 108 | optionalScore := 0.0 109 | 110 | requiredRecs := 0 111 | optionalRecs := 0 112 | 113 | for _, r := range records { 114 | if r.Required { 115 | requiredScore += r.Score 116 | requiredRecs++ 117 | } else { 118 | optionalScore += r.Score 119 | optionalRecs++ 120 | } 121 | } 122 | 123 | return &octScoreResult{ 124 | id: id, 125 | requiredScore: requiredScore, 126 | optionalScore: optionalScore, 127 | requiredRecords: requiredRecs, 128 | optionalRecords: optionalRecs, 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /pkg/cpe/cpe.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 | package cpe 15 | 16 | import ( 17 | "regexp" 18 | ) 19 | 20 | type CPE string 21 | 22 | const cpeRegex = (`^([c][pP][eE]:/[AHOaho]?(:[A-Za-z0-9\._\-~%]*){0,6})`) + 23 | (`|(cpe:2\.3:[aho\*\-](:(((\?*|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!"#$$%&'\(\)\+,/:;<=>@\[\]\^\x60\{\|}~]))+(\?*|\*?))|[\*\-])){5}(:(([a-zA-Z]{2,3}(-([a-zA-Z]{2}|[0-9]{3}))?)|[\*\-]))(:(((\?*|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!"#$$%&'\(\)\+,/:;<=>@\[\]\^\x60\{\|}~]))+(\?*|\*?))|[\*\-])){4})$`) 24 | 25 | func (cpe CPE) Valid() bool { 26 | return regexp.MustCompile(cpeRegex).MatchString(cpe.String()) 27 | } 28 | 29 | func NewCPE(cpe string) CPE { 30 | return CPE(cpe) 31 | } 32 | 33 | func (cpe CPE) String() string { 34 | return string(cpe) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/cpe/cpe_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 | package cpe 15 | 16 | import ( 17 | "testing" 18 | ) 19 | 20 | func TestValidCPE(t *testing.T) { 21 | var tests = []struct { 22 | name string 23 | input string 24 | want bool 25 | }{ 26 | {"Is empty value is valid CPE2.3", "", false}, 27 | {"Is XYZ is valid CPE2.3", "xyz", false}, 28 | {"Is cpe:-2.3:a:CycloneDX:cyclonedx-go:v0.7.0:*:*:*:*:*:*:* is valid CPE2.3", "cpe:-2.3:a:CycloneDX:cyclonedx-go:v0.7.0:*:*:*:*:*:*:*", false}, 29 | {"Is cpe:2.3:a:interlynk:sbomqs:\\(devel\\):*:*:*:*:*:*:* is valid CPE2.3", "cpe:2.3:a:interlynk:sbomqs:\\(devel\\):*:*:*:*:*:*:*", true}, 30 | {"Is cpe:/a:%40thi.ng%2fegf_project:%40thi.ng%2fegf:0.2.0::~~~node.js~~ is valid CPE2.2", "cpe:/a:%40thi.ng%2fegf_project:%40thi.ng%2fegf:0.2.0::~~~node.js~~", true}, 31 | } 32 | for _, tt := range tests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | cpeinput := NewCPE(tt.input) 35 | if cpeinput.Valid() != tt.want { 36 | t.Errorf("got %t, want %t", cpeinput.Valid(), tt.want) 37 | } 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pkg/engine/compliance.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 engine 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "os" 21 | 22 | "github.com/interlynk-io/sbomqs/pkg/compliance" 23 | "github.com/interlynk-io/sbomqs/pkg/compliance/common" 24 | "github.com/interlynk-io/sbomqs/pkg/logger" 25 | "github.com/interlynk-io/sbomqs/pkg/sbom" 26 | "github.com/spf13/afero" 27 | ) 28 | 29 | func ComplianceRun(ctx context.Context, ep *Params) error { 30 | log := logger.FromContext(ctx) 31 | log.Debug("engine.ComplianceRun()") 32 | 33 | if len(ep.Path) <= 0 { 34 | log.Fatal("path is required") 35 | } 36 | 37 | log.Debugf("Config: %+v", ep) 38 | 39 | doc, err := getSbomDocument(ctx, ep) 40 | if err != nil { 41 | log.Debugf("getSbomDocument failed for file :%s\n", ep.Path[0]) 42 | fmt.Printf("failed to get sbom document for %s\n", ep.Path[0]) 43 | return err 44 | } 45 | 46 | var reportType string 47 | 48 | switch { 49 | case ep.Bsi: 50 | reportType = "BSI" 51 | case ep.BsiV2: 52 | reportType = "BSI-V2" 53 | case ep.Oct: 54 | reportType = "OCT" 55 | case ep.Fsct: 56 | reportType = "FSCT" 57 | default: 58 | reportType = "NTIA" 59 | } 60 | 61 | var outFormat string 62 | 63 | switch { 64 | case ep.Basic: 65 | outFormat = "basic" 66 | case ep.JSON: 67 | outFormat = "json" 68 | default: 69 | outFormat = "detailed" 70 | } 71 | 72 | coloredOutput := ep.Color 73 | 74 | err = compliance.ComplianceResult(ctx, *doc, reportType, ep.Path[0], outFormat, coloredOutput) 75 | if err != nil { 76 | log.Debugf("compliance.ComplianceResult failed for file :%s\n", ep.Path[0]) 77 | fmt.Printf("failed to get compliance result for %s\n", ep.Path[0]) 78 | return err 79 | } 80 | 81 | log.Debugf("Compliance Report: %s\n", ep.Path[0]) 82 | return nil 83 | } 84 | 85 | func getSbomDocument(ctx context.Context, ep *Params) (*sbom.Document, error) { 86 | log := logger.FromContext(ctx) 87 | log.Debugf("engine.getSbomDocument()") 88 | 89 | path := ep.Path[0] 90 | blob := ep.Path[0] 91 | signature := ep.Signature 92 | publicKey := ep.PublicKey 93 | 94 | if signature == "" && publicKey == "" { 95 | standaloneSBOMFile, signatureRetrieved, publicKeyRetrieved, err := common.RetrieveSignatureFromSBOM(ctx, blob) 96 | if err != nil { 97 | log.Debug("failed to retrieve signature and public key from embedded sbom: %w", err) 98 | } 99 | blob = standaloneSBOMFile 100 | signature = signatureRetrieved 101 | publicKey = publicKeyRetrieved 102 | } 103 | 104 | sig := sbom.Signature{ 105 | SigValue: signature, 106 | PublicKey: publicKey, 107 | Blob: blob, 108 | } 109 | var doc sbom.Document 110 | 111 | if IsURL(path) { 112 | log.Debugf("Processing Git URL path :%s\n", path) 113 | url, sbomFilePath := path, path 114 | var err error 115 | 116 | if IsGit(url) { 117 | sbomFilePath, url, err = handleURL(path) 118 | if err != nil { 119 | log.Fatal("failed to get sbomFilePath, rawURL: %w", err) 120 | } 121 | } 122 | fs := afero.NewMemMapFs() 123 | 124 | file, err := fs.Create(sbomFilePath) 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | f, err := ProcessURL(url, file) 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | doc, err = sbom.NewSBOMDocument(ctx, f, sig) 135 | if err != nil { 136 | log.Fatalf("failed to parse SBOM document: %w", err) 137 | } 138 | } else { 139 | if _, err := os.Stat(path); err != nil { 140 | log.Debugf("os.Stat failed for file :%s\n", path) 141 | fmt.Printf("failed to stat %s\n", path) 142 | return nil, err 143 | } 144 | 145 | f, err := os.Open(path) 146 | if err != nil { 147 | log.Debugf("os.Open failed for file :%s\n", path) 148 | fmt.Printf("failed to open %s\n", path) 149 | return nil, err 150 | } 151 | defer f.Close() 152 | 153 | doc, err = sbom.NewSBOMDocument(ctx, f, sig) 154 | if err != nil { 155 | log.Debugf("failed to create sbom document for :%s\n", path) 156 | log.Debugf("%s\n", err) 157 | fmt.Printf("failed to parse %s : %s\n", path, err) 158 | return nil, err 159 | } 160 | } 161 | 162 | return &doc, nil 163 | } 164 | -------------------------------------------------------------------------------- /pkg/engine/dtrack.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 engine 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "os" 21 | "strings" 22 | "time" 23 | 24 | dtrack "github.com/DependencyTrack/client-go" 25 | "github.com/google/uuid" 26 | "github.com/interlynk-io/sbomqs/pkg/logger" 27 | "github.com/interlynk-io/sbomqs/pkg/reporter" 28 | "github.com/interlynk-io/sbomqs/pkg/sbom" 29 | "github.com/interlynk-io/sbomqs/pkg/scorer" 30 | "github.com/samber/lo" 31 | ) 32 | 33 | type DtParams struct { 34 | URL string 35 | APIKey string 36 | ProjectIDs []uuid.UUID 37 | 38 | JSON bool 39 | Basic bool 40 | Detailed bool 41 | 42 | TagProjectWithScore bool 43 | Timeout int // handle cutom timeout 44 | } 45 | 46 | func DtrackScore(ctx context.Context, dtP *DtParams) error { 47 | log := logger.FromContext(ctx) 48 | log.Debug("engine.DtrackScore()") 49 | 50 | log.Debugf("Config: %+v", dtP) 51 | 52 | timeout := time.Duration(dtP.Timeout) * time.Second 53 | 54 | log.Debug("Timeout set to: ", timeout) 55 | 56 | dTrackClient, err := dtrack.NewClient(dtP.URL, 57 | dtrack.WithAPIKey(dtP.APIKey), dtrack.WithTimeout(timeout), dtrack.WithDebug(false)) 58 | if err != nil { 59 | log.Fatalf("Failed to create Dependency-Track client: %s", err) 60 | } 61 | 62 | for _, pid := range dtP.ProjectIDs { 63 | log.Debugf("Processing project %s", pid) 64 | 65 | prj, err := dTrackClient.Project.Get(ctx, pid) 66 | if err != nil { 67 | log.Fatalf("Failed to get project: %s", err) 68 | } 69 | 70 | bom, err := dTrackClient.BOM.ExportProject(ctx, pid, dtrack.BOMFormatJSON, dtrack.BOMVariantInventory) 71 | if err != nil { 72 | log.Fatalf("Failed to export project: %s", err) 73 | } 74 | 75 | { 76 | fname := fmt.Sprintf("tmpfile-%s", pid) 77 | f, err := os.CreateTemp("", fname) 78 | if err != nil { 79 | log.Fatal(err) 80 | } 81 | 82 | defer f.Close() 83 | defer os.Remove(f.Name()) 84 | 85 | _, err = f.WriteString(bom) 86 | if err != nil { 87 | log.Fatalf("Failed to write string: %v", err) 88 | } 89 | 90 | ep := &Params{} 91 | ep.Path = append(ep.Path, f.Name()) 92 | doc, scores, err := processFile(ctx, ep, ep.Path[0], nil) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | if dtP.TagProjectWithScore { 98 | log.Debugf("Project: %+v", prj.Tags) 99 | // remove old score 100 | prj.Tags = lo.Filter(prj.Tags, func(t dtrack.Tag, _ int) bool { 101 | return !strings.HasPrefix(t.Name, "sbomqs=") 102 | }) 103 | 104 | tag := fmt.Sprintf("sbomqs=%0.1f", scores.AvgScore()) 105 | prj.Tags = append(prj.Tags, dtrack.Tag{Name: tag}) 106 | 107 | log.Debugf("Tagging project with %s", tag) 108 | log.Debugf("Project: %+v", prj.Tags) 109 | 110 | _, err = dTrackClient.Project.Update(ctx, prj) 111 | if err != nil { 112 | log.Fatalf("Failed to tag project: %s", err) 113 | } 114 | } 115 | 116 | path := fmt.Sprintf("ID: %s, Name: %s, Version: %s", prj.UUID, prj.Name, prj.Version) 117 | 118 | reportFormat := "detailed" 119 | if dtP.Basic { 120 | reportFormat = "basic" 121 | } else if dtP.JSON { 122 | reportFormat = "json" 123 | } 124 | 125 | nr := reporter.NewReport(ctx, 126 | []sbom.Document{doc}, 127 | []scorer.Scores{scores}, 128 | []string{path}, 129 | reporter.WithFormat(reportFormat)) 130 | nr.Report() 131 | } 132 | } 133 | 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /pkg/engine/list.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 engine 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/interlynk-io/sbomqs/pkg/list" 21 | "github.com/interlynk-io/sbomqs/pkg/logger" 22 | ) 23 | 24 | func parseListParams(ep *Params) *list.Params { 25 | return &list.Params{ 26 | Path: ep.Path, 27 | Features: ep.Features, 28 | JSON: ep.JSON, 29 | Basic: ep.Basic, 30 | Detailed: ep.Detailed, 31 | Color: ep.Color, 32 | Missing: ep.Missing, 33 | Debug: ep.Debug, 34 | } 35 | } 36 | 37 | func ListRun(ctx context.Context, ep *Params) error { 38 | log := logger.FromContext(ctx) 39 | log.Debug("engine.ListRun()") 40 | 41 | lep := parseListParams(ep) 42 | 43 | // Process the SBOMs and features 44 | _, err := list.ComponentsListResult(ctx, lep) 45 | if err != nil { 46 | log.Debugf("failed to process SBOMs: %v", err) 47 | return err 48 | } 49 | 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /pkg/engine/score_test.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/spf13/afero" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestHandleURL(t *testing.T) { 14 | testCases := []struct { 15 | name string 16 | input string 17 | expectedPath string 18 | expectedRawURL string 19 | expectedError bool 20 | }{ 21 | { 22 | name: "Valid URL", 23 | input: "https://github.com/interlynk-io/sbomqs/blob/main/samples/sbomqs-spdx-syft.json", 24 | expectedPath: "samples/sbomqs-spdx-syft.json", 25 | expectedRawURL: "https://raw.githubusercontent.com/interlynk-io/sbomqs/main/samples/sbomqs-spdx-syft.json", 26 | expectedError: false, 27 | }, 28 | { 29 | name: "Valid URL with direct file", 30 | input: "https://github.com/viveksahu26/go-url/blob/main/spdx.json", 31 | expectedError: false, 32 | expectedPath: "spdx.json", 33 | expectedRawURL: "https://raw.githubusercontent.com/viveksahu26/go-url/main/spdx.json", 34 | }, 35 | { 36 | name: "Invalid URL with not enough parts", 37 | input: "https://github.com/interlynk-io/sbomqs/blob/main/", 38 | expectedPath: "", 39 | expectedRawURL: "", 40 | expectedError: true, 41 | }, 42 | { 43 | name: "Malformed URL", 44 | input: "invalid-url", 45 | expectedPath: "", 46 | expectedRawURL: "", 47 | expectedError: true, 48 | }, 49 | } 50 | 51 | for _, tc := range testCases { 52 | t.Run(tc.name, func(t *testing.T) { 53 | sbomFilePath, rawURL, err := handleURL(tc.input) 54 | if tc.expectedError { 55 | assert.Error(t, err) 56 | assert.Equal(t, tc.expectedPath, sbomFilePath) 57 | assert.Equal(t, tc.expectedRawURL, rawURL) 58 | } else { 59 | assert.NoError(t, err) 60 | assert.Equal(t, tc.expectedPath, sbomFilePath) 61 | assert.Equal(t, tc.expectedRawURL, rawURL) 62 | } 63 | }) 64 | } 65 | } 66 | 67 | // TestProcessURL function 68 | func TestProcessURL(t *testing.T) { 69 | tests := []struct { 70 | name string 71 | url string 72 | statusCode int 73 | expectedError bool 74 | expectedErrorMessage error 75 | }{ 76 | { 77 | name: "Successful download", 78 | url: "https://github.com/interlynk-io/sbomqs/blob/main/samples/sbomqs-spdx-syft.json", 79 | statusCode: http.StatusOK, 80 | expectedError: false, 81 | expectedErrorMessage: nil, 82 | }, 83 | { 84 | name: "Failed to get data", 85 | url: "http://example.com/file.txt", 86 | statusCode: http.StatusNotFound, 87 | expectedError: true, 88 | expectedErrorMessage: fmt.Errorf("failed to download file: %s %s", "404", http.StatusText(http.StatusNotFound)), 89 | }, 90 | } 91 | 92 | for _, tt := range tests { 93 | t.Run(tt.name, func(t *testing.T) { 94 | fs := afero.NewMemMapFs() 95 | file, err := fs.Create("testfile.txt") 96 | if err != nil { 97 | log.Fatalf("error: %v", err) 98 | } 99 | 100 | _, err = ProcessURL(tt.url, file) 101 | if tt.expectedError { 102 | assert.EqualError(t, err, tt.expectedErrorMessage.Error()) 103 | } else { 104 | assert.NoError(t, err) 105 | } 106 | }) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /pkg/engine/share.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 engine 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "strings" 21 | 22 | "github.com/interlynk-io/sbomqs/pkg/logger" 23 | "github.com/interlynk-io/sbomqs/pkg/reporter" 24 | "github.com/interlynk-io/sbomqs/pkg/sbom" 25 | "github.com/interlynk-io/sbomqs/pkg/scorer" 26 | "github.com/interlynk-io/sbomqs/pkg/share" 27 | ) 28 | 29 | func ShareRun(ctx context.Context, ep *Params) error { 30 | log := logger.FromContext(ctx) 31 | log.Debug("engine.ShareRun()") 32 | 33 | if len(ep.Path) <= 0 { 34 | log.Fatal("path is required") 35 | } 36 | 37 | doc, scores, err := processFile(ctx, ep, ep.Path[0], nil) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | url, err := share.Share(ctx, doc, scores, ep.Path[0]) 43 | if err != nil { 44 | fmt.Printf("Error sharing file %s: %s", ep.Path, err) 45 | return err 46 | } 47 | nr := reporter.NewReport(ctx, 48 | []sbom.Document{doc}, 49 | []scorer.Scores{scores}, 50 | []string{ep.Path[0]}, 51 | reporter.WithFormat(strings.ToLower("basic"))) 52 | nr.Report() 53 | fmt.Printf("ShareLink: %s\n", url) 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /pkg/licenses/embed_licenses.go: -------------------------------------------------------------------------------- 1 | package licenses 2 | 3 | import ( 4 | "embed" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "strings" 9 | ) 10 | 11 | var ( 12 | //go:embed files 13 | res embed.FS 14 | 15 | licenses = map[string]string{ 16 | "spdx": "files/licenses_spdx.json", 17 | "spdxException": "files/licenses_spdx_exception.json", 18 | "aboutcode": "files/licenses_aboutcode.json", 19 | } 20 | ) 21 | 22 | type spdxLicense struct { 23 | Version string `json:"licenseListVersion"` 24 | Licenses []spdxLicenseDetail `json:"licenses"` 25 | Exceptions []spdxLicenseDetail `json:"exceptions"` 26 | } 27 | 28 | type spdxLicenseDetail struct { 29 | Reference string `json:"reference"` 30 | IsDeprecated bool `json:"isDeprecatedLicenseId"` 31 | DetailsURL string `json:"detailsUrl"` 32 | ReferenceNumber int `json:"referenceNumber"` 33 | Name string `json:"name"` 34 | LicenseID string `json:"licenseId"` 35 | LicenseExceptionID string `json:"licenseExceptionId"` 36 | SeeAlso []string `json:"seeAlso"` 37 | IsOsiApproved bool `json:"isOsiApproved"` 38 | IsFsfLibre bool `json:"isFsfLibre"` 39 | } 40 | 41 | type aboutCodeLicenseDetail struct { 42 | LicenseKey string `json:"license_key"` 43 | Category string `json:"category"` 44 | SpdxLicenseKey string `json:"spdx_license_key"` 45 | OtherSpdxLicenseKeys []string `json:"other_spdx_license_keys"` 46 | Exception bool `json:"is_exception"` 47 | Deprecated bool `json:"is_deprecated"` 48 | JSON string `json:"json"` 49 | Yaml string `json:"yaml"` 50 | HTML string `json:"html"` 51 | License string `json:"license"` 52 | } 53 | 54 | var ( 55 | licenseList = map[string]meta{} 56 | licenseListAboutCode = map[string]meta{} 57 | ) 58 | 59 | func loadSpdxLicense() error { 60 | licData, err := res.ReadFile(licenses["spdx"]) 61 | if err != nil { 62 | fmt.Printf("error: %v\n", err) 63 | return err 64 | } 65 | 66 | var sl spdxLicense 67 | if err := json.Unmarshal(licData, &sl); err != nil { 68 | fmt.Printf("error: %v\n", err) 69 | return err 70 | } 71 | 72 | for _, l := range sl.Licenses { 73 | licenseList[l.LicenseID] = meta{ 74 | name: l.Name, 75 | short: l.LicenseID, 76 | deprecated: l.IsDeprecated, 77 | osiApproved: l.IsOsiApproved, 78 | fsfLibre: l.IsFsfLibre, 79 | restrictive: false, 80 | exception: false, 81 | freeAnyUse: false, 82 | source: "spdx", 83 | } 84 | } 85 | // fmt.Printf("loaded %d licenses\n", len(licenseList)) 86 | return nil 87 | } 88 | 89 | func loadSpdxExceptions() error { 90 | licData, err := res.ReadFile(licenses["spdxException"]) 91 | if err != nil { 92 | fmt.Printf("error: %v\n", err) 93 | return err 94 | } 95 | 96 | var sl spdxLicense 97 | if err := json.Unmarshal(licData, &sl); err != nil { 98 | fmt.Printf("error: %v\n", err) 99 | return err 100 | } 101 | 102 | for _, l := range sl.Exceptions { 103 | licenseList[l.LicenseExceptionID] = meta{ 104 | name: l.Name, 105 | short: l.LicenseExceptionID, 106 | deprecated: l.IsDeprecated, 107 | osiApproved: l.IsOsiApproved, 108 | fsfLibre: l.IsFsfLibre, 109 | restrictive: false, 110 | exception: true, 111 | freeAnyUse: false, 112 | source: "spdx", 113 | } 114 | } 115 | // fmt.Printf("loaded %d licenses\n", len(licenseList)) 116 | 117 | return nil 118 | } 119 | 120 | func loadAboutCodeLicense() error { 121 | licData, err := res.ReadFile(licenses["aboutcode"]) 122 | if err != nil { 123 | fmt.Printf("error: %v\n", err) 124 | return err 125 | } 126 | 127 | var acl []aboutCodeLicenseDetail 128 | 129 | if err := json.Unmarshal(licData, &acl); err != nil { 130 | fmt.Printf("error: %v\n", err) 131 | return err 132 | } 133 | 134 | isRestrictive := func(category string) bool { 135 | lowerCategory := strings.ToLower(category) 136 | 137 | if strings.Contains(lowerCategory, "copyleft") { 138 | return true 139 | } 140 | 141 | if strings.Contains(lowerCategory, "restricted") { 142 | return true 143 | } 144 | 145 | return false 146 | } 147 | 148 | isFreeAnyUse := func(category string) bool { 149 | lowerCategory := strings.ToLower(category) 150 | return strings.Contains(lowerCategory, "public") 151 | } 152 | 153 | for _, l := range acl { 154 | for _, otherKey := range l.OtherSpdxLicenseKeys { 155 | licenseListAboutCode[otherKey] = meta{ 156 | name: l.LicenseKey, 157 | short: otherKey, 158 | deprecated: l.Deprecated, 159 | osiApproved: false, 160 | fsfLibre: false, 161 | restrictive: isRestrictive(l.Category), 162 | exception: l.Exception, 163 | freeAnyUse: isFreeAnyUse(l.Category), 164 | source: "aboutcode", 165 | } 166 | } 167 | 168 | licenseListAboutCode[l.SpdxLicenseKey] = meta{ 169 | name: l.LicenseKey, 170 | short: l.SpdxLicenseKey, 171 | deprecated: l.Deprecated, 172 | osiApproved: false, 173 | fsfLibre: false, 174 | restrictive: isRestrictive(l.Category), 175 | exception: l.Exception, 176 | freeAnyUse: isFreeAnyUse(l.Category), 177 | source: "aboutcode", 178 | } 179 | } 180 | // fmt.Printf("loaded %d licenses\n", len(LicenseListAboutCode)) 181 | 182 | return nil 183 | } 184 | 185 | func init() { 186 | err := loadSpdxLicense() 187 | if err != nil { 188 | log.Printf("Failed to load spdx license: %v", err) 189 | } 190 | err = loadSpdxExceptions() 191 | if err != nil { 192 | log.Printf("Failed to load spdx exceptions: %v", err) 193 | } 194 | err = loadAboutCodeLicense() 195 | if err != nil { 196 | log.Printf("Failed to load about code license: %v", err) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /pkg/licenses/license.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 | "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 | Custom() bool 35 | Spdx() bool 36 | AboutCode() bool 37 | } 38 | 39 | type meta struct { 40 | name string 41 | short string 42 | deprecated bool 43 | osiApproved bool 44 | fsfLibre bool 45 | freeAnyUse bool 46 | restrictive bool 47 | exception bool 48 | source string 49 | } 50 | 51 | func (m meta) Name() string { 52 | return m.name 53 | } 54 | 55 | func (m meta) ShortID() string { 56 | return m.short 57 | } 58 | 59 | func (m meta) Deprecated() bool { 60 | return m.deprecated 61 | } 62 | 63 | func (m meta) OsiApproved() bool { 64 | return m.osiApproved 65 | } 66 | 67 | func (m meta) FsfLibre() bool { 68 | return m.fsfLibre 69 | } 70 | 71 | func (m meta) FreeAnyUse() bool { 72 | return m.freeAnyUse 73 | } 74 | 75 | func (m meta) Restrictive() bool { 76 | return m.restrictive 77 | } 78 | 79 | func (m meta) Exception() bool { 80 | return m.exception 81 | } 82 | 83 | func (m meta) Source() string { 84 | return m.source 85 | } 86 | 87 | func (m meta) Custom() bool { 88 | return m.source == "custom" 89 | } 90 | 91 | func (m meta) Spdx() bool { 92 | return m.source == "spdx" 93 | } 94 | 95 | func (m meta) AboutCode() bool { 96 | return m.source == "aboutcode" 97 | } 98 | 99 | func LookupSpdxLicense(licenseKey string) (License, error) { 100 | if licenseKey == "" { 101 | return nil, errors.New("license not found") 102 | } 103 | 104 | lowerKey := strings.ToLower(licenseKey) 105 | 106 | if lowerKey == "none" || lowerKey == "noassertion" { 107 | return nil, errors.New("license not found") 108 | } 109 | 110 | tLicKey := strings.TrimRight(licenseKey, "+") 111 | 112 | // Lookup spdx & exception list 113 | license, ok := licenseList[tLicKey] 114 | 115 | if !ok { 116 | return nil, errors.New("license not found") 117 | } 118 | 119 | return license, nil 120 | } 121 | 122 | func LookupAboutCodeLicense(licenseKey string) (License, error) { 123 | if licenseKey == "" { 124 | return nil, errors.New("license not found") 125 | } 126 | 127 | lowerKey := strings.ToLower(licenseKey) 128 | 129 | if lowerKey == "none" || lowerKey == "noassertion" { 130 | return nil, errors.New("license not found") 131 | } 132 | 133 | tLicKey := strings.TrimRight(licenseKey, "+") 134 | 135 | license, ok := licenseListAboutCode[tLicKey] 136 | 137 | if !ok { 138 | return nil, errors.New("license not found") 139 | } 140 | 141 | return license, nil 142 | } 143 | 144 | func LookupExpression(expression string, customLicenses []License) []License { 145 | customLookup := func(licenseKey string) (License, error) { 146 | if len(customLicenses) == 0 { 147 | return nil, errors.New("license not found") 148 | } 149 | 150 | for _, l := range customLicenses { 151 | if l.ShortID() == licenseKey { 152 | return l, nil 153 | } 154 | } 155 | return nil, errors.New("license not found") 156 | } 157 | 158 | lExp := strings.ToLower(expression) 159 | 160 | if expression == "" || lExp == "none" || lExp == "noassertion" { 161 | return []License{} 162 | } 163 | 164 | extLicenses, err := spdxexp.ExtractLicenses(expression) 165 | if err != nil { 166 | return []License{CreateCustomLicense(expression, expression)} 167 | } 168 | 169 | licenses := []License{} 170 | 171 | for _, l := range extLicenses { 172 | trimLicenseKey := strings.TrimRight(l, "+") 173 | 174 | license, err := LookupSpdxLicense(trimLicenseKey) 175 | if err == nil { 176 | licenses = append(licenses, license) 177 | continue 178 | } 179 | 180 | license, err = LookupAboutCodeLicense(trimLicenseKey) 181 | if err == nil { 182 | licenses = append(licenses, license) 183 | continue 184 | } 185 | 186 | //if custom license list is provided use that. 187 | license, err = customLookup(trimLicenseKey) 188 | if err == nil { 189 | licenses = append(licenses, license) 190 | continue 191 | } 192 | 193 | //if nothing else this license is custom 194 | licenses = append(licenses, CreateCustomLicense(trimLicenseKey, trimLicenseKey)) 195 | } 196 | 197 | return licenses 198 | } 199 | 200 | func CreateCustomLicense(id, name string) License { 201 | return meta{ 202 | name: name, 203 | short: id, 204 | deprecated: false, 205 | osiApproved: false, 206 | fsfLibre: false, 207 | freeAnyUse: false, 208 | restrictive: false, 209 | exception: false, 210 | source: "custom", 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /pkg/list/types.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 list 16 | 17 | type Result struct { 18 | FilePath string 19 | Feature string 20 | Missing bool 21 | TotalComponents int 22 | Components []ComponentResult // For component-based features 23 | DocumentProperty DocumentResult // For SBOM-based features 24 | Errors []string 25 | } 26 | 27 | type ComponentResult struct { 28 | Name string 29 | Version string 30 | Values string 31 | } 32 | 33 | type DocumentResult struct { 34 | Key string // e.g., "Authors", "Creation Timestamp" 35 | Value string // e.g., "John Doe", "2023-01-12T22:06:03Z" 36 | Present bool // Indicates if the property is present 37 | } 38 | 39 | type Params struct { 40 | Path []string 41 | 42 | // input control 43 | Features []string 44 | 45 | // output control 46 | JSON bool 47 | Basic bool 48 | Detailed bool 49 | Color bool 50 | 51 | Missing bool 52 | 53 | Debug bool 54 | } 55 | -------------------------------------------------------------------------------- /pkg/logger/log.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 logger 16 | 17 | import ( 18 | "context" 19 | "log" 20 | 21 | "go.uber.org/zap" 22 | ) 23 | 24 | var logger *zap.SugaredLogger 25 | 26 | type logKey struct{} 27 | 28 | func InitProdLogger() { 29 | l, err := zap.NewProduction() 30 | if err != nil { 31 | log.Fatalf("Failed to initialize logger: %v", err) 32 | } 33 | 34 | defer func() { 35 | if err := logger.Sync(); err != nil { 36 | return 37 | } 38 | }() 39 | 40 | if logger != nil { 41 | panic("logger already initialized") 42 | } 43 | logger = l.Sugar() 44 | } 45 | 46 | func InitDebugLogger() { 47 | l, err := zap.NewDevelopment() 48 | if err != nil { 49 | log.Printf("Failed to zap new development: %v", err) 50 | } 51 | 52 | defer func() { 53 | if err := logger.Sync(); err != nil { 54 | return 55 | } 56 | }() 57 | 58 | if logger != nil { 59 | panic("logger already initialized") 60 | } 61 | logger = l.Sugar() 62 | } 63 | 64 | func WithLogger(ctx context.Context) context.Context { 65 | return context.WithValue(ctx, logKey{}, logger) 66 | } 67 | 68 | func WithLoggerAndCancel(ctx context.Context) (context.Context, context.CancelFunc) { 69 | return context.WithCancel(context.WithValue(ctx, logKey{}, logger)) 70 | } 71 | 72 | func FromContext(ctx context.Context) *zap.SugaredLogger { 73 | if logger, ok := ctx.Value(logKey{}).(*zap.SugaredLogger); ok { 74 | return logger 75 | } 76 | 77 | return zap.NewNop().Sugar() 78 | } 79 | -------------------------------------------------------------------------------- /pkg/omniborid/omniborid.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 omniborid 16 | 17 | import "regexp" 18 | 19 | type OMNIBORID string 20 | 21 | const omniRegex = `^gitoid:blob:sha1:[a-fA-F0-9]{40}$` 22 | 23 | func (omni OMNIBORID) Valid() bool { 24 | return regexp.MustCompile(omniRegex).MatchString(omni.String()) 25 | } 26 | 27 | func NewOmni(omni string) OMNIBORID { 28 | return OMNIBORID(omni) 29 | } 30 | 31 | func (omni OMNIBORID) String() string { 32 | return string(omni) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/omniborid/omniborid_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 | package omniborid 15 | 16 | import ( 17 | "testing" 18 | ) 19 | 20 | func TestValidOMNIBORID(t *testing.T) { 21 | tests := []struct { 22 | name string 23 | input string 24 | want bool 25 | }{ 26 | {"Is empty value a valid OMNIBORID", "", false}, 27 | {"Is XYZ a valid OMNIBORID", "xyz", false}, 28 | {"Is gitoid:blob:sha1:a94a8fe5ccb19ba61c4c0873d391e987982fbbd3 a valid OMNIBORID", "gitoid:blob:sha1:a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", true}, 29 | {"Is gitoid:blob:sha1:a94a8fe5ccb19ba61c4c0873d391e987982fbbd3a a valid OMNIBORID", "gitoid:blob:sha1:a94a8fe5ccb19ba61c4c0873d391e987982fbbd3a", false}, 30 | } 31 | for _, tt := range tests { 32 | t.Run(tt.name, func(t *testing.T) { 33 | omniInput := NewOmni(tt.input) 34 | if omniInput.Valid() != tt.want { 35 | t.Errorf("got %t, want %t", omniInput.Valid(), tt.want) 36 | } 37 | }) 38 | } 39 | } 40 | 41 | func TestString(t *testing.T) { 42 | tests := []struct { 43 | name string 44 | input string 45 | want string 46 | }{ 47 | {"Empty OMNIBORID value", "", ""}, 48 | {"Valid OMNIBORID", "gitoid:blob:sha1:a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", "gitoid:blob:sha1:a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"}, 49 | } 50 | for _, tt := range tests { 51 | t.Run(tt.name, func(t *testing.T) { 52 | omniInput := NewOmni(tt.input) 53 | if omniInput.String() != tt.want { 54 | t.Errorf("got %s, want %s", omniInput.String(), tt.want) 55 | } 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pkg/purl/purl.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 purl 16 | 17 | import ( 18 | pkg_purl "github.com/package-url/packageurl-go" 19 | ) 20 | 21 | type PURL string 22 | 23 | func NewPURL(prl string) PURL { 24 | return PURL(prl) 25 | } 26 | 27 | func (p PURL) Valid() bool { 28 | _, err := pkg_purl.FromString(p.String()) 29 | return err == nil 30 | } 31 | 32 | func (p PURL) String() string { 33 | return string(p) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/purl/purl_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 purl 16 | 17 | import ( 18 | "testing" 19 | ) 20 | 21 | func TestValid(t *testing.T) { 22 | var tests = []struct { 23 | name string 24 | input string 25 | want bool 26 | }{ 27 | {"Is empty value is valid PURL", "", false}, 28 | {"Is XYZ is valid PURL", "xyz", false}, 29 | {"Is pkg golang/github.com/CycloneDX/cyclonedx-go@v0.7.0 is valid PURL", "pkg,golang/github.com/CycloneDX/cyclonedx-go@v0.7.0", false}, 30 | {"Is pkg:golang/github.com/CycloneDX/cyclonedx-go@v0.7.0 is valid PURL", "pkg:golang/github.com/CycloneDX/cyclonedx-go@v0.7.0", true}, 31 | } 32 | for _, tt := range tests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | input := NewPURL(tt.input) 35 | if input.Valid() != tt.want { 36 | t.Errorf("got %t, want %t", input.Valid(), tt.want) 37 | } 38 | }) 39 | } 40 | } 41 | 42 | func TestString(t *testing.T) { 43 | var tests = []struct { 44 | name string 45 | input string 46 | want string 47 | }{ 48 | {"Empty PURL value", "", ""}, 49 | {"valid PURL", "pkg:golang/github.com/CycloneDX/cyclonedx-go@v0.7.0", "pkg:golang/github.com/CycloneDX/cyclonedx-go@v0.7.0"}, 50 | } 51 | for _, tt := range tests { 52 | t.Run(tt.name, func(t *testing.T) { 53 | input := NewPURL(tt.input) 54 | if input.String() != tt.want { 55 | t.Errorf("got %s, want %s", input.String(), tt.want) 56 | } 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pkg/reporter/basic.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 reporter 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | ) 21 | 22 | func (r *Reporter) simpleReport() { 23 | for index, path := range r.Paths { 24 | scores := r.Scores[index] 25 | doc := r.Docs[index] 26 | 27 | format := doc.Spec().FileFormat() 28 | spec := doc.Spec().GetSpecType() 29 | specVersion := doc.Spec().GetVersion() 30 | 31 | if spec == "spdx" { 32 | specVersion = strings.Replace(specVersion, "SPDX-", "", 1) 33 | } 34 | 35 | if spec == "cyclonedx" { 36 | spec = "cdx" 37 | } 38 | 39 | fmt.Printf("%0.1f\t%s\t%s\t%s\t%s\n", scores.AvgScore(), spec, specVersion, format, path) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /pkg/reporter/detailed.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 reporter 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "sort" 21 | "strings" 22 | 23 | "github.com/fatih/color" 24 | "github.com/olekukonko/tablewriter" 25 | ) 26 | 27 | func (r *Reporter) detailedReport() { 28 | for index, path := range r.Paths { 29 | doc := r.Docs[index] 30 | scores := r.Scores[index] 31 | colorOp := r.Color 32 | outDoc := [][]string{} 33 | 34 | for _, score := range scores.ScoreList() { 35 | var l []string 36 | if score.Ignore() { 37 | l = []string{score.Category(), score.Feature(), " - ", score.Descr()} 38 | } else { 39 | l = []string{score.Category(), score.Feature(), fmt.Sprintf("%0.1f/10.0", score.Score()), score.Descr()} 40 | } 41 | outDoc = append(outDoc, l) 42 | } 43 | 44 | sort.Slice(outDoc, func(i, j int) bool { 45 | switch strings.Compare(outDoc[i][0], outDoc[j][0]) { 46 | case -1: 47 | return true 48 | case 1: 49 | return false 50 | } 51 | return outDoc[i][1] < outDoc[j][1] 52 | }) 53 | 54 | fmt.Printf("SBOM Quality by Interlynk Score:%0.1f\tcomponents:%d\t%s\n", scores.AvgScore(), len(doc.Components()), path) 55 | 56 | // Initialize tablewriter table with borders 57 | table := tablewriter.NewWriter(os.Stdout) 58 | table.SetHeader([]string{"Category", "Feature", "Score", "Desc"}) 59 | table.SetRowLine(true) 60 | table.SetAutoMergeCellsByColumnIndex([]int{0}) 61 | 62 | if colorOp { 63 | for _, row := range outDoc { 64 | scoreText := row[2] 65 | scoreValue := parseScore(row[2]) 66 | 67 | // Apply color based on the score value 68 | var coloredScore string 69 | switch { 70 | case scoreValue < 5.0: 71 | coloredScore = color.New(color.FgRed).Sprintf("%s", scoreText) 72 | default: 73 | coloredScore = color.New(color.FgGreen).Sprintf("%s", scoreText) 74 | } 75 | coloredCategory := color.New(color.FgHiMagenta).Sprint(row[0]) 76 | coloredFeature := color.New(color.FgHiCyan).Sprint(row[1]) 77 | coloredDesc := color.New(color.FgHiBlue).Sprint(row[3]) 78 | 79 | table.Append([]string{coloredCategory, coloredFeature, coloredScore, coloredDesc}) 80 | } 81 | } else { 82 | table.AppendBulk(outDoc) 83 | } 84 | 85 | table.Render() 86 | } 87 | } 88 | 89 | // parseScore extracts the numeric score value from a formatted score string (e.g., "9.7/10.0"). 90 | func parseScore(scoreStr string) float64 { 91 | var scoreValue float64 92 | if _, err := fmt.Sscanf(scoreStr, "%f", &scoreValue); err != nil { 93 | fmt.Printf("Error scanning score: %v\n", err) 94 | } 95 | 96 | return scoreValue 97 | } 98 | -------------------------------------------------------------------------------- /pkg/reporter/json.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 reporter 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "time" 21 | 22 | "github.com/google/uuid" 23 | "github.com/interlynk-io/sbomqs/pkg/scorer" 24 | "sigs.k8s.io/release-utils/version" 25 | ) 26 | 27 | type score struct { 28 | Category string `json:"category"` 29 | Feature string `json:"feature"` 30 | Score float64 `json:"score"` 31 | MaxScore float64 `json:"max_score"` 32 | Desc string `json:"description"` 33 | Ignored bool `json:"ignored"` 34 | } 35 | type file struct { 36 | Name string `json:"file_name"` 37 | Spec string `json:"spec"` 38 | SpecVersion string `json:"spec_version"` 39 | Format string `json:"file_format"` 40 | AvgScore float64 `json:"avg_score"` 41 | Components int `json:"num_components"` 42 | CreationTime string `json:"creation_time"` 43 | ToolName string `json:"gen_tool_name"` 44 | ToolVersion string `json:"gen_tool_version"` 45 | Scores []*score `json:"scores"` 46 | } 47 | 48 | type creation struct { 49 | Name string `json:"name"` 50 | Version string `json:"version"` 51 | ScoringEngine string `json:"scoring_engine_version"` 52 | Vendor string `json:"vendor"` 53 | } 54 | 55 | type jsonReport struct { 56 | RunID string `json:"run_id"` 57 | TimeStamp string `json:"timestamp"` 58 | CreationInfo creation `json:"creation_info"` 59 | Files []file `json:"files"` 60 | } 61 | 62 | func newJSONReport() *jsonReport { 63 | return &jsonReport{ 64 | RunID: uuid.New().String(), 65 | TimeStamp: time.Now().UTC().Format(time.RFC3339), 66 | CreationInfo: creation{ 67 | Name: "sbomqs", 68 | Version: version.GetVersionInfo().GitVersion, 69 | ScoringEngine: scorer.EngineVersion, 70 | Vendor: "Interlynk (support@interlynk.io)", 71 | }, 72 | Files: []file{}, 73 | } 74 | } 75 | 76 | func (r *Reporter) jsonReport(onlyResponse bool) (string, error) { 77 | jr := newJSONReport() 78 | for index, path := range r.Paths { 79 | doc := r.Docs[index] 80 | scores := r.Scores[index] 81 | 82 | f := file{} 83 | f.AvgScore = scores.AvgScore() 84 | f.Components = len(doc.Components()) 85 | f.Format = doc.Spec().FileFormat() 86 | f.Name = path 87 | f.Spec = doc.Spec().GetSpecType() 88 | f.SpecVersion = doc.Spec().GetVersion() 89 | tools := doc.Tools() 90 | 91 | if len(tools) > 0 { 92 | // Use the first tool for now 93 | f.ToolName = tools[0].GetName() 94 | f.ToolVersion = tools[0].GetVersion() 95 | } 96 | f.CreationTime = doc.Spec().GetCreationTimestamp() 97 | 98 | for _, ss := range scores.ScoreList() { 99 | ns := new(score) 100 | ns.Category = ss.Category() 101 | ns.Feature = ss.Feature() 102 | ns.Score = ss.Score() 103 | ns.MaxScore = ss.MaxScore() 104 | ns.Desc = ss.Descr() 105 | ns.Ignored = ss.Ignore() 106 | 107 | f.Scores = append(f.Scores, ns) 108 | } 109 | 110 | jr.Files = append(jr.Files, f) 111 | } 112 | o, err := json.MarshalIndent(jr, "", " ") 113 | if err != nil { 114 | return "", err 115 | } 116 | if !onlyResponse { 117 | fmt.Println(string(o)) 118 | } 119 | return string(o), nil 120 | } 121 | -------------------------------------------------------------------------------- /pkg/reporter/report.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 reporter 16 | 17 | import ( 18 | "context" 19 | "log" 20 | 21 | "github.com/interlynk-io/sbomqs/pkg/sbom" 22 | "github.com/interlynk-io/sbomqs/pkg/scorer" 23 | ) 24 | 25 | type Reporter struct { 26 | Ctx context.Context 27 | 28 | Docs []sbom.Document 29 | Scores []scorer.Scores 30 | Paths []string 31 | 32 | // optional params 33 | Format string 34 | Color bool 35 | } 36 | 37 | var ReportFormats = []string{"basic", "detailed", "json"} 38 | 39 | type Option func(r *Reporter) 40 | 41 | func WithFormat(c string) Option { 42 | return func(r *Reporter) { 43 | r.Format = c 44 | } 45 | } 46 | 47 | func WithColor(c bool) Option { 48 | return func(r *Reporter) { 49 | r.Color = c 50 | } 51 | } 52 | 53 | func NewReport(ctx context.Context, doc []sbom.Document, scores []scorer.Scores, paths []string, opts ...Option) *Reporter { 54 | r := &Reporter{ 55 | Ctx: ctx, 56 | Docs: doc, 57 | Scores: scores, 58 | Paths: paths, 59 | } 60 | 61 | for _, opt := range opts { 62 | opt(r) 63 | } 64 | return r 65 | } 66 | 67 | func (r *Reporter) Report() { 68 | if r.Format == "basic" { 69 | r.simpleReport() 70 | } else if r.Format == "detailed" { 71 | r.detailedReport() 72 | } else if r.Format == "json" { 73 | _, err := r.jsonReport(false) 74 | if err != nil { 75 | log.Printf("Failed to print json report: %v", err) 76 | } 77 | } else { 78 | r.detailedReport() 79 | } 80 | } 81 | 82 | func (r *Reporter) ShareReport() (string, error) { 83 | return r.jsonReport(true) 84 | } 85 | -------------------------------------------------------------------------------- /pkg/sbom/author.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 sbom 16 | 17 | //counterfeiter:generate . Author 18 | type GetAuthor interface { 19 | GetName() string 20 | GetType() string 21 | GetEmail() string 22 | GetPhone() string 23 | } 24 | 25 | type Author struct { 26 | Name string 27 | Email string 28 | AuthorType string // person or org 29 | Phone string 30 | } 31 | 32 | func (a Author) GetName() string { 33 | return a.Name 34 | } 35 | 36 | func (a Author) GetType() string { 37 | return a.AuthorType 38 | } 39 | 40 | func (a Author) GetEmail() string { 41 | return a.Email 42 | } 43 | 44 | func (a Author) GetPhone() string { 45 | return a.Phone 46 | } 47 | -------------------------------------------------------------------------------- /pkg/sbom/checksum.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 sbom 16 | 17 | //counterfeiter:generate . Checksum 18 | type GetChecksum interface { 19 | GetAlgo() string 20 | GetContent() string 21 | } 22 | 23 | type Checksum struct { 24 | Alg string 25 | Content string 26 | } 27 | 28 | func (c Checksum) GetAlgo() string { 29 | return c.Alg 30 | } 31 | 32 | func (c Checksum) GetContent() string { 33 | return c.Content 34 | } 35 | -------------------------------------------------------------------------------- /pkg/sbom/component.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 sbom 16 | 17 | //counterfeiter:generate . Component 18 | import ( 19 | "github.com/interlynk-io/sbomqs/pkg/cpe" 20 | "github.com/interlynk-io/sbomqs/pkg/licenses" 21 | "github.com/interlynk-io/sbomqs/pkg/omniborid" 22 | "github.com/interlynk-io/sbomqs/pkg/purl" 23 | "github.com/interlynk-io/sbomqs/pkg/swhid" 24 | "github.com/interlynk-io/sbomqs/pkg/swid" 25 | ) 26 | 27 | type GetComponent interface { 28 | GetID() string 29 | GetName() string 30 | GetVersion() string 31 | GetCpes() []cpe.CPE 32 | GetPurls() []purl.PURL 33 | Swhids() []swhid.SWHID 34 | OmniborIDs() []omniborid.OMNIBORID 35 | Swids() []swid.SWID 36 | Licenses() []licenses.License 37 | DeclaredLicenses() []licenses.License 38 | ConcludedLicenses() []licenses.License 39 | GetChecksums() []GetChecksum 40 | PrimaryPurpose() string 41 | RequiredFields() bool 42 | Suppliers() GetSupplier 43 | Manufacturer() GetManufacturer 44 | CountOfDependencies() int 45 | SourceCodeURL() string 46 | GetDownloadLocationURL() string 47 | SourceCodeHash() string 48 | IsPrimaryComponent() bool 49 | HasRelationShips() bool 50 | RelationShipState() string 51 | GetSpdxID() string 52 | GetFileAnalyzed() bool 53 | GetCopyRight() string 54 | GetPackageLicenseDeclared() string 55 | GetPackageLicenseConcluded() string 56 | ExternalReferences() []GetExternalReference 57 | GetComposition(string) string 58 | GetPrimaryCompInfo() GetPrimaryComp 59 | } 60 | 61 | type Component struct { 62 | Name string 63 | Version string 64 | Cpes []cpe.CPE 65 | Purls []purl.PURL 66 | Swhid []swhid.SWHID 67 | OmniID []omniborid.OMNIBORID 68 | Swid []swid.SWID 69 | licenses []licenses.License 70 | declaredLicense []licenses.License 71 | concludedLicense []licenses.License 72 | Checksums []GetChecksum 73 | purpose string 74 | isReqFieldsPresent bool 75 | ID string 76 | Supplier Supplier 77 | manufacturer Manufacturer 78 | dependenciesCount int 79 | sourceCodeURL string 80 | DownloadLocation string 81 | sourceCodeHash string 82 | isPrimary bool 83 | PrimaryCompt PrimaryComp 84 | hasRelationships bool 85 | RelationshipState string 86 | Spdxid string 87 | FileAnalyzed bool 88 | CopyRight string 89 | PackageLicenseConcluded string 90 | PackageLicenseDeclared string 91 | ExternalRefs []GetExternalReference 92 | composition map[string]string 93 | } 94 | 95 | func NewComponent() *Component { 96 | return &Component{} 97 | } 98 | 99 | func (c Component) GetPrimaryCompInfo() GetPrimaryComp { 100 | return c.PrimaryCompt 101 | } 102 | 103 | func (c Component) GetName() string { 104 | return c.Name 105 | } 106 | 107 | func (c Component) GetVersion() string { 108 | return c.Version 109 | } 110 | 111 | func (c Component) GetPurls() []purl.PURL { 112 | return c.Purls 113 | } 114 | 115 | func (c Component) GetCpes() []cpe.CPE { 116 | return c.Cpes 117 | } 118 | 119 | func (c Component) Swhids() []swhid.SWHID { 120 | return c.Swhid 121 | } 122 | 123 | func (c Component) Swids() []swid.SWID { 124 | return c.Swid 125 | } 126 | 127 | func (c Component) OmniborIDs() []omniborid.OMNIBORID { 128 | return c.OmniID 129 | } 130 | 131 | func (c Component) Licenses() []licenses.License { 132 | return c.licenses 133 | } 134 | 135 | func (c Component) DeclaredLicenses() []licenses.License { 136 | return c.licenses 137 | } 138 | 139 | func (c Component) ConcludedLicenses() []licenses.License { 140 | return c.licenses 141 | } 142 | 143 | func (c Component) GetChecksums() []GetChecksum { 144 | return c.Checksums 145 | } 146 | 147 | func (c Component) PrimaryPurpose() string { 148 | return c.purpose 149 | } 150 | 151 | func (c Component) RequiredFields() bool { 152 | return c.isReqFieldsPresent 153 | } 154 | 155 | func (c Component) GetID() string { 156 | return c.ID 157 | } 158 | 159 | func (c Component) Manufacturer() GetManufacturer { 160 | return c.manufacturer 161 | } 162 | 163 | func (c Component) Suppliers() GetSupplier { 164 | return c.Supplier 165 | } 166 | 167 | func (c Component) CountOfDependencies() int { 168 | return c.dependenciesCount 169 | } 170 | 171 | func (c Component) SourceCodeURL() string { 172 | return c.sourceCodeURL 173 | } 174 | 175 | func (c Component) GetDownloadLocationURL() string { 176 | return c.DownloadLocation 177 | } 178 | 179 | func (c Component) SourceCodeHash() string { 180 | return c.sourceCodeHash 181 | } 182 | 183 | func (c Component) IsPrimaryComponent() bool { 184 | return c.isPrimary 185 | } 186 | 187 | func (c Component) HasRelationShips() bool { 188 | return c.hasRelationships 189 | } 190 | 191 | func (c Component) RelationShipState() string { 192 | return c.RelationshipState 193 | } 194 | 195 | func (c Component) GetSpdxID() string { 196 | return c.Spdxid 197 | } 198 | 199 | func (c Component) GetFileAnalyzed() bool { 200 | return c.FileAnalyzed 201 | } 202 | 203 | func (c Component) GetCopyRight() string { 204 | return c.CopyRight 205 | } 206 | 207 | func (c Component) GetPackageLicenseConcluded() string { 208 | return c.PackageLicenseConcluded 209 | } 210 | 211 | func (c Component) GetPackageLicenseDeclared() string { 212 | return c.PackageLicenseDeclared 213 | } 214 | 215 | func (c Component) ExternalReferences() []GetExternalReference { 216 | return c.ExternalRefs 217 | } 218 | 219 | func (c Component) GetComposition(componentID string) string { 220 | return c.composition[componentID] 221 | } 222 | -------------------------------------------------------------------------------- /pkg/sbom/component_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 sbom 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/interlynk-io/sbomqs/pkg/cpe" 21 | "github.com/interlynk-io/sbomqs/pkg/purl" 22 | ) 23 | 24 | func TestGetCpeFromCompo(t *testing.T) { 25 | tests := []struct { 26 | name string 27 | input []cpe.CPE 28 | want int 29 | }{ 30 | {"get cpe from component", []cpe.CPE{"cpe:-2.3:a:CycloneDX:cyclonedx-go:v0.7.0:*:*:*:*:*:*:*"}, 1}, 31 | {"Is XYZ is valid CPE2.3", []cpe.CPE{""}, 0}, 32 | } 33 | for _, tt := range tests { 34 | t.Run(tt.name, func(t *testing.T) { 35 | cp := Component{ 36 | Cpes: tt.input, 37 | } 38 | if len(tt.input) != len(cp.Cpes) { 39 | t.Errorf("got %d, want %d", len(cp.Cpes), len(tt.input)) 40 | } 41 | }) 42 | } 43 | } 44 | 45 | func Test_component_Purls(t *testing.T) { 46 | tests := []struct { 47 | name string 48 | input []purl.PURL 49 | want int 50 | }{ 51 | {"1 PURL set on component", []purl.PURL{"pkg:golang/github.com/dummy/dummyArrayLib@v2.4.1"}, 1}, 52 | {"0 PURL set on component", []purl.PURL{""}, 0}, 53 | } 54 | for _, tt := range tests { 55 | t.Run(tt.name, func(t *testing.T) { 56 | pl := Component{ 57 | Purls: tt.input, 58 | } 59 | if len(tt.input) != len(pl.Purls) { 60 | t.Errorf("got %d, want %d", len(pl.Purls), len(tt.input)) 61 | } 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /pkg/sbom/contact.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 sbom 16 | 17 | //counterfeiter:generate . Contact 18 | 19 | type GetContact interface { 20 | GetName() string 21 | GetEmail() string 22 | GetPhone() string 23 | } 24 | 25 | type Contact struct { 26 | Name string 27 | Email string 28 | Phone string 29 | } 30 | 31 | func (c Contact) GetName() string { 32 | return c.Name 33 | } 34 | 35 | func (c Contact) GetEmail() string { 36 | return c.Email 37 | } 38 | 39 | func (c Contact) GetPhone() string { 40 | return c.Phone 41 | } 42 | -------------------------------------------------------------------------------- /pkg/sbom/document.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 sbom 16 | 17 | //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate 18 | 19 | //counterfeiter:generate . Document 20 | type Document interface { 21 | Spec() Spec 22 | Components() []GetComponent 23 | Relations() []GetRelation 24 | Authors() []GetAuthor 25 | Tools() []GetTool 26 | Logs() []string 27 | 28 | Lifecycles() []string 29 | Manufacturer() GetManufacturer 30 | Supplier() GetSupplier 31 | 32 | PrimaryComp() GetPrimaryComp 33 | GetRelationships(string) []string 34 | 35 | Vulnerabilities() []GetVulnerabilities 36 | Signature() GetSignature 37 | } 38 | -------------------------------------------------------------------------------- /pkg/sbom/externalReference.go: -------------------------------------------------------------------------------- 1 | package sbom 2 | 3 | type GetExternalReference interface { 4 | GetRefType() string 5 | GetRefLocator() string 6 | } 7 | 8 | type ExternalReference struct { 9 | RefType string 10 | RefLocator string 11 | } 12 | 13 | func (e ExternalReference) GetRefType() string { 14 | return e.RefType 15 | } 16 | 17 | func (e ExternalReference) GetRefLocator() string { 18 | return e.RefLocator 19 | } 20 | -------------------------------------------------------------------------------- /pkg/sbom/manufacturer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 sbom 16 | 17 | //counterfeiter:generate . Manufacturer 18 | 19 | type GetManufacturer interface { 20 | GetName() string 21 | GetURL() string 22 | GetEmail() string 23 | GetContacts() []Contact 24 | } 25 | 26 | type Manufacturer struct { 27 | Name string 28 | URL string 29 | Email string 30 | Contacts []Contact 31 | } 32 | 33 | func (m Manufacturer) GetName() string { 34 | return m.Name 35 | } 36 | 37 | func (m Manufacturer) GetURL() string { 38 | return m.URL 39 | } 40 | 41 | func (m Manufacturer) GetEmail() string { 42 | return m.Email 43 | } 44 | 45 | func (m Manufacturer) GetContacts() []Contact { 46 | return m.Contacts 47 | } 48 | -------------------------------------------------------------------------------- /pkg/sbom/primarycomp.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 sbom 16 | 17 | type GetPrimaryComp interface { 18 | IsPresent() bool 19 | GetID() string 20 | GetName() string 21 | GetVersion() string 22 | GetTotalNoOfDependencies() int 23 | HasDependencies() bool 24 | GetDependencies() []string 25 | } 26 | 27 | type PrimaryComp struct { 28 | Present bool 29 | ID string 30 | Dependecies int 31 | HasDependency bool 32 | Name string 33 | Version string 34 | AllDependencies []string 35 | } 36 | 37 | func (pc PrimaryComp) IsPresent() bool { 38 | return pc.Present 39 | } 40 | 41 | func (pc PrimaryComp) GetID() string { 42 | return pc.ID 43 | } 44 | 45 | func (pc PrimaryComp) GetName() string { 46 | return pc.Name 47 | } 48 | 49 | func (pc PrimaryComp) GetVersion() string { 50 | return pc.Version 51 | } 52 | 53 | func (pc PrimaryComp) GetTotalNoOfDependencies() int { 54 | return pc.Dependecies 55 | } 56 | 57 | func (pc PrimaryComp) HasDependencies() bool { 58 | return pc.HasDependency 59 | } 60 | 61 | func (pc PrimaryComp) GetDependencies() []string { 62 | return pc.AllDependencies 63 | } 64 | -------------------------------------------------------------------------------- /pkg/sbom/relation.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 sbom 16 | 17 | //counterfeiter:generate . Relation 18 | type GetRelation interface { 19 | GetFrom() string 20 | GetTo() string 21 | } 22 | 23 | type Relation struct { 24 | From string 25 | To string 26 | } 27 | 28 | func (r Relation) GetFrom() string { 29 | return r.From 30 | } 31 | 32 | func (r Relation) GetTo() string { 33 | return r.To 34 | } 35 | -------------------------------------------------------------------------------- /pkg/sbom/sbom.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 sbom 16 | 17 | import ( 18 | "bufio" 19 | "context" 20 | "encoding/json" 21 | "encoding/xml" 22 | "errors" 23 | "io" 24 | "log" 25 | "strings" 26 | 27 | "github.com/interlynk-io/sbomqs/pkg/logger" 28 | "gopkg.in/yaml.v2" 29 | ) 30 | 31 | type SpecFormat string 32 | 33 | const ( 34 | SBOMSpecSPDX SpecFormat = "spdx" 35 | SBOMSpecCDX SpecFormat = "cyclonedx" 36 | SBOMSpecUnknown SpecFormat = "unknown" 37 | ) 38 | 39 | type ( 40 | FileFormat string 41 | FormatVersion string 42 | ) 43 | 44 | const ( 45 | FileFormatJSON FileFormat = "json" 46 | FileFormatRDF FileFormat = "rdf" 47 | FileFormatYAML FileFormat = "yaml" 48 | FileFormatTagValue FileFormat = "tag-value" 49 | FileFormatXML FileFormat = "xml" 50 | FileFormatUnknown FileFormat = "unknown" 51 | ) 52 | 53 | type spdxbasic struct { 54 | ID string `json:"SPDXID" yaml:"SPDXID"` 55 | Version string `json:"spdxVersion" yaml:"spdxVersion"` 56 | } 57 | 58 | type cdxbasic struct { 59 | XMLNS string `json:"-" xml:"xmlns,attr"` 60 | BOMFormat string `json:"bomFormat" xml:"-"` 61 | } 62 | 63 | func SupportedSBOMSpecs() []string { 64 | return []string{string(SBOMSpecSPDX), string(SBOMSpecCDX)} 65 | } 66 | 67 | func SupportedSBOMSpecVersions(f string) []string { 68 | switch strings.ToLower(f) { 69 | case "cyclonedx": 70 | return cdxSpecVersions 71 | case "spdx": 72 | return spdxSpecVersions 73 | default: 74 | return []string{} 75 | } 76 | } 77 | 78 | func SupportedSBOMFileFormats(f string) []string { 79 | switch strings.ToLower(f) { 80 | case "cyclonedx": 81 | return cdxFileFormats 82 | case "spdx": 83 | return spdxFileFormats 84 | default: 85 | return []string{} 86 | } 87 | } 88 | 89 | func SupportedPrimaryPurpose(f string) []string { 90 | switch strings.ToLower(f) { 91 | case "cyclonedx": 92 | return cdxPrimaryPurpose 93 | case "spdx": 94 | return spdxPrimaryPurpose 95 | default: 96 | return []string{} 97 | } 98 | } 99 | 100 | func detectSbomFormat(f io.ReadSeeker) (SpecFormat, FileFormat, FormatVersion, error) { 101 | defer func() { 102 | _, err := f.Seek(0, io.SeekStart) 103 | if err != nil { 104 | log.Printf("Failed to seek: %v", err) 105 | } 106 | }() 107 | 108 | _, err := f.Seek(0, io.SeekStart) 109 | if err != nil { 110 | log.Fatalf("Failed to seek: %v", err) 111 | } 112 | 113 | var s spdxbasic 114 | if err := json.NewDecoder(f).Decode(&s); err == nil { 115 | if strings.HasPrefix(s.ID, "SPDX") { 116 | return SBOMSpecSPDX, FileFormatJSON, FormatVersion(s.Version), nil 117 | } 118 | } 119 | 120 | _, err = f.Seek(0, io.SeekStart) 121 | if err != nil { 122 | log.Printf("Failed to seek: %v", err) 123 | } 124 | 125 | var cdx cdxbasic 126 | if err := json.NewDecoder(f).Decode(&cdx); err == nil { 127 | if cdx.BOMFormat == "CycloneDX" { 128 | return SBOMSpecCDX, FileFormatJSON, "", nil 129 | } 130 | } 131 | 132 | _, err = f.Seek(0, io.SeekStart) 133 | if err != nil { 134 | log.Printf("Failed to seek: %v", err) 135 | } 136 | 137 | if err := xml.NewDecoder(f).Decode(&cdx); err == nil { 138 | if strings.HasPrefix(cdx.XMLNS, "http://cyclonedx.org") { 139 | return SBOMSpecCDX, FileFormatXML, "", nil 140 | } 141 | } 142 | _, err = f.Seek(0, io.SeekStart) 143 | if err != nil { 144 | log.Printf("Failed to seek: %v", err) 145 | } 146 | 147 | if sc := bufio.NewScanner(f); sc.Scan() { 148 | if strings.HasPrefix(sc.Text(), "SPDX") { 149 | return SBOMSpecSPDX, FileFormatTagValue, "", nil 150 | } 151 | } 152 | 153 | _, err = f.Seek(0, io.SeekStart) 154 | if err != nil { 155 | log.Printf("Failed to seek: %v", err) 156 | } 157 | 158 | var y spdxbasic 159 | if err := yaml.NewDecoder(f).Decode(&y); err == nil { 160 | if strings.HasPrefix(y.ID, "SPDX") { 161 | return SBOMSpecSPDX, FileFormatYAML, FormatVersion(s.Version), nil 162 | } 163 | } 164 | 165 | return SBOMSpecUnknown, FileFormatUnknown, "", nil 166 | } 167 | 168 | func NewSBOMDocument(ctx context.Context, f io.ReadSeeker, sig Signature) (Document, error) { 169 | log := logger.FromContext(ctx) 170 | 171 | spec, format, version, err := detectSbomFormat(f) 172 | if err != nil { 173 | return nil, err 174 | } 175 | 176 | log.Debugf("SBOM detect spec:%s format:%s", spec, format) 177 | 178 | var doc Document 179 | 180 | switch spec { 181 | case SBOMSpecSPDX: 182 | doc, err = newSPDXDoc(ctx, f, format, version, sig) 183 | case SBOMSpecCDX: 184 | doc, err = newCDXDoc(ctx, f, format, sig) 185 | default: 186 | return nil, errors.New("unsupported sbom format") 187 | } 188 | 189 | return doc, err 190 | } 191 | -------------------------------------------------------------------------------- /pkg/sbom/sbomfakes/fake_author.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package sbomfakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/interlynk-io/sbomqs/pkg/sbom" 8 | ) 9 | 10 | type FakeAuthor struct { 11 | EmailStub func() string 12 | emailMutex sync.RWMutex 13 | emailArgsForCall []struct { 14 | } 15 | emailReturns struct { 16 | result1 string 17 | } 18 | emailReturnsOnCall map[int]struct { 19 | result1 string 20 | } 21 | NameStub func() string 22 | nameMutex sync.RWMutex 23 | nameArgsForCall []struct { 24 | } 25 | nameReturns struct { 26 | result1 string 27 | } 28 | nameReturnsOnCall map[int]struct { 29 | result1 string 30 | } 31 | TypeStub func() string 32 | typeMutex sync.RWMutex 33 | typeArgsForCall []struct { 34 | } 35 | typeReturns struct { 36 | result1 string 37 | } 38 | typeReturnsOnCall map[int]struct { 39 | result1 string 40 | } 41 | invocations map[string][][]interface{} 42 | invocationsMutex sync.RWMutex 43 | } 44 | 45 | func (fake *FakeAuthor) GetPhone() string { 46 | // Implement the method as needed 47 | return "" 48 | } 49 | 50 | func (fake *FakeAuthor) GetEmail() string { 51 | fake.emailMutex.Lock() 52 | ret, specificReturn := fake.emailReturnsOnCall[len(fake.emailArgsForCall)] 53 | fake.emailArgsForCall = append(fake.emailArgsForCall, struct { 54 | }{}) 55 | stub := fake.EmailStub 56 | fakeReturns := fake.emailReturns 57 | fake.recordInvocation("Email", []interface{}{}) 58 | fake.emailMutex.Unlock() 59 | if stub != nil { 60 | return stub() 61 | } 62 | if specificReturn { 63 | return ret.result1 64 | } 65 | return fakeReturns.result1 66 | } 67 | 68 | func (fake *FakeAuthor) EmailCallCount() int { 69 | fake.emailMutex.RLock() 70 | defer fake.emailMutex.RUnlock() 71 | return len(fake.emailArgsForCall) 72 | } 73 | 74 | func (fake *FakeAuthor) EmailCalls(stub func() string) { 75 | fake.emailMutex.Lock() 76 | defer fake.emailMutex.Unlock() 77 | fake.EmailStub = stub 78 | } 79 | 80 | func (fake *FakeAuthor) EmailReturns(result1 string) { 81 | fake.emailMutex.Lock() 82 | defer fake.emailMutex.Unlock() 83 | fake.EmailStub = nil 84 | fake.emailReturns = struct { 85 | result1 string 86 | }{result1} 87 | } 88 | 89 | func (fake *FakeAuthor) EmailReturnsOnCall(i int, result1 string) { 90 | fake.emailMutex.Lock() 91 | defer fake.emailMutex.Unlock() 92 | fake.EmailStub = nil 93 | if fake.emailReturnsOnCall == nil { 94 | fake.emailReturnsOnCall = make(map[int]struct { 95 | result1 string 96 | }) 97 | } 98 | fake.emailReturnsOnCall[i] = struct { 99 | result1 string 100 | }{result1} 101 | } 102 | 103 | func (fake *FakeAuthor) GetName() string { 104 | fake.nameMutex.Lock() 105 | ret, specificReturn := fake.nameReturnsOnCall[len(fake.nameArgsForCall)] 106 | fake.nameArgsForCall = append(fake.nameArgsForCall, struct { 107 | }{}) 108 | stub := fake.NameStub 109 | fakeReturns := fake.nameReturns 110 | fake.recordInvocation("Name", []interface{}{}) 111 | fake.nameMutex.Unlock() 112 | if stub != nil { 113 | return stub() 114 | } 115 | if specificReturn { 116 | return ret.result1 117 | } 118 | return fakeReturns.result1 119 | } 120 | 121 | func (fake *FakeAuthor) NameCallCount() int { 122 | fake.nameMutex.RLock() 123 | defer fake.nameMutex.RUnlock() 124 | return len(fake.nameArgsForCall) 125 | } 126 | 127 | func (fake *FakeAuthor) NameCalls(stub func() string) { 128 | fake.nameMutex.Lock() 129 | defer fake.nameMutex.Unlock() 130 | fake.NameStub = stub 131 | } 132 | 133 | func (fake *FakeAuthor) NameReturns(result1 string) { 134 | fake.nameMutex.Lock() 135 | defer fake.nameMutex.Unlock() 136 | fake.NameStub = nil 137 | fake.nameReturns = struct { 138 | result1 string 139 | }{result1} 140 | } 141 | 142 | func (fake *FakeAuthor) NameReturnsOnCall(i int, result1 string) { 143 | fake.nameMutex.Lock() 144 | defer fake.nameMutex.Unlock() 145 | fake.NameStub = nil 146 | if fake.nameReturnsOnCall == nil { 147 | fake.nameReturnsOnCall = make(map[int]struct { 148 | result1 string 149 | }) 150 | } 151 | fake.nameReturnsOnCall[i] = struct { 152 | result1 string 153 | }{result1} 154 | } 155 | 156 | func (fake *FakeAuthor) GetType() string { 157 | fake.typeMutex.Lock() 158 | ret, specificReturn := fake.typeReturnsOnCall[len(fake.typeArgsForCall)] 159 | fake.typeArgsForCall = append(fake.typeArgsForCall, struct { 160 | }{}) 161 | stub := fake.TypeStub 162 | fakeReturns := fake.typeReturns 163 | fake.recordInvocation("Type", []interface{}{}) 164 | fake.typeMutex.Unlock() 165 | if stub != nil { 166 | return stub() 167 | } 168 | if specificReturn { 169 | return ret.result1 170 | } 171 | return fakeReturns.result1 172 | } 173 | 174 | func (fake *FakeAuthor) TypeCallCount() int { 175 | fake.typeMutex.RLock() 176 | defer fake.typeMutex.RUnlock() 177 | return len(fake.typeArgsForCall) 178 | } 179 | 180 | func (fake *FakeAuthor) TypeCalls(stub func() string) { 181 | fake.typeMutex.Lock() 182 | defer fake.typeMutex.Unlock() 183 | fake.TypeStub = stub 184 | } 185 | 186 | func (fake *FakeAuthor) TypeReturns(result1 string) { 187 | fake.typeMutex.Lock() 188 | defer fake.typeMutex.Unlock() 189 | fake.TypeStub = nil 190 | fake.typeReturns = struct { 191 | result1 string 192 | }{result1} 193 | } 194 | 195 | func (fake *FakeAuthor) TypeReturnsOnCall(i int, result1 string) { 196 | fake.typeMutex.Lock() 197 | defer fake.typeMutex.Unlock() 198 | fake.TypeStub = nil 199 | if fake.typeReturnsOnCall == nil { 200 | fake.typeReturnsOnCall = make(map[int]struct { 201 | result1 string 202 | }) 203 | } 204 | fake.typeReturnsOnCall[i] = struct { 205 | result1 string 206 | }{result1} 207 | } 208 | 209 | func (fake *FakeAuthor) Invocations() map[string][][]interface{} { 210 | fake.invocationsMutex.RLock() 211 | defer fake.invocationsMutex.RUnlock() 212 | fake.emailMutex.RLock() 213 | defer fake.emailMutex.RUnlock() 214 | fake.nameMutex.RLock() 215 | defer fake.nameMutex.RUnlock() 216 | fake.typeMutex.RLock() 217 | defer fake.typeMutex.RUnlock() 218 | copiedInvocations := map[string][][]interface{}{} 219 | for key, value := range fake.invocations { 220 | copiedInvocations[key] = value 221 | } 222 | return copiedInvocations 223 | } 224 | 225 | func (fake *FakeAuthor) recordInvocation(key string, args []interface{}) { 226 | fake.invocationsMutex.Lock() 227 | defer fake.invocationsMutex.Unlock() 228 | if fake.invocations == nil { 229 | fake.invocations = map[string][][]interface{}{} 230 | } 231 | if fake.invocations[key] == nil { 232 | fake.invocations[key] = [][]interface{}{} 233 | } 234 | fake.invocations[key] = append(fake.invocations[key], args) 235 | } 236 | 237 | var _ sbom.GetAuthor = new(FakeAuthor) 238 | -------------------------------------------------------------------------------- /pkg/sbom/signature.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 sbom 16 | 17 | type GetSignature interface { 18 | GetSigValue() string 19 | GetPublicKey() string 20 | GetBlob() string 21 | } 22 | 23 | type Signature struct { 24 | SigValue string 25 | PublicKey string 26 | Blob string 27 | } 28 | 29 | func (s *Signature) GetSigValue() string { 30 | return s.SigValue 31 | } 32 | 33 | func (s *Signature) GetPublicKey() string { 34 | return s.PublicKey 35 | } 36 | 37 | func (s *Signature) GetBlob() string { 38 | return s.Blob 39 | } 40 | -------------------------------------------------------------------------------- /pkg/sbom/spec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 sbom 16 | 17 | import ( 18 | "github.com/interlynk-io/sbomqs/pkg/licenses" 19 | ) 20 | 21 | type Spec interface { 22 | GetVersion() string 23 | FileFormat() string 24 | Parsable() bool 25 | GetName() string 26 | GetSpecType() string 27 | RequiredFields() bool 28 | GetCreationTimestamp() string 29 | GetLicenses() []licenses.License 30 | GetNamespace() string 31 | GetURI() string 32 | GetOrganization() string 33 | GetComment() string 34 | GetSpdxID() string 35 | GetExtDocRef() []string 36 | } 37 | 38 | type Specs struct { 39 | Version string 40 | Format string 41 | SpecType string 42 | Name string 43 | isReqFieldsPresent bool 44 | Licenses []licenses.License 45 | CreationTimestamp string 46 | Namespace string 47 | URI string 48 | Organization string 49 | Comment string 50 | Spdxid string 51 | ExternalDocReference []string 52 | } 53 | 54 | func NewSpec() *Specs { 55 | return &Specs{} 56 | } 57 | 58 | func (s Specs) GetOrganization() string { 59 | return s.Organization 60 | } 61 | 62 | func (s Specs) GetComment() string { 63 | return s.Comment 64 | } 65 | 66 | func (s Specs) GetSpdxID() string { 67 | return s.Spdxid 68 | } 69 | 70 | func (s Specs) GetVersion() string { 71 | return s.Version 72 | } 73 | 74 | func (s Specs) FileFormat() string { 75 | return s.Format 76 | } 77 | 78 | func (s Specs) Parsable() bool { 79 | return true 80 | } 81 | 82 | func (s Specs) GetName() string { 83 | return s.Name 84 | } 85 | 86 | func (s Specs) GetSpecType() string { 87 | return s.SpecType 88 | } 89 | 90 | func (s Specs) RequiredFields() bool { 91 | return s.isReqFieldsPresent 92 | } 93 | 94 | func (s Specs) GetCreationTimestamp() string { 95 | return s.CreationTimestamp 96 | } 97 | 98 | func (s Specs) GetLicenses() []licenses.License { 99 | return s.Licenses 100 | } 101 | 102 | func (s Specs) GetNamespace() string { 103 | return s.Namespace 104 | } 105 | 106 | func (s Specs) GetURI() string { 107 | return s.URI 108 | } 109 | 110 | func (s Specs) GetExtDocRef() []string { 111 | return s.ExternalDocReference 112 | } 113 | -------------------------------------------------------------------------------- /pkg/sbom/supplier.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 sbom 16 | 17 | //counterfeiter:generate . Supplier 18 | 19 | type GetSupplier interface { 20 | GetName() string 21 | GetEmail() string 22 | GetURL() string 23 | GetContacts() []Contact 24 | IsPresent() bool 25 | } 26 | 27 | type Supplier struct { 28 | Name string 29 | Email string 30 | URL string 31 | Contacts []Contact 32 | } 33 | 34 | func (s Supplier) GetName() string { 35 | return s.Name 36 | } 37 | 38 | func (s Supplier) GetEmail() string { 39 | return s.Email 40 | } 41 | 42 | func (s Supplier) GetURL() string { 43 | return s.URL 44 | } 45 | 46 | func (s Supplier) GetContacts() []Contact { 47 | return s.Contacts 48 | } 49 | 50 | func (s Supplier) IsPresent() bool { 51 | return s.Name != "" || s.Email != "" || s.URL != "" || len(s.Contacts) > 0 52 | } 53 | -------------------------------------------------------------------------------- /pkg/sbom/tool.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 sbom 16 | 17 | type GetTool interface { 18 | GetName() string 19 | GetVersion() string 20 | } 21 | type Tool struct { 22 | Name string 23 | Version string 24 | } 25 | 26 | func (t Tool) GetName() string { 27 | return t.Name 28 | } 29 | 30 | func (t Tool) GetVersion() string { 31 | return t.Version 32 | } 33 | -------------------------------------------------------------------------------- /pkg/sbom/vulnerabilities.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 sbom 16 | 17 | type GetVulnerabilities interface { 18 | GetID() string 19 | } 20 | 21 | type Vulnerability struct { 22 | ID string 23 | } 24 | 25 | func (v Vulnerability) GetID() string { 26 | return v.ID 27 | } 28 | -------------------------------------------------------------------------------- /pkg/scorer/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 scorer 16 | 17 | import ( 18 | "log" 19 | "os" 20 | 21 | "gopkg.in/yaml.v2" 22 | ) 23 | 24 | func DefaultConfig() string { 25 | d, err := yaml.Marshal(checks) 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | 30 | return string(d) 31 | } 32 | 33 | func ReadConfigFile(path string) ([]Filter, error) { 34 | f, err := os.Open(path) 35 | if err != nil { 36 | return nil, err 37 | } 38 | defer f.Close() 39 | 40 | var cks []check 41 | err = yaml.NewDecoder(f).Decode(&cks) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | filters := []Filter{} 47 | for _, ck := range cks { 48 | if ck.Ignore { 49 | filters = append(filters, Filter{ck.Key, Feature}) 50 | } 51 | } 52 | 53 | return filters, nil 54 | } 55 | -------------------------------------------------------------------------------- /pkg/scorer/criteria.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 scorer 16 | 17 | import ( 18 | "github.com/interlynk-io/sbomqs/pkg/sbom" 19 | ) 20 | 21 | type category string 22 | 23 | const ( 24 | structural category = "Structural" 25 | ntiam category = "NTIA-minimum-elements" 26 | semantic category = "Semantic" 27 | quality category = "Quality" 28 | sharing category = "Sharing" 29 | ) 30 | 31 | type check struct { 32 | Category string `yaml:"category"` 33 | Key string `yaml:"feature"` 34 | Ignore bool `yaml:"disabled"` 35 | Descr string `yaml:"descrption"` 36 | evaluate func(sbom.Document, *check) score 37 | } 38 | 39 | var checks = []check{ 40 | // structural 41 | {string(structural), "sbom_spec", false, "SBOM Specification", specCheck}, 42 | {string(structural), "sbom_spec_version", false, "Spec Version", specVersionCheck}, 43 | {string(structural), "sbom_spec_file_format", false, "Spec File Format", specFileFormatCheck}, 44 | {string(structural), "sbom_parsable", false, "Spec is parsable", specParsableCheck}, 45 | 46 | // ntia minimum 47 | {string(ntiam), "comp_with_supplier", false, "components have suppliers", compSupplierCheck}, 48 | {string(ntiam), "comp_with_name", false, "components have a name", compWithNameCheck}, 49 | {string(ntiam), "comp_with_version", false, "components have a version", compWithVersionCheck}, 50 | {string(ntiam), "comp_with_uniq_ids", false, "components have uniq ids", compWithUniqIDCheck}, 51 | {string(ntiam), "sbom_dependencies", false, "sbom has dependencies", docWithDepedenciesCheck}, 52 | {string(ntiam), "sbom_authors", false, "sbom has authors", docWithAuthorsCheck}, 53 | {string(ntiam), "sbom_creation_timestamp", false, "sbom has creation timestamp", docWithTimeStampCheck}, 54 | 55 | // semantic 56 | {string(semantic), "sbom_required_fields", false, "sbom has all required fields", docWithRequiredFieldCheck}, 57 | {string(semantic), "comp_with_licenses", false, "components have licenses", compWithLicensesCheck}, 58 | {string(semantic), "comp_with_checksums", false, "components have checksums", compWithChecksumsCheck}, 59 | 60 | // quality 61 | {string(quality), "comp_valid_licenses", false, "components with valid licenses", compWithValidLicensesCheck}, 62 | {string(quality), "comp_with_primary_purpose", false, "components with primary purpose", compWithPrimaryPackageCheck}, 63 | {string(quality), "comp_with_deprecated_licenses", false, "components with deprecated licenses", compWithNoDepLicensesCheck}, 64 | {string(quality), "comp_with_restrictive_licenses", false, "components with restrictive_licenses", compWithRestrictedLicensesCheck}, 65 | {string(quality), "comp_with_any_vuln_lookup_id", false, "components with any vulnerability lookup id", compWithAnyLookupIDCheck}, 66 | {string(quality), "comp_with_multi_vuln_lookup_id", false, "components with multiple vulnerability lookup id", compWithMultipleIDCheck}, 67 | {string(quality), "sbom_with_creator_and_version", false, "sbom has creator and version", docWithCreatorCheck}, 68 | {string(quality), "sbom_with_primary_component", false, "sbom has primary component", docWithPrimaryComponentCheck}, 69 | 70 | // sharing 71 | {string(sharing), "sbom_sharable", false, "sbom document has a sharable license", sharableLicenseCheck}, 72 | } 73 | -------------------------------------------------------------------------------- /pkg/scorer/ntia.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 scorer 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | 21 | "github.com/interlynk-io/sbomqs/pkg/sbom" 22 | "github.com/samber/lo" 23 | ) 24 | 25 | func compSupplierCheck(d sbom.Document, c *check) score { 26 | s := newScoreFromCheck(c) 27 | 28 | totalComponents := len(d.Components()) 29 | if totalComponents == 0 { 30 | s.setScore(0.0) 31 | s.setDesc("N/A (no components)") 32 | s.setIgnore(true) 33 | return *s 34 | } 35 | 36 | withNames := lo.CountBy(d.Components(), func(c sbom.GetComponent) bool { 37 | return c.Suppliers().IsPresent() 38 | }) 39 | 40 | if totalComponents > 0 { 41 | s.setScore((float64(withNames) / float64(totalComponents)) * 10.0) 42 | } 43 | 44 | s.setDesc(fmt.Sprintf("%d/%d have supplier names", withNames, totalComponents)) 45 | 46 | return *s 47 | } 48 | 49 | func compWithNameCheck(d sbom.Document, c *check) score { 50 | s := newScoreFromCheck(c) 51 | totalComponents := len(d.Components()) 52 | if totalComponents == 0 { 53 | s.setScore(0.0) 54 | s.setDesc("N/A (no components)") 55 | s.setIgnore(true) 56 | return *s 57 | } 58 | withNames := lo.CountBy(d.Components(), func(c sbom.GetComponent) bool { 59 | return c.GetName() != "" 60 | }) 61 | if totalComponents > 0 { 62 | s.setScore((float64(withNames) / float64(totalComponents)) * 10.0) 63 | } 64 | s.setDesc(fmt.Sprintf("%d/%d have names", withNames, totalComponents)) 65 | 66 | return *s 67 | } 68 | 69 | func compWithVersionCheck(d sbom.Document, c *check) score { 70 | s := newScoreFromCheck(c) 71 | 72 | totalComponents := len(d.Components()) 73 | if totalComponents == 0 { 74 | s.setScore(0.0) 75 | s.setDesc("N/A (no components)") 76 | s.setIgnore(true) 77 | return *s 78 | } 79 | withVersions := lo.CountBy(d.Components(), func(c sbom.GetComponent) bool { 80 | return c.GetVersion() != "" 81 | }) 82 | if totalComponents > 0 { 83 | s.setScore((float64(withVersions) / float64(totalComponents)) * 10.0) 84 | } 85 | s.setDesc(fmt.Sprintf("%d/%d have versions", withVersions, totalComponents)) 86 | 87 | return *s 88 | } 89 | 90 | func compWithUniqIDCheck(d sbom.Document, c *check) score { 91 | s := newScoreFromCheck(c) 92 | 93 | totalComponents := len(d.Components()) 94 | if totalComponents == 0 { 95 | s.setScore(0.0) 96 | s.setDesc("N/A (no components)") 97 | s.setIgnore(true) 98 | return *s 99 | } 100 | 101 | compIDs := lo.FilterMap(d.Components(), func(c sbom.GetComponent, _ int) (string, bool) { 102 | if c.GetID() == "" { 103 | return "", false 104 | } 105 | return strings.Join([]string{d.Spec().GetNamespace(), c.GetID()}, ""), true 106 | }) 107 | 108 | // uniqComps := lo.Uniq(compIDs) 109 | 110 | if totalComponents > 0 { 111 | s.setScore((float64(len(compIDs)) / float64(totalComponents)) * 10.0) 112 | } 113 | s.setDesc(fmt.Sprintf("%d/%d have unique ID's", len(compIDs), totalComponents)) 114 | return *s 115 | } 116 | 117 | func docWithDepedenciesCheck(d sbom.Document, c *check) score { 118 | s := newScoreFromCheck(c) 119 | var totalDependencies int 120 | if d.PrimaryComp() != nil { 121 | totalDependencies = d.PrimaryComp().GetTotalNoOfDependencies() 122 | } 123 | if totalDependencies > 0 { 124 | s.setScore(10.0) 125 | } 126 | s.setDesc(fmt.Sprintf("doc has %d dependencies ", totalDependencies)) 127 | return *s 128 | } 129 | 130 | func docWithAuthorsCheck(d sbom.Document, c *check) score { 131 | s := newScoreFromCheck(c) 132 | 133 | noOfAuthors := len(d.Authors()) 134 | noOfTools := len(d.Tools()) 135 | 136 | totalAuthors := noOfAuthors + noOfTools 137 | 138 | if totalAuthors > 0 { 139 | s.setScore(10.0) 140 | } 141 | s.setDesc(fmt.Sprintf("doc has %d authors", totalAuthors)) 142 | 143 | return *s 144 | } 145 | 146 | func docWithTimeStampCheck(d sbom.Document, c *check) score { 147 | s := newScoreFromCheck(c) 148 | 149 | if d.Spec().GetCreationTimestamp() != "" { 150 | s.setScore(10.0) 151 | } 152 | 153 | s.setDesc(fmt.Sprintf("doc has creation timestamp %s", d.Spec().GetCreationTimestamp())) 154 | return *s 155 | } 156 | -------------------------------------------------------------------------------- /pkg/scorer/quality.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 scorer 16 | 17 | import ( 18 | "fmt" 19 | "math" 20 | "strings" 21 | 22 | "github.com/interlynk-io/sbomqs/pkg/licenses" 23 | "github.com/interlynk-io/sbomqs/pkg/sbom" 24 | "github.com/samber/lo" 25 | ) 26 | 27 | func compWithValidLicensesCheck(d sbom.Document, c *check) score { 28 | s := newScoreFromCheck(c) 29 | 30 | totalComponents := len(d.Components()) 31 | if totalComponents == 0 { 32 | s.setScore(0.0) 33 | s.setDesc("N/A (no components)") 34 | s.setIgnore(true) 35 | return *s 36 | } 37 | 38 | compScores := lo.Map(d.Components(), func(c sbom.GetComponent, _ int) float64 { 39 | tl := len(c.Licenses()) 40 | 41 | if tl == 0 { 42 | return 0.0 43 | } 44 | 45 | validLic := lo.CountBy(c.Licenses(), func(l licenses.License) bool { 46 | return l.Spdx() 47 | }) 48 | 49 | if validLic == 0 { 50 | return 0.0 51 | } 52 | 53 | return (float64(validLic) / float64(tl)) * 10.0 54 | }) 55 | 56 | totalCompScore := lo.Reduce(compScores, func(agg float64, a float64, _ int) float64 { 57 | if !math.IsNaN(a) { 58 | return agg + a 59 | } 60 | return agg 61 | }, 0.0) 62 | 63 | finalScore := (totalCompScore / float64(totalComponents)) 64 | 65 | compsWithValidScores := lo.CountBy(compScores, func(score float64) bool { 66 | return score > 0.0 67 | }) 68 | 69 | s.setScore(finalScore) 70 | 71 | s.setDesc(fmt.Sprintf("%d/%d components with valid license ", compsWithValidScores, totalComponents)) 72 | 73 | return *s 74 | } 75 | 76 | func compWithPrimaryPackageCheck(d sbom.Document, c *check) score { 77 | s := newScoreFromCheck(c) 78 | 79 | totalComponents := len(d.Components()) 80 | if totalComponents == 0 { 81 | s.setScore(0.0) 82 | s.setDesc("N/A (no components)") 83 | s.setIgnore(true) 84 | return *s 85 | } 86 | withPurpose := lo.CountBy(d.Components(), func(c sbom.GetComponent) bool { 87 | return c.PrimaryPurpose() != "" && lo.Contains(sbom.SupportedPrimaryPurpose(d.Spec().GetSpecType()), strings.ToLower(c.PrimaryPurpose())) 88 | }) 89 | 90 | finalScore := (float64(withPurpose) / float64(totalComponents)) * 10.0 91 | s.setScore(finalScore) 92 | 93 | s.setDesc(fmt.Sprintf("%d/%d components have primary purpose specified", withPurpose, totalComponents)) 94 | return *s 95 | } 96 | 97 | func compWithNoDepLicensesCheck(d sbom.Document, c *check) score { 98 | s := newScoreFromCheck(c) 99 | totalComponents := len(d.Components()) 100 | if totalComponents == 0 { 101 | s.setScore(0.0) 102 | s.setDesc("N/A (no components)") 103 | s.setIgnore(true) 104 | return *s 105 | } 106 | 107 | totalLicenses := lo.Reduce(d.Components(), func(agg int, c sbom.GetComponent, _ int) int { 108 | return agg + len(c.Licenses()) 109 | }, 0) 110 | 111 | withDepLicense := lo.CountBy(d.Components(), func(c sbom.GetComponent) bool { 112 | deps := lo.CountBy(c.Licenses(), func(l licenses.License) bool { 113 | return l.Deprecated() 114 | }) 115 | return deps > 0 116 | }) 117 | 118 | if totalLicenses == 0 { 119 | s.setScore(0.0) 120 | s.setDesc("no licenses found") 121 | } else { 122 | finalScore := (float64(totalComponents-withDepLicense) / float64(totalComponents)) * 10.0 123 | s.setScore(finalScore) 124 | s.setDesc(fmt.Sprintf("%d/%d components have deprecated licenses", withDepLicense, totalComponents)) 125 | } 126 | return *s 127 | } 128 | 129 | func compWithRestrictedLicensesCheck(d sbom.Document, c *check) score { 130 | s := newScoreFromCheck(c) 131 | totalComponents := len(d.Components()) 132 | if totalComponents == 0 { 133 | s.setScore(0.0) 134 | s.setDesc("N/A (no components)") 135 | s.setIgnore(true) 136 | return *s 137 | } 138 | 139 | totalLicenses := lo.Reduce(d.Components(), func(agg int, c sbom.GetComponent, _ int) int { 140 | return agg + len(c.Licenses()) 141 | }, 0) 142 | 143 | withRestrictLicense := lo.CountBy(d.Components(), func(c sbom.GetComponent) bool { 144 | rest := lo.CountBy(c.Licenses(), func(l licenses.License) bool { 145 | return l.Restrictive() 146 | }) 147 | return rest > 0 148 | }) 149 | 150 | if totalLicenses == 0 { 151 | s.setScore(0.0) 152 | s.setDesc("no licenses found") 153 | } else { 154 | finalScore := (float64(totalComponents-withRestrictLicense) / float64(totalComponents)) * 10.0 155 | s.setScore(finalScore) 156 | s.setDesc(fmt.Sprintf("%d/%d components have restricted licenses", withRestrictLicense, totalComponents)) 157 | } 158 | return *s 159 | } 160 | 161 | func compWithAnyLookupIDCheck(d sbom.Document, c *check) score { 162 | s := newScoreFromCheck(c) 163 | 164 | totalComponents := len(d.Components()) 165 | if totalComponents == 0 { 166 | s.setScore(0.0) 167 | s.setDesc("N/A (no components)") 168 | s.setIgnore(true) 169 | return *s 170 | } 171 | 172 | withAnyLookupID := lo.CountBy(d.Components(), func(c sbom.GetComponent) bool { 173 | if len(c.GetCpes()) > 0 || len(c.GetPurls()) > 0 { 174 | return true 175 | } 176 | return false 177 | }) 178 | 179 | finalScore := (float64(withAnyLookupID) / float64(totalComponents)) * 10.0 180 | 181 | s.setScore(finalScore) 182 | 183 | s.setDesc(fmt.Sprintf("%d/%d components have any lookup id", withAnyLookupID, totalComponents)) 184 | 185 | return *s 186 | } 187 | 188 | func compWithMultipleIDCheck(d sbom.Document, c *check) score { 189 | s := newScoreFromCheck(c) 190 | 191 | totalComponents := len(d.Components()) 192 | if totalComponents == 0 { 193 | s.setScore(0.0) 194 | s.setDesc("N/A (no components)") 195 | s.setIgnore(true) 196 | return *s 197 | } 198 | 199 | withMultipleID := lo.CountBy(d.Components(), func(c sbom.GetComponent) bool { 200 | if len(c.GetCpes()) > 0 && len(c.GetPurls()) > 0 { 201 | return true 202 | } 203 | return false 204 | }) 205 | 206 | finalScore := (float64(withMultipleID) / float64(totalComponents)) * 10.0 207 | 208 | s.setScore(finalScore) 209 | 210 | s.setDesc(fmt.Sprintf("%d/%d components have multiple lookup id", withMultipleID, totalComponents)) 211 | 212 | return *s 213 | } 214 | 215 | func docWithCreatorCheck(d sbom.Document, c *check) score { 216 | s := newScoreFromCheck(c) 217 | 218 | totalTools := len(d.Tools()) 219 | 220 | withCreatorAndVersion := lo.CountBy(d.Tools(), func(t sbom.GetTool) bool { 221 | return t.GetName() != "" && t.GetVersion() != "" 222 | }) 223 | 224 | finalScore := (float64(withCreatorAndVersion) / float64(totalTools)) * 10.0 225 | 226 | s.setScore(finalScore) 227 | s.setDesc(fmt.Sprintf("%d/%d tools have creator and version", withCreatorAndVersion, totalTools)) 228 | return *s 229 | } 230 | 231 | func docWithPrimaryComponentCheck(d sbom.Document, c *check) score { 232 | s := newScoreFromCheck(c) 233 | 234 | if d.PrimaryComp().IsPresent() { 235 | s.setScore(10.0) 236 | s.setDesc("primary component found") 237 | return *s 238 | } 239 | s.setScore(0.0) 240 | s.setDesc("no primary component found") 241 | return *s 242 | } 243 | -------------------------------------------------------------------------------- /pkg/scorer/score.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 scorer 16 | 17 | import "math" 18 | 19 | type Score interface { 20 | Category() string 21 | Feature() string 22 | Ignore() bool 23 | Score() float64 24 | Descr() string 25 | MaxScore() float64 26 | } 27 | 28 | //nolint:revive,stylecheck 29 | const MAX_SCORE float64 = 10.0 30 | 31 | type score struct { 32 | category string 33 | feature string 34 | descr string 35 | score float64 36 | ignore bool 37 | } 38 | 39 | func newScoreFromCheck(c *check) *score { 40 | return &score{ 41 | category: c.Category, 42 | feature: c.Key, 43 | ignore: false, 44 | } 45 | } 46 | 47 | func (s *score) setScore(f float64) { 48 | if math.IsNaN(f) { 49 | s.score = 0.0 50 | } else { 51 | s.score = f 52 | } 53 | } 54 | 55 | func (s *score) setDesc(d string) { 56 | s.descr = d 57 | } 58 | 59 | func (s *score) setIgnore(i bool) { 60 | s.ignore = i 61 | } 62 | 63 | func (s score) Category() string { 64 | return s.category 65 | } 66 | 67 | func (s score) Feature() string { 68 | return s.feature 69 | } 70 | 71 | func (s score) Score() float64 { 72 | return s.score 73 | } 74 | 75 | func (s score) Ignore() bool { 76 | return s.ignore 77 | } 78 | 79 | func (s score) Descr() string { 80 | return s.descr 81 | } 82 | 83 | func (s score) MaxScore() float64 { 84 | return MAX_SCORE 85 | } 86 | -------------------------------------------------------------------------------- /pkg/scorer/scorer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 scorer 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/interlynk-io/sbomqs/pkg/sbom" 21 | ) 22 | 23 | const EngineVersion = "7" 24 | 25 | type filterType int 26 | 27 | const ( 28 | Feature filterType = iota 29 | Category 30 | ) 31 | 32 | type Filter struct { 33 | Name string 34 | Ftype filterType 35 | } 36 | 37 | type Scorer struct { 38 | ctx context.Context 39 | doc sbom.Document 40 | 41 | // optional params 42 | featFilter map[string]bool 43 | catFilter map[string]bool 44 | } 45 | 46 | func NewScorer(ctx context.Context, doc sbom.Document) *Scorer { 47 | scorer := &Scorer{ 48 | ctx: ctx, 49 | doc: doc, 50 | featFilter: make(map[string]bool), 51 | catFilter: make(map[string]bool), 52 | } 53 | 54 | return scorer 55 | } 56 | 57 | func (s *Scorer) AddFilter(nm string, ftype filterType) { 58 | switch ftype { 59 | case Feature: 60 | s.featFilter[nm] = true 61 | case Category: 62 | s.catFilter[nm] = true 63 | } 64 | } 65 | 66 | func (s *Scorer) Score() Scores { 67 | if s.doc == nil { 68 | return newScores() 69 | } 70 | 71 | if len(s.featFilter) > 0 { 72 | return s.featureScores() 73 | } 74 | 75 | if len(s.catFilter) > 0 { 76 | return s.catScores() 77 | } 78 | 79 | return s.AllScores() 80 | } 81 | 82 | func (s *Scorer) catScores() Scores { 83 | scores := newScores() 84 | 85 | for _, c := range checks { 86 | cCopy := c // Create a copy of c 87 | if s.catFilter[c.Category] { 88 | scores.addScore(c.evaluate(s.doc, &cCopy)) 89 | } 90 | } 91 | 92 | return scores 93 | } 94 | 95 | func (s *Scorer) featureScores() Scores { 96 | scores := newScores() 97 | 98 | for _, c := range checks { 99 | if s.featFilter[c.Key] { 100 | scores.addScore(c.evaluate(s.doc, &c)) //nolint:gosec 101 | } 102 | } 103 | 104 | return scores 105 | } 106 | 107 | func (s *Scorer) AllScores() Scores { 108 | scores := newScores() 109 | 110 | for _, c := range checks { 111 | scores.addScore(c.evaluate(s.doc, &c)) //nolint:gosec 112 | } 113 | 114 | return scores 115 | } 116 | -------------------------------------------------------------------------------- /pkg/scorer/scores.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 scorer 16 | 17 | type Scores interface { 18 | Count() int 19 | AvgScore() float64 20 | ScoreList() []Score 21 | } 22 | 23 | type scores struct { 24 | scs []Score 25 | } 26 | 27 | func newScores() *scores { 28 | return &scores{ 29 | scs: []Score{}, 30 | } 31 | } 32 | 33 | func (s *scores) addScore(ss score) { 34 | s.scs = append(s.scs, ss) 35 | } 36 | 37 | func (s scores) Count() int { 38 | return len(s.scs) 39 | } 40 | 41 | func (s scores) AvgScore() float64 { 42 | score := 0.0 43 | for _, s := range s.scs { 44 | if !s.Ignore() { 45 | score += s.Score() 46 | } 47 | } 48 | return score / float64(s.Count()) 49 | } 50 | 51 | func (s scores) ScoreList() []Score { 52 | return s.scs 53 | } 54 | -------------------------------------------------------------------------------- /pkg/scorer/semantic.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 scorer 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/interlynk-io/sbomqs/pkg/sbom" 21 | "github.com/samber/lo" 22 | ) 23 | 24 | func docWithRequiredFieldCheck(d sbom.Document, c *check) score { 25 | s := newScoreFromCheck(c) 26 | 27 | totalComponents := len(d.Components()) 28 | 29 | docOK := d.Spec().RequiredFields() 30 | noOfPkgs := lo.CountBy(d.Components(), func(c sbom.GetComponent) bool { 31 | return c.RequiredFields() 32 | }) 33 | pkgsOK := false 34 | if totalComponents > 0 && noOfPkgs == totalComponents { 35 | pkgsOK = true 36 | } 37 | 38 | var docScore, pkgScore float64 39 | 40 | if !docOK && pkgsOK { 41 | docScore = 0 42 | pkgScore = 10.0 43 | s.setScore((docScore + pkgScore) / 2.0) 44 | s.setScore(0.0) 45 | } 46 | 47 | if docOK && !pkgsOK { 48 | docScore = 10.0 49 | if totalComponents > 0 { 50 | pkgScore = (float64(noOfPkgs) / float64(totalComponents)) * 10.0 51 | } 52 | s.setScore((docScore + pkgScore) / 2.0) 53 | } 54 | 55 | if docOK && pkgsOK { 56 | s.setScore(10.0) 57 | } 58 | 59 | s.setDesc(fmt.Sprintf("Doc Fields:%t Pkg Fields:%t", docOK, pkgsOK)) 60 | 61 | return *s 62 | } 63 | 64 | func compWithLicensesCheck(d sbom.Document, c *check) score { 65 | s := newScoreFromCheck(c) 66 | 67 | totalComponents := len(d.Components()) 68 | if totalComponents == 0 { 69 | s.setScore(0.0) 70 | s.setDesc("N/A (no components)") 71 | s.setIgnore(true) 72 | return *s 73 | } 74 | withLicenses := lo.CountBy(d.Components(), func(c sbom.GetComponent) bool { 75 | return len(c.Licenses()) > 0 76 | }) 77 | 78 | if totalComponents > 0 { 79 | s.setScore((float64(withLicenses) / float64(totalComponents)) * 10.0) 80 | } 81 | 82 | s.setDesc(fmt.Sprintf("%d/%d have licenses", withLicenses, totalComponents)) 83 | 84 | return *s 85 | } 86 | 87 | func compWithChecksumsCheck(d sbom.Document, c *check) score { 88 | s := newScoreFromCheck(c) 89 | 90 | totalComponents := len(d.Components()) 91 | if totalComponents == 0 { 92 | s.setScore(0.0) 93 | s.setDesc("N/A (no components)") 94 | s.setIgnore(true) 95 | return *s 96 | } 97 | 98 | withChecksums := lo.CountBy(d.Components(), func(c sbom.GetComponent) bool { 99 | return len(c.GetChecksums()) > 0 100 | }) 101 | 102 | if totalComponents > 0 { 103 | s.setScore((float64(withChecksums) / float64(totalComponents)) * 10.0) 104 | } 105 | 106 | s.setDesc(fmt.Sprintf("%d/%d have checksums", withChecksums, totalComponents)) 107 | 108 | return *s 109 | } 110 | -------------------------------------------------------------------------------- /pkg/scorer/sharing.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 scorer 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/interlynk-io/sbomqs/pkg/licenses" 21 | "github.com/interlynk-io/sbomqs/pkg/sbom" 22 | "github.com/samber/lo" 23 | ) 24 | 25 | func sharableLicenseCheck(d sbom.Document, c *check) score { 26 | s := newScoreFromCheck(c) 27 | 28 | lics := d.Spec().GetLicenses() 29 | 30 | freeLics := lo.CountBy(lics, func(l licenses.License) bool { 31 | return l.FreeAnyUse() 32 | }) 33 | 34 | if len(lics) > 0 && freeLics == len(lics) { 35 | s.setScore(10.0) 36 | } 37 | 38 | s.setDesc(fmt.Sprintf("doc has a sharable license free %d :: of %d", freeLics, len(lics))) 39 | return *s 40 | } 41 | -------------------------------------------------------------------------------- /pkg/scorer/structural.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 scorer 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | 21 | "github.com/interlynk-io/sbomqs/pkg/sbom" 22 | ) 23 | 24 | func specCheck(d sbom.Document, c *check) score { 25 | s := newScoreFromCheck(c) 26 | 27 | specs := sbom.SupportedSBOMSpecs() 28 | s.setDesc(fmt.Sprintf("provided sbom is in a supported sbom format of %s", strings.Join(specs, ","))) 29 | 30 | for _, spec := range specs { 31 | if d.Spec().GetSpecType() == spec { 32 | s.setScore(10.0) 33 | } 34 | } 35 | return *s 36 | } 37 | 38 | func specVersionCheck(d sbom.Document, c *check) score { 39 | s := newScoreFromCheck(c) 40 | 41 | versions := sbom.SupportedSBOMSpecVersions(d.Spec().GetSpecType()) 42 | s.setDesc(fmt.Sprintf("provided sbom should be in supported spec version for spec:%s and versions: %s", d.Spec().GetVersion(), strings.Join(versions, ","))) 43 | 44 | for _, ver := range versions { 45 | if d.Spec().GetVersion() == ver { 46 | s.setScore(10.0) 47 | } 48 | } 49 | 50 | return *s 51 | } 52 | 53 | func specFileFormatCheck(d sbom.Document, c *check) score { 54 | s := newScoreFromCheck(c) 55 | 56 | formats := sbom.SupportedSBOMFileFormats(d.Spec().GetSpecType()) 57 | s.setDesc(fmt.Sprintf("provided sbom should be in supported file format for spec: %s and version: %s", d.Spec().FileFormat(), strings.Join(formats, ","))) 58 | 59 | for _, format := range formats { 60 | if d.Spec().FileFormat() == format { 61 | s.setScore(10.0) 62 | } 63 | } 64 | return *s 65 | } 66 | 67 | func specParsableCheck(d sbom.Document, c *check) score { 68 | s := newScoreFromCheck(c) 69 | s.setDesc("provided sbom is parsable") 70 | if d.Spec().Parsable() { 71 | s.setScore(10.0) 72 | } 73 | 74 | return *s 75 | } 76 | -------------------------------------------------------------------------------- /pkg/scorer/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package tools 5 | 6 | import ( 7 | _ "github.com/maxbrunsfeld/counterfeiter/v6" 8 | ) 9 | 10 | // This file imports packages that are used when running go generate, or used 11 | // during the development process but not otherwise depended on by built code. 12 | -------------------------------------------------------------------------------- /pkg/share/share.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 share 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "fmt" 21 | "io" 22 | "net/http" 23 | "net/url" 24 | "strings" 25 | 26 | "github.com/interlynk-io/sbomqs/pkg/reporter" 27 | "github.com/interlynk-io/sbomqs/pkg/sbom" 28 | "github.com/interlynk-io/sbomqs/pkg/scorer" 29 | ) 30 | 31 | func Share(ctx context.Context, doc sbom.Document, scores scorer.Scores, sbomFileName string) (string, error) { 32 | nr := reporter.NewReport(ctx, 33 | []sbom.Document{doc}, 34 | []scorer.Scores{scores}, 35 | []string{sbomFileName}, 36 | reporter.WithFormat(strings.ToLower("json"))) 37 | 38 | js, err := nr.ShareReport() 39 | if err != nil { 40 | return "", err 41 | } 42 | 43 | return sentToBenchmark(js) 44 | } 45 | 46 | type shareResonse struct { 47 | URL string `json:"url"` 48 | } 49 | 50 | func sentToBenchmark(js string) (string, error) { 51 | req := &http.Request{ 52 | Method: "POST", 53 | URL: &url.URL{Scheme: "https", Host: "sbombenchmark.dev", Path: "/user/score"}, 54 | Header: http.Header{ 55 | "Content-Type": []string{"application/json"}, 56 | }, 57 | Body: io.NopCloser(strings.NewReader(js)), 58 | } 59 | 60 | resp, err := http.DefaultClient.Do(req) 61 | if err != nil { 62 | return "", err 63 | } 64 | 65 | defer resp.Body.Close() 66 | 67 | if resp.StatusCode != 200 { 68 | return "", fmt.Errorf("bad response from Benchmark: %s", resp.Status) 69 | } 70 | 71 | data, _ := io.ReadAll(resp.Body) 72 | sr := shareResonse{} 73 | 74 | err = json.Unmarshal(data, &sr) 75 | if err != nil { 76 | return "", err 77 | } 78 | 79 | return sr.URL, nil 80 | } 81 | -------------------------------------------------------------------------------- /pkg/swhid/swhid.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 swhid 16 | 17 | import "regexp" 18 | 19 | type SWHID string 20 | 21 | const swhidRegex = `^swh:1:cnt:[a-fA-F0-9]{40}$` 22 | 23 | func (swhid SWHID) Valid() bool { 24 | return regexp.MustCompile(swhidRegex).MatchString(swhid.String()) 25 | } 26 | 27 | func NewSWHID(swhid string) SWHID { 28 | return SWHID(swhid) 29 | } 30 | 31 | func (swhid SWHID) String() string { 32 | return string(swhid) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/swhid/swhid_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 swhid 16 | 17 | import ( 18 | "testing" 19 | ) 20 | 21 | func TestValidSWHID(t *testing.T) { 22 | tests := []struct { 23 | name string 24 | input string 25 | want bool 26 | }{ 27 | {"Is empty value a valid SWHID", "", false}, 28 | {"Is XYZ a valid SWHID", "xyz", false}, 29 | {"Is swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2 a valid SWHID", "swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2", true}, 30 | {"Is swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2a a valid SWHID", "swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2a", false}, 31 | } 32 | for _, tt := range tests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | swhidInput := NewSWHID(tt.input) 35 | if swhidInput.Valid() != tt.want { 36 | t.Errorf("got %t, want %t", swhidInput.Valid(), tt.want) 37 | } 38 | }) 39 | } 40 | } 41 | 42 | func TestString(t *testing.T) { 43 | tests := []struct { 44 | name string 45 | input string 46 | want string 47 | }{ 48 | {"Empty SWHID value", "", ""}, 49 | {"Valid SWHID", "swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2", "swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2"}, 50 | } 51 | for _, tt := range tests { 52 | t.Run(tt.name, func(t *testing.T) { 53 | swhidInput := NewSWHID(tt.input) 54 | if swhidInput.String() != tt.want { 55 | t.Errorf("got %s, want %s", swhidInput.String(), tt.want) 56 | } 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pkg/swid/swid.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 swid 16 | 17 | type SWID interface { 18 | GetName() string 19 | GetTagID() string 20 | Valid() bool 21 | String() string 22 | } 23 | 24 | type swid struct { 25 | TagID string 26 | Name string 27 | } 28 | 29 | func (s swid) GetName() string { 30 | return s.Name 31 | } 32 | 33 | func (s swid) GetTagID() string { 34 | return s.TagID 35 | } 36 | 37 | func (s swid) Valid() bool { 38 | // Basic validation: check if the TagID is a non-empty string 39 | return s.TagID != "" 40 | } 41 | 42 | func (s swid) String() string { 43 | return s.TagID 44 | } 45 | 46 | func NewSWID(tagID, name string) SWID { 47 | return swid{ 48 | TagID: tagID, 49 | Name: name, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pkg/swid/swid_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 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 swid 16 | 17 | import ( 18 | "testing" 19 | ) 20 | 21 | func TestValidSWID(t *testing.T) { 22 | tests := []struct { 23 | name string 24 | input string 25 | want bool 26 | }{ 27 | {"Is empty value a valid SWID", "", false}, 28 | {"Is XYZ a valid SWID", "xyz", true}, 29 | {"Is example-swid a valid SWID", "example-swid", true}, 30 | {"Is example_swid a valid SWID", "example_swid", true}, 31 | {"Is example.swid a valid SWID", "example.swid", true}, 32 | } 33 | for _, tt := range tests { 34 | t.Run(tt.name, func(t *testing.T) { 35 | swidInput := NewSWID(tt.input, "example-name") 36 | if swidInput.Valid() != tt.want { 37 | t.Errorf("got %t, want %t", swidInput.Valid(), tt.want) 38 | } 39 | }) 40 | } 41 | } 42 | 43 | func TestGetName(t *testing.T) { 44 | tests := []struct { 45 | name string 46 | input string 47 | want string 48 | }{ 49 | {"Get name of SWID", "example-swid", "example-name"}, 50 | } 51 | for _, tt := range tests { 52 | t.Run(tt.name, func(t *testing.T) { 53 | swidInput := NewSWID(tt.input, "example-name") 54 | if swidInput.GetName() != tt.want { 55 | t.Errorf("got %s, want %s", swidInput.GetName(), tt.want) 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func TestGetTagID(t *testing.T) { 62 | tests := []struct { 63 | name string 64 | input string 65 | want string 66 | }{ 67 | {"Get TagID of SWID", "example-swid", "example-swid"}, 68 | } 69 | for _, tt := range tests { 70 | t.Run(tt.name, func(t *testing.T) { 71 | swidInput := NewSWID(tt.input, "example-name") 72 | if swidInput.GetTagID() != tt.want { 73 | t.Errorf("got %s, want %s", swidInput.GetTagID(), tt.want) 74 | } 75 | }) 76 | } 77 | } 78 | 79 | func TestString(t *testing.T) { 80 | tests := []struct { 81 | name string 82 | input string 83 | want string 84 | }{ 85 | {"String representation of SWID", "example-swid", "example-swid"}, 86 | } 87 | for _, tt := range tests { 88 | t.Run(tt.name, func(t *testing.T) { 89 | swidInput := NewSWID(tt.input, "example-name") 90 | if swidInput.String() != tt.want { 91 | t.Errorf("got %s, want %s", swidInput.String(), tt.want) 92 | } 93 | }) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /samples/signature-test-data/public_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvIBp7rOk+MGGUwha4MF8 3 | 9KMN66cJg0oR3aJ/E3Id6AbsP20xy1H/wi+flgPHvk13ZZiBKlQl/4Uqq6W/ZzYq 4 | 6qVJWl/q/n1ScudeikF6J/hejv6yvsWESlV9GWUnCL1OtpTuq7NMH2RIUt39aupM 5 | LaLoZ3JGP/l81l8uf8esQkZ3rkcl4lPAue9oZwPB61g+lNAklDdYcl/uBaAbDePE 6 | 6cRaSF9jOfgyaV/WxBhVf8u+IO28XRihBW9pSmwmR8P24/xfNqE9XfxMQL8hqMPn 7 | IokHHl4sYhIDpjyK2SB+BozFLKtQoDdk+18Ku7eN7GNqYQlmSfTuz05Qi62wqyfC 8 | twIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /samples/signature-test-data/sbom.sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlynk-io/sbomqs/7fb5cece50e89250a90515e0401d9348b3c5c11e/samples/signature-test-data/sbom.sig -------------------------------------------------------------------------------- /samples/test-license.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", 3 | "bomFormat": "CycloneDX", 4 | "specVersion": "1.4", 5 | "serialNumber": "urn:uuid:3337e3a3-62e6-4cbb-abf5-51284a43f9f2", 6 | "version": 1, 7 | "metadata": { 8 | "timestamp": "2023-05-04T02:34:37-07:00", 9 | "tools": [ 10 | { 11 | "vendor": "CycloneDX", 12 | "name": "cyclonedx-gomod", 13 | "version": "v1.4.0", 14 | "hashes": [ 15 | { 16 | "alg": "MD5", 17 | "content": "f37a3d3473b89e4ad73e84547e0f40ac" 18 | }, 19 | { 20 | "alg": "SHA-1", 21 | "content": "a984dfd1da110417ac1d961111113a565db172b5" 22 | }, 23 | { 24 | "alg": "SHA-256", 25 | "content": "3eae94260619fa7a79c64bb0549f7005c9b422306d88251cbcb43f095d978a46" 26 | }, 27 | { 28 | "alg": "SHA-384", 29 | "content": "eab77f9e180c7846293c859e7ba4779cbd1c41f9414ab7759bb1a59aa2e98957a88f2f58ff74528467e17533b92f759e" 30 | }, 31 | { 32 | "alg": "SHA-512", 33 | "content": "b1385b31ac001811370f2a2a45c3b5cd3bda9e523c00cc33b55192068bf03b75624d9e86740bf167d9ddd3e7f895913876b8e01221d5a35f9f59913b63cef925" 34 | } 35 | ], 36 | "externalReferences": [ 37 | { 38 | "url": "https://github.com/CycloneDX/cyclonedx-gomod", 39 | "type": "vcs" 40 | }, 41 | { 42 | "url": "https://cyclonedx.org", 43 | "type": "website" 44 | } 45 | ] 46 | } 47 | ], 48 | "component": { 49 | "bom-ref": "pkg:golang/github.com/interlynk-io/sbomqs@v0.0.16-0.20230424202416-6a969c2dcfe4?type=module", 50 | "type": "application", 51 | "name": "github.com/interlynk-io/sbomqs", 52 | "version": "v0.0.16-0.20230424202416-6a969c2dcfe4", 53 | "purl": "pkg:golang/github.com/interlynk-io/sbomqs@v0.0.16-0.20230424202416-6a969c2dcfe4?type=module\u0026goos=linux\u0026goarch=amd64", 54 | "externalReferences": [ 55 | { 56 | "url": "https://github.com/interlynk-io/sbomqs", 57 | "type": "vcs" 58 | } 59 | ], 60 | "properties": [ 61 | { 62 | "name": "cdx:gomod:build:env:CGO_ENABLED", 63 | "value": "1" 64 | }, 65 | { 66 | "name": "cdx:gomod:build:env:GOARCH", 67 | "value": "amd64" 68 | }, 69 | { 70 | "name": "cdx:gomod:build:env:GOOS", 71 | "value": "linux" 72 | }, 73 | { 74 | "name": "cdx:gomod:build:env:GOVERSION", 75 | "value": "go1.20.3" 76 | } 77 | ], 78 | "licenses": [ 79 | { 80 | "license": { 81 | "id": "Apache-2.0" 82 | } 83 | } 84 | ] 85 | } 86 | }, 87 | "components": [ 88 | { 89 | "bom-ref": "pkg:golang/github.com/CycloneDX/cyclonedx-go@v0.7.1?type=module", 90 | "type": "library", 91 | "name": "github.com/CycloneDX/cyclonedx-go", 92 | "version": "v0.7.1", 93 | "scope": "required", 94 | "purl": "pkg:golang/github.com/CycloneDX/cyclonedx-go@v0.7.1?type=module\u0026goos=linux\u0026goarch=amd64", 95 | "externalReferences": [ 96 | { 97 | "url": "https://github.com/CycloneDX/cyclonedx-go", 98 | "type": "vcs" 99 | } 100 | ], 101 | "licenses": [ 102 | { 103 | "license": { 104 | "id": "Apache-2.0" 105 | } 106 | } 107 | ] 108 | }, 109 | { 110 | "bom-ref": "pkg:golang/github.com/DependencyTrack/client-go@v0.9.0?type=module", 111 | "type": "library", 112 | "name": "github.com/DependencyTrack/client-go", 113 | "version": "v0.9.0", 114 | "scope": "required", 115 | "purl": "pkg:golang/github.com/DependencyTrack/client-go@v0.9.0?type=module\u0026goos=linux\u0026goarch=amd64", 116 | "externalReferences": [ 117 | { 118 | "url": "https://github.com/DependencyTrack/client-go", 119 | "type": "vcs" 120 | } 121 | ], 122 | "licenses": [ 123 | { 124 | "license": { 125 | "id": "Apache-2.0" 126 | } 127 | } 128 | ] 129 | }, 130 | { 131 | "bom-ref": "pkg:golang/github.com/common-nighthawk/go-figure@v0.0.0-20210622060536-734e95fb86be?type=module", 132 | "type": "library", 133 | "name": "github.com/common-nighthawk/go-figure", 134 | "version": "v0.0.0-20210622060536-734e95fb86be", 135 | "scope": "required", 136 | "purl": "pkg:golang/github.com/common-nighthawk/go-figure@v0.0.0-20210622060536-734e95fb86be?type=module\u0026goos=linux\u0026goarch=amd64", 137 | "externalReferences": [ 138 | { 139 | "url": "https://github.com/common-nighthawk/go-figure", 140 | "type": "vcs" 141 | } 142 | ], 143 | "licenses": [ 144 | { 145 | "license": { 146 | "id": "MIT" 147 | } 148 | } 149 | ] 150 | }, 151 | { 152 | "bom-ref": "pkg:golang/github.com/google/uuid@v1.3.0?type=module", 153 | "type": "library", 154 | "name": "github.com/google/uuid", 155 | "version": "v1.3.0", 156 | "scope": "required", 157 | "purl": "pkg:golang/github.com/google/uuid@v1.3.0?type=module\u0026goos=linux\u0026goarch=amd64", 158 | "externalReferences": [ 159 | { 160 | "url": "https://github.com/google/uuid", 161 | "type": "vcs" 162 | } 163 | ], 164 | "licenses": [ 165 | { 166 | "expression": "Apache-2.0 AND (MIT OR GPL-2.0-only)" 167 | } 168 | ] 169 | }, 170 | { 171 | "bom-ref": "pkg:golang/github.com/mattn/go-runewidth@v0.0.14?type=module", 172 | "type": "library", 173 | "name": "github.com/mattn/go-runewidth", 174 | "version": "v0.0.14", 175 | "scope": "required", 176 | "purl": "pkg:golang/github.com/mattn/go-runewidth@v0.0.14?type=module\u0026goos=linux\u0026goarch=amd64", 177 | "externalReferences": [ 178 | { 179 | "url": "https://github.com/mattn/go-runewidth", 180 | "type": "vcs" 181 | } 182 | ], 183 | "licenses": [ 184 | { 185 | "license": { 186 | "name": "Custom MIT" 187 | } 188 | } 189 | ] 190 | }, 191 | { 192 | "bom-ref": "pkg:golang/sigs.k8s.io/yaml@v1.3.0?type=module", 193 | "type": "library", 194 | "name": "sigs.k8s.io/yaml", 195 | "version": "v1.3.0", 196 | "scope": "required", 197 | "purl": "pkg:golang/sigs.k8s.io/yaml@v1.3.0?type=module\u0026goos=linux\u0026goarch=amd64" 198 | } 199 | ] 200 | } --------------------------------------------------------------------------------