diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index fad4ad6..6a8c676 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -60,7 +60,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v2.20.4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -71,7 +71,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v2.20.4 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -84,4 +84,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v2.20.4 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 59e07c9..77bf152 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@80e868c13c90f172d68d1f4501dee99e2479f7af # v1.1.1 + uses: ossf/scorecard-action@08b4669551908b1024bb425080c797723083c031 # v1.1.1 with: results_file: results.sarif results_format: sarif @@ -57,6 +57,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@5f532563584d71fdef14ee64d17bafb34f751ce5 # v1.0.26 + uses: github/codeql-action/upload-sarif@v2.20.4 # v1.0.26 with: sarif_file: results.sarif diff --git a/Makefile b/Makefile index 752df65..c811989 100644 --- a/Makefile +++ b/Makefile @@ -9,10 +9,12 @@ GOLINTER_VERSION := v1.52.2 # OCI registry ZOT := $(TOOLSDIR)/bin/zot -ZOT_VERSION := 1.4.3 +ZOT_VERSION := 2.0.0-rc5 # OCI registry clients ORAS := $(TOOLSDIR)/bin/oras ORAS_VERSION := 1.0.0-rc.1 +REGCTL := $(TOOLSDIR)/bin/regctl +REGCTL_VERSION := 0.5.0 BATS := $(TOOLSDIR)/bin/bats BINARY := stacker-bom @@ -52,6 +54,11 @@ $(ORAS): tar xvzf oras.tar.gz -C $(TOOLSDIR)/bin oras rm oras.tar.gz +$(REGCTL): + mkdir -p $(TOOLSDIR)/bin + curl -Lo $(REGCTL) https://github.com/regclient/regclient/releases/download/v$(REGCTL_VERSION)/regctl-linux-amd64 + chmod +x $(REGCTL) + $(BATS): rm -rf bats-core; \ git clone https://github.com/bats-core/bats-core.git; \ @@ -59,7 +66,7 @@ $(BATS): rm -rf bats-core .PHONY: test -test: $(BATS) $(ZOT) $(ORAS) +test: $(BATS) $(ZOT) $(ORAS) $(REGCTL) go test -v -race -cover -coverpkg=./... $(BATS) --trace --verbose-run --print-output-on-failure --show-output-of-passing-tests test/*.bats diff --git a/go.mod b/go.mod index 618b21d..eddc83f 100644 --- a/go.mod +++ b/go.mod @@ -6,13 +6,15 @@ require ( github.com/anchore/syft v0.80.0 github.com/gabriel-vasile/mimetype v1.4.2 github.com/knqyf263/go-rpmdb v0.0.0-20230301153543-ba94b245509b + github.com/martencassel/go-apkutils v0.2.9 github.com/minio/sha256-simd v1.0.0 github.com/mitchellh/mapstructure v1.5.0 github.com/rs/zerolog v1.29.1 - github.com/sirupsen/logrus v1.9.0 + github.com/sassoftware/go-rpmutils v0.2.0 + github.com/sirupsen/logrus v1.9.3 github.com/spdx/tools-golang v0.5.0 github.com/spf13/cobra v1.7.0 - pault.ag/go/debian v0.12.0 + pault.ag/go/debian v0.15.0 sigs.k8s.io/bom v0.5.2-0.20230512052447-fef7b03b207d ) @@ -36,14 +38,14 @@ require ( github.com/becheran/wildmatch-go v1.0.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect - github.com/cloudflare/circl v1.1.0 // indirect + github.com/cloudflare/circl v1.3.3 // indirect github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect github.com/containerd/containerd v1.7.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da // indirect github.com/docker/cli v23.0.1+incompatible // indirect - github.com/docker/distribution v2.8.1+incompatible // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect github.com/docker/docker v23.0.5+incompatible // indirect github.com/docker/docker-credential-helpers v0.7.0 // indirect github.com/docker/go-connections v0.4.0 // indirect @@ -97,7 +99,6 @@ require ( github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/rivo/uniseg v0.2.0 // indirect - github.com/sassoftware/go-rpmutils v0.2.0 // indirect github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e // indirect github.com/secure-systems-lab/go-securesystemslib v0.6.0 // indirect github.com/sergi/go-diff v1.3.1 // indirect @@ -119,7 +120,7 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect gitlab.alpinelinux.org/alpine/go v0.7.0 // indirect - golang.org/x/crypto v0.8.0 // indirect + golang.org/x/crypto v0.9.0 // indirect golang.org/x/exp v0.0.0-20230202163644-54bba9f4231b // indirect golang.org/x/mod v0.10.0 // indirect golang.org/x/net v0.10.0 // indirect @@ -136,6 +137,6 @@ require ( gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - pault.ag/go/topsort v0.0.0-20160530003732-f98d2ad46e1a // indirect + pault.ag/go/topsort v0.1.1 // indirect sigs.k8s.io/release-utils v0.7.4 // indirect ) diff --git a/go.sum b/go.sum index a23f6a8..912eab1 100644 --- a/go.sum +++ b/go.sum @@ -132,8 +132,9 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.1.0 h1:bZgT/A+cikZnKIwn7xL2OBj012Bmvho/o6RpRvv3GKY= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= +github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -166,8 +167,8 @@ github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da/go.mod h1:B3tI9iGHi4i github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4= github.com/docker/cli v23.0.1+incompatible h1:LRyWITpGzl2C9e9uGxzisptnxAn1zfZKXy13Ul2Q5oM= github.com/docker/cli v23.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= -github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v23.0.5+incompatible h1:DaxtlTJjFSnLOXVNUBU1+6kXGz2lpDoEAH6QoxaSg8k= github.com/docker/docker v23.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= @@ -420,6 +421,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/martencassel/go-apkutils v0.2.9 h1:EHttpwXRK5rvs/fOmmYWyzV2fM6mkyUYLZRZ2IMkkso= +github.com/martencassel/go-apkutils v0.2.9/go.mod h1:ykCCYUKwa+SnUgPjVZDqJJuv4hSXiqczYRXRlw7ClDE= github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -556,8 +559,8 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skeema/knownhosts v1.1.0 h1:Wvr9V0MxhjRbl3f9nMnKnFfiWTJmtECJ9Njkea3ysW0= github.com/skeema/knownhosts v1.1.0/go.mod h1:sKFq3RD6/TKZkSWn8boUbDC7Qkgcv+8XXijpFO6roag= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -655,7 +658,6 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -667,8 +669,8 @@ golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= -golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1157,10 +1159,10 @@ modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= modernc.org/sqlite v1.22.1 h1:P2+Dhp5FR1RlVRkQ3dDfCiv3Ok8XPxqpe70IjYVA9oE= -pault.ag/go/debian v0.12.0 h1:b8ctSdBSGJ98NE1VLn06aSx70EUpczlP2qqSHEiYYJA= -pault.ag/go/debian v0.12.0/go.mod h1:UbnMr3z/KZepjq7VzbYgBEfz8j4+Pyrm2L5X1fzhy/k= -pault.ag/go/topsort v0.0.0-20160530003732-f98d2ad46e1a h1:WwS7vlB5H2AtwKj1jsGwp2ZLud1x6WXRXh2fXsRqrcA= -pault.ag/go/topsort v0.0.0-20160530003732-f98d2ad46e1a/go.mod h1:INqx0ClF7kmPAMk2zVTX8DRnhZ/yaA/Mg52g8KFKE7k= +pault.ag/go/debian v0.15.0 h1:x2VLkGx/nTvGdgW0+FzFSgQZKt7zo9zSVYkikRjNTns= +pault.ag/go/debian v0.15.0/go.mod h1:JFl0XWRCv9hWBrB5MDDZjA5GSEs1X3zcFK/9kCNIUmE= +pault.ag/go/topsort v0.1.1 h1:L0QnhUly6LmTv0e3DEzbN2q6/FGgAcQvaEw65S53Bg4= +pault.ag/go/topsort v0.1.1/go.mod h1:r1kc/L0/FZ3HhjezBIPaNVhkqv8L0UJ9bxRuHRVZ0q4= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= diff --git a/pkg/distro/apk/apk.go b/pkg/distro/apk/apk.go new file mode 100644 index 0000000..07ef519 --- /dev/null +++ b/pkg/distro/apk/apk.go @@ -0,0 +1,337 @@ +package apk + +import ( + "archive/tar" + "bufio" + "compress/gzip" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/martencassel/go-apkutils/apk" + "github.com/rs/zerolog/log" + "sigs.k8s.io/bom/pkg/spdx" + "stackerbuild.io/stacker-bom/pkg/bom" + "stackerbuild.io/stacker-bom/pkg/buildgen" +) + +// https://wiki.alpinelinux.org/wiki/Apk_spec + +type IndexEntry struct { + PullChecksum string + PackageName string + PackageVersion string + PackageArchitecture string + PackageSize string + PackageInstalledSize string + PackageDescription string + PackageURL string + PackageLicense string + PackageOrigin string + PackageMaintainer string + BuildTimeStamp string + GitCommitAport string + PullDependencies string + PackageProvides string + Acls []*ACLEntry +} + +type ACLEntry struct { + DirName string + DirMode string + RelFileName string + FileMode string + FileChecksum string +} + +// ParsePackage given a apk pkg emits a sbom. +func ParsePackage(input, output, author, organization, license string) error { + fhandle, err := os.Open(input) + if err != nil { + log.Error().Err(err).Str("input", input).Msg("unable to open file") + + return err + } + defer fhandle.Close() + + apk, err := apk.ReadApk(fhandle) + if err != nil { + log.Error().Err(err).Str("path", input).Msg("unable to load package") + + return err + } + + sdoc := spdx.NewDocument() + sdoc.Creator.Person = author + sdoc.Creator.Organization = organization + sdoc.Creator.Tool = []string{"stackerbuild.io/sbom"} + sdoc.Creator.Tool = []string{fmt.Sprintf("stackerbuild.io/sbom@%s", buildgen.Commit)} + + var pkglicense string + + if apk.PkgInfo.PkgLicense == "" { + pkglicense = license + } else { + pkglicense = apk.PkgInfo.PkgLicense + } + + spkg := &spdx.Package{ + Entity: spdx.Entity{ + Name: apk.PkgInfo.PkgName, + }, + Version: apk.PkgInfo.PkgVer, + Originator: struct { + Person string + Organization string + }{ + Person: apk.PkgInfo.PkgMaintainer, + }, + LicenseDeclared: pkglicense, + } + + if err := sdoc.AddPackage(spkg); err != nil { + log.Error().Err(err).Msg("unable to add package to doc") + + return err + } + + tgzfh, err := os.Open(input) + if err != nil { + return err + } + defer tgzfh.Close() + + gzfh, err := gzip.NewReader(tgzfh) + if err != nil { + return err + } + + trfh := tar.NewReader(gzfh) + + for { + hdr, err := trfh.Next() + if err != nil && !errors.Is(err, io.EOF) { + log.Error().Err(err).Msg("unable to get next content") + + return err + } + + if hdr == nil { + break + } + + if hdr.Typeflag != tar.TypeReg { + log.Warn().Str("name", hdr.Name).Msg("ignoring entry") + + continue + } + + if strings.HasPrefix(hdr.Name, ".PKGINFO") || strings.HasPrefix(hdr.Name, ".SIGN") { + log.Warn().Str("name", hdr.Name).Msg("ignoring entry") + + continue + } + + buf := make([]byte, hdr.Size) + + var bufsz int + + if bufsz, err = trfh.Read(buf); err != nil { + if !errors.Is(err, io.EOF) { + log.Error().Err(err).Str("name", hdr.Name).Msg("unable to read content") + + return err + } + } + + cksum := sha256.Sum256(buf) + + log.Info().Str("name", hdr.Name). + Int("size", bufsz). + Str("cksum", fmt.Sprintf("SHA256:%s", hex.EncodeToString(cksum[:]))). + Msg("file entry detected") + + sfile := &spdx.File{ + Entity: spdx.Entity{ + Name: fmt.Sprintf("/%s", hdr.Name), + Checksum: map[string]string{"SHA256": hex.EncodeToString(cksum[:])}, + }, + } + if err := spkg.AddFile(sfile); err != nil { + log.Error().Err(err).Msg("unable to add file to package") + + return err + } + } + + if err := bom.WriteDocument(sdoc, output); err != nil { + log.Error().Err(err).Str("path", output).Msg("unable to write output") + + return err + } + + return nil +} + +func InstalledPackage(doc *spdx.Document, pkg *IndexEntry, files []string) error { + spkg := &spdx.Package{ + Entity: spdx.Entity{ + Name: pkg.PackageName, + }, + Version: pkg.PackageVersion, + Originator: struct { + Person string + Organization string + }{ + Person: pkg.PackageMaintainer, + }, + LicenseDeclared: pkg.PackageLicense, + } + + for _, file := range files { + sinfo, err := os.Stat(file) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + continue + } + + return err + } + + if !sinfo.Mode().IsRegular() { + log.Warn().Str("file", file).Interface("mode", sinfo.Mode()).Msg("skipping entry since not a regular file") + + continue + } + + fhandle, err := os.Open(file) + if err != nil { + return err + } + defer fhandle.Close() + + shaWriter := sha256.New() + if _, err := io.Copy(shaWriter, fhandle); err != nil { + return err + } + + cksum := shaWriter.Sum(nil) + + sfile := spdx.NewFile() + sfile.SetEntity( + &spdx.Entity{ + Name: file, + Checksum: map[string]string{"SHA256": hex.EncodeToString(cksum)}, + }, + ) + + if err := spkg.AddFile(sfile); err != nil { + log.Error().Err(err).Msg("unable to add file to package") + + return err + } + } + + if err := doc.AddPackage(spkg); err != nil { + log.Error().Err(err).Msg("unable to add package to doc") + + return err + } + + return nil +} + +func InstalledPackages(doc *spdx.Document) error { + path := "/lib/apk/db/installed" + + fhandle, err := os.Open(path) + if err != nil { + return err + } + defer fhandle.Close() + + scanner := bufio.NewScanner(fhandle) + scanner.Split(bufio.ScanLines) + + var pkg *IndexEntry + lastDir := "" + files := []string{} + + for scanner.Scan() { + line := scanner.Text() + + match, err := regexp.MatchString(`^$`, line) //nolint:staticcheck + if err != nil { + return err + } + + if match { + if err := InstalledPackage(doc, pkg, files); err != nil { + return err + } + + // a new package + pkg = &IndexEntry{} + + continue + } + + if pkg == nil { + // a new package + pkg = &IndexEntry{} + } + + rgxp := regexp.MustCompile(`^(?P[a-zA-Z-]+?):\s*(?P.*)$`) + params := rgxp.FindStringSubmatch(line) + key := params[rgxp.SubexpIndex("Key")] + val := params[rgxp.SubexpIndex("Value")] + + switch key { + case "C": + log.Debug().Str("package", val).Msg("package found") + + if val[:2] != "Q1" { + log.Error().Err(err).Str("type", val[:2]).Msg("unknown checksum type") + } + + dec, err := base64.StdEncoding.DecodeString(val[2:]) + if err != nil { + log.Error().Err(err).Str("type", val[:2]).Msg("unknown checksum type") + + return err + } + + log.Info().Str("checksum", string(dec)).Msg("sha1") + case "P": + log.Debug().Str("package", val).Msg("package found") + pkg.PackageName = val + case "V": + pkg.PackageVersion = val + case "A": + pkg.PackageArchitecture = val + case "I": + pkg.PackageDescription = val + case "U": + pkg.PackageURL = val + case "L": + pkg.PackageLicense = val + case "o": + pkg.PackageOrigin = val + case "m": + pkg.PackageMaintainer = val + case "F": + lastDir = val + case "R": + files = append(files, filepath.Join("/", lastDir, val)) + } + } + + return nil +} diff --git a/pkg/distro/deb/deb.go b/pkg/distro/deb/deb.go index d778ce7..eeb76f2 100644 --- a/pkg/distro/deb/deb.go +++ b/pkg/distro/deb/deb.go @@ -69,8 +69,8 @@ func ParsePackage(input, output, author, organization, license string) error { break } - if hdr.Typeflag == tar.TypeDir { - log.Warn().Str("name", hdr.Name).Msg("ignoring dir entry") + if hdr.Typeflag != tar.TypeReg { + log.Warn().Str("name", hdr.Name).Msg("ignoring entry") continue } diff --git a/pkg/distro/distro.go b/pkg/distro/distro.go index 1d99608..6b04bbe 100644 --- a/pkg/distro/distro.go +++ b/pkg/distro/distro.go @@ -7,6 +7,7 @@ import ( "github.com/rs/zerolog/log" "sigs.k8s.io/bom/pkg/spdx" "stackerbuild.io/stacker-bom/errors" + "stackerbuild.io/stacker-bom/pkg/distro/apk" "stackerbuild.io/stacker-bom/pkg/distro/deb" "stackerbuild.io/stacker-bom/pkg/distro/rpm" ) @@ -26,7 +27,12 @@ func InstalledPackages(doc *spdx.Document) error { log.Error().Err(rpmerr).Msg("rpm: unable to get installed packages") } - if deberr != nil && rpmerr != nil { + apkerr := apk.InstalledPackages(doc) + if apkerr != nil { + log.Error().Err(apkerr).Msg("apk: unable to get installed packages") + } + + if deberr != nil && rpmerr != nil && apkerr != nil { return errors.ErrNotFound } @@ -46,6 +52,10 @@ func ParsePackage(input, author, organization, license, output string) error { switch mtype.String() { case "application/vnd.debian.binary-package": return deb.ParsePackage(input, output, author, organization, license) + case "application/x-rpm": + return rpm.ParsePackage(input, output, author, organization, license) + case "application/gzip": // best effort + return apk.ParsePackage(input, output, author, organization, license) default: return fmt.Errorf("%w: mime-type %s", errors.ErrUnsupported, mtype.String()) } diff --git a/pkg/distro/rpm/rpm.go b/pkg/distro/rpm/rpm.go index 41125de..d4e68ae 100644 --- a/pkg/distro/rpm/rpm.go +++ b/pkg/distro/rpm/rpm.go @@ -3,15 +3,153 @@ package rpm import ( "crypto/sha256" "encoding/hex" + "fmt" "io" "os" "strings" rpmdb "github.com/knqyf263/go-rpmdb/pkg" "github.com/rs/zerolog/log" + "github.com/sassoftware/go-rpmutils" "sigs.k8s.io/bom/pkg/spdx" + "stackerbuild.io/stacker-bom/pkg/bom" + "stackerbuild.io/stacker-bom/pkg/buildgen" ) +// ParsePackage given a rpm pkg emits a sbom. +func ParsePackage(input, output, author, organization, license string) error { + fhandle, err := os.Open(input) + if err != nil { + log.Error().Err(err).Str("path", input).Msg("unable to open file") + + return err + } + + defer fhandle.Close() + + rpmfile, err := rpmutils.ReadRpm(fhandle) + if err != nil { + log.Error().Err(err).Str("path", input).Msg("unable to load package") + + return err + } + + // Getting metadata + nevra, err := rpmfile.Header.GetNEVRA() + if err != nil { + return err + } + + vendor, err := rpmfile.Header.GetStrings(rpmutils.VENDOR) + if err != nil { + return err + } + + lic, err := rpmfile.Header.GetStrings(rpmutils.LICENSE) + if err != nil { + return err + } + + var pkglicense string + + if len(lic) == 0 { + pkglicense = license + } else { + pkglicense = strings.Join(lic, " ") + } + + desc, err := rpmfile.Header.GetStrings(rpmutils.DESCRIPTION) + if err != nil { + return err + } + + url, err := rpmfile.Header.GetStrings(rpmutils.URL) + if err != nil { + return err + } + + sdoc := spdx.NewDocument() + sdoc.Creator.Person = author + sdoc.Creator.Organization = organization + sdoc.Creator.Tool = []string{"stackerbuild.io/sbom"} + sdoc.Creator.Tool = []string{fmt.Sprintf("stackerbuild.io/sbom@%s", buildgen.Commit)} + + spkg := &spdx.Package{ + Entity: spdx.Entity{ + Name: nevra.Name, + DownloadLocation: url[0], + }, + Version: nevra.Version, + Comment: desc[0], + Originator: struct { + Person string + Organization string + }{ + Organization: vendor[0], + }, + LicenseDeclared: pkglicense, + } + + if err := sdoc.AddPackage(spkg); err != nil { + log.Error().Err(err).Msg("unable to add package to doc") + + return err + } + + finfos, err := rpmfile.Header.GetFiles() + if err != nil { + return err + } + + for _, finfo := range finfos { + info, err := os.Lstat(finfo.Name()) + if err != nil { + log.Warn().Str("package", nevra.Name).Str("version", nevra.Version).Str("file", finfo.Name()).Msg("file is missing!") + + continue + } + + if !info.Mode().IsRegular() { + continue + } + + fhandle, err := os.Open(finfo.Name()) + if err != nil { + return err + } + defer fhandle.Close() + + shaWriter := sha256.New() + if _, err := io.Copy(shaWriter, fhandle); err != nil { + return err + } + + cksum := shaWriter.Sum(nil) + + sfile := spdx.NewFile() + sfile.SetEntity( + &spdx.Entity{ + Name: finfo.Name(), + Checksum: map[string]string{"SHA256": hex.EncodeToString(cksum)}, + }, + ) + + if err := spkg.AddFile(sfile); err != nil { + log.Error().Err(err).Msg("unable to add file to package") + + return err + } + } + + if err := bom.WriteDocument(sdoc, output); err != nil { + log.Error().Err(err).Str("path", output).Msg("unable to write output") + + return err + } + + return nil +} + func InstalledPackage(doc *spdx.Document, pkg *rpmdb.PackageInfo) error { spkg := &spdx.Package{ Entity: spdx.Entity{ diff --git a/test/bom.bats b/test/bom.bats index e4a14bd..f9a4126 100644 --- a/test/bom.bats +++ b/test/bom.bats @@ -12,7 +12,7 @@ function teardown() { @test "bom workflow" { # inventory - docker run -v ${TOPDIR}/bin:/opt/bin -v ${BOMD}:/stacker-artifacts -i ubuntu:latest /opt/bin/stacker-bom-linux-amd64 inventory -x /proc,/sys,/dev,/tmp,/opt,/stacker-artifacts -o /stacker-artifacts/inventory.json + docker run -v ${TOPDIR}/bin:/opt/bin -v ${BOMD}:/stacker-artifacts -i ubuntu:latest /opt/bin/stacker-bom-linux-amd64 inventory -x /proc,/sys,/dev,/tmp,/opt,/var/lib/dpkg/info,/var/log,/var/cache,/var/lib/systemd,/var/lib/dpkg,/var/lib/apt,/var/lib/pam,/var/lib/shells.state,/stacker-artifacts -o /stacker-artifacts/inventory.json [ -f ${BOMD}/inventory.json ] # discover installed packages docker run -v ${TOPDIR}/bin:/opt/bin -v ${BOMD}:/stacker-artifacts -i ubuntu:latest /opt/bin/stacker-bom-linux-amd64 discover -o /stacker-artifacts/discover.json @@ -23,7 +23,7 @@ function teardown() { # push the image skopeo copy --format=oci --dest-tls-verify=false docker://ubuntu:latest docker://${ZOT_HOST}:${ZOT_PORT}/ubuntu:latest # attach bom artifacts as references - oras attach --plain-http --image-spec v1.1-image --artifact-type vnd.stacker-bom.inventory ${ZOT_HOST}:${ZOT_PORT}/ubuntu:latest ${BOMD}/inventory.json - oras attach --plain-http --image-spec v1.1-image --artifact-type application/org.spdx+json ${ZOT_HOST}:${ZOT_PORT}/ubuntu:latest ${BOMD}/discover.json - oras discover --plain-http ${ZOT_HOST}:${ZOT_PORT}/ubuntu:latest + regctl artifact put --artifact-type application/vnd.stacker-bom.inventory -f ${BOMD}/inventory.json --subject ${ZOT_HOST}:${ZOT_PORT}/ubuntu:latest + regctl artifact put --artifact-type application/org.spdx+json -f ${BOMD}/discover.json --subject ${ZOT_HOST}:${ZOT_PORT}/ubuntu:latest + regctl artifact tree ${ZOT_HOST}:${ZOT_PORT}/ubuntu:latest } diff --git a/test/helpers.bash b/test/helpers.bash index fc7ec8c..9810f47 100644 --- a/test/helpers.bash +++ b/test/helpers.bash @@ -53,6 +53,8 @@ EOF echo "Timed out waiting for zot" exit 1 fi + # setup a OCI client + regctl registry set --tls=disabled $ZOT_HOST:$ZOT_PORT } function common_teardown {