Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add SPDX SBOM generation option #5

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 0 additions & 28 deletions .github/workflows/bats.yml

This file was deleted.

56 changes: 56 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: "CI"
on: [push, pull_request]

jobs:
bats:
name: bats
runs-on: ubuntu-latest
steps:

- name: Setup bats
uses: mig4/setup-bats@v1
with:
bats-version: 1.9.0

- name: Check out code
uses: actions/checkout@v4

- name: Configure git (user name)
run: git config --global user.name "Decomposer (CI)"

- name: Configure git (user email)
run: git config --global user.email "[email protected]"

- name: Configure git (default branch)
run: git config --global init.defaultBranch master

- name: Run bats
run: bats --print-output-on-failure -r tests

jsonschema:
name: jsonschema
runs-on: ubuntu-latest
Comment on lines +30 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might be better in a separate workflow. I think the curl is not enough and we need an actual git clone to work with, and that would open up the road to a testsuite where we clone and test with multiple different repos.

env:
DECOMPOSER_TARGET_DIR: /tmp/decomposer
steps:

- name: Check out code
uses: actions/checkout@v4

- name: Install JSON schema
run: pip install check-jsonschema

- name: Fetch decomposer.json from Lunr
run: curl -o decomposer.json https://raw.githubusercontent.com/lunr-php/lunr/HEAD/decomposer.json

- name: Create target dir
run: mkdir -p "$DECOMPOSER_TARGET_DIR"

- name: Install libraries
run: bin/decomposer install

- name: Generate SBOM
run: bin/decomposer sbom

- name: Validate
run: check-jsonschema --schemafile https://raw.githubusercontent.com/spdx/spdx-spec/v2.3/schemas/spdx-schema.json decomposer.sbom.json
2 changes: 2 additions & 0 deletions bin/decomposer
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ if [ "${COMMAND}" = 'help' ]; then
fi

CWD=$( pwd )
DECOMPOSER_VERSION="1.0"
DECOMPOSER_ROOT="$( dirname "$(realpath $0)" )/.."
COMMAND_FILE="${DECOMPOSER_ROOT}/libexec/decomposer/decomposer-${COMMAND}"

Expand All @@ -35,6 +36,7 @@ source "${DECOMPOSER_ROOT}/share/decomposer/helpers.sh"

export CWD
export DECOMPOSER_ROOT
export DECOMPOSER_VERSION
export -f exit_error

exec "${COMMAND_FILE}" "$@"
117 changes: 117 additions & 0 deletions libexec/decomposer/decomposer-sbom
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#!/usr/bin/env bash

DECOMPOSER_TARGET_DIR=${DECOMPOSER_TARGET_DIR:-/var/www/libs/}

TMP_FILE=$( mktemp )
trap 'rm "${TMP_FILE}"' EXIT

SBOM_FILE=${CWD}/decomposer.sbom.json
NAMESPACE="$(git remote get-url origin)#$(git rev-parse HEAD)/$(basename $SBOM_FILE)"

parse_arguments() {
while [ "$#" -gt 0 ]; do
case "$1" in
"-f" | "--file")
if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then
case "$2" in
/*) SBOM_FILE="$2" ;;
*) SBOM_FILE="${CWD}/$2" ;;
esac
shift
fi
;;
*)
printf "Unknown option %s\n" "$1"
if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then
shift
fi
;;
esac
shift
done
}

validate_arguments() {
if ! [ -f "${CWD}/decomposer.json" ]; then
exit_error 'No decomposer.json found.'
fi

if [ -f "${SBOM_FILE}" ]; then
printf 'Backing up old SBOM...'
mv "${SBOM_FILE}" "${SBOM_FILE}.backup"

case "$?" in
0) printf 'done\n' ;;
1) printf 'failed (%s)\n' "${status_text}" ;;
2) printf 'skipped (%s)\n' "${status_text}" ;;
esac
fi

# Clear SBOM and test write
if ! printf '' 2> /dev/null > "${SBOM_FILE}"; then
exit_error "File '${SBOM_FILE}' is not writable."
fi
}

process_file() {
local file="$1"

printf '{' > "${TMP_FILE}"
map_libraries_object "${file}" process_library
printf '}' >> "${TMP_FILE}"

echo '{}' |
jq --argjson packages "$(sed 's/,}$/}/' "${TMP_FILE}")" \
--arg name $(basename "${CWD}") \
--arg version "${DECOMPOSER_VERSION}" \
--arg namespace "$NAMESPACE" \
--from-file "${DECOMPOSER_ROOT}/share/decomposer/sbom.jq" > "${SBOM_FILE}"
}

process_library() {
local name="$1"
local object="$2"

local status_text

printf 'Processing %s...' "${name}"

write_locked_library "${name}" "${object}" status_text

case "$?" in
0) printf 'done\n' ;;
1) printf 'failed (%s)\n' "${status_text}" ;;
2) printf 'skipped (%s)\n' "${status_text}" ;;
esac
}

write_locked_library() {
local name="$1"
local object="$2"
local status_text_variable="$3"

local revision version url library_target_dir

version=$( jq -r '.version' <<< "${object}" )
revision=$( jq -r '.revision//.version' <<< "${object}" )
url=$( jq -r '.url' <<< "${object}" )

library_target_dir=$( jq -r '."target-dir"' <<< "${object}" )
if [ "${library_target_dir}" = "null" ]; then
library_target_dir="${name}-${version}"
else
library_target_dir="${name}-${version}${library_target_dir}"
fi

cd "${DECOMPOSER_TARGET_DIR}/${library_target_dir}" || return 1

local commit
commit=$(git -C "${DECOMPOSER_TARGET_DIR}/${library_target_dir}" rev-parse HEAD)

printf '"%s":{"commit": "%s", "revision": "%s", "version": "%s", "url": "%s"},' "${name}" "${commit}" "${revision}" "${version}" "${url}" >> "${TMP_FILE}"
}

parse_arguments "$@"
validate_arguments

process_file "${CWD}/decomposer.json"
34 changes: 34 additions & 0 deletions share/decomposer/sbom.jq
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
. += {
name: $name,
SPDXID: "SPDXRef-\($name)",
Comment on lines +1 to +3
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the changelog, we'd need the project version in here too. I see examples adding it to the name, which I suppose would be good enough. We could extract it from there again. A dedicated version field might be easier, though I haven't really found that in the spec. Just in some examples of other generators.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With version, you are referring to commit hashes right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, anything we can use as input for git log

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

git log 0.8.0-1-g6ed6e9fa..

This works, for example, so the output of git describe would be possible too

dataLicense: "CC0-1.0",
spdxVersion: "SPDX-2.2",
comment: "This document was created using Decomposer \($version) using information from the declared git repos.",
documentNamespace: ($namespace // "NOASSERTION"),
creationInfo: {
created: (now | todateiso8601),
creators: ["Tool: Decomposer-\($version)"]
},
packages: ($packages | to_entries | map({
SPDXID: "SPDXRef-\(.key)-\(.value.version)",
name: .key,
versionInfo: .value.version,
copyrightText: "NOASSERTION",
downloadLocation: (.value.url // "NOASSERTION"),
licenseConcluded: "NOASSERTION",
licenseDeclared: (.value.license // "NOASSERTION"),
filesAnalyzed: false,
sourceInfo: "Git checkout of the object \(.value.revision) from the repo",
annotations: [
{
annotationDate: (now | todateiso8601),
annotationType: "OTHER",
annotator: "Tool: Decomposer-\($version)",
comment: "Generated with checksum as commit hash"
}
],
checksums: [
{algorithm: "SHA1", checksumValue: .value.commit}
]
}))
}
Loading