diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index aee646413..a6540f226 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -9,7 +9,7 @@ on: - "v*" env: - GO_VERSION: "1.17.x" + GO_VERSION: "1.18.x" jobs: quality-gate: @@ -34,6 +34,16 @@ jobs: checkName: "Static analysis" ref: ${{ github.event.pull_request.head.sha || github.sha }} + - name: Check static analysis results - golangci-lint + uses: fountainhead/action-wait-for-check@v1.0.0 + id: static-analysis-golangci-lint + with: + token: ${{ secrets.GITHUB_TOKEN }} + # This check name is defined as the github action job name (in .github/workflows/testing.yaml) + checkName: "Static analysis - golangci-lint" + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - name: Check unit test results uses: fountainhead/action-wait-for-check@v1.0.0 id: unit @@ -80,9 +90,10 @@ jobs: ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Quality gate - if: steps.static-analysis.outputs.conclusion != 'success' || steps.unit.outputs.conclusion != 'success' || steps.integration.outputs.conclusion != 'success' || steps.cli-linux.outputs.conclusion != 'success' || steps.acceptance-linux.outputs.conclusion != 'success' || steps.acceptance-mac.outputs.conclusion != 'success' + if: steps.static-analysis.outputs.conclusion != 'success' || steps.static-analysis-golangci-lint.outputs.conclusion != 'success' || steps.unit.outputs.conclusion != 'success' || steps.integration.outputs.conclusion != 'success' || steps.cli-linux.outputs.conclusion != 'success' || steps.acceptance-linux.outputs.conclusion != 'success' || steps.acceptance-mac.outputs.conclusion != 'success' run: | echo "Static Analysis Status: ${{ steps.static-analysis.conclusion }}" + echo "Static Analysis Status - Golangci-Lint: ${{ steps.static-analysis-golangci-lint.conclusion }}" echo "Unit Test Status: ${{ steps.unit.outputs.conclusion }}" echo "Integration Test Status: ${{ steps.integration.outputs.conclusion }}" echo "Acceptance Test (Linux) Status: ${{ steps.acceptance-linux.outputs.conclusion }}" diff --git a/.github/workflows/validations.yaml b/.github/workflows/validations.yaml index bf50df0f1..7d45579d0 100644 --- a/.github/workflows/validations.yaml +++ b/.github/workflows/validations.yaml @@ -7,9 +7,49 @@ on: pull_request: env: - GO_VERSION: "1.17.x" + GO_VERSION: "1.18.x" + GO_STABLE_VERSION: true jobs: +# TODO: remove this job once golanci-lint is compatible with g01.18+ + Static-Analysis-Golanci-lint: + name: "Static analysis - golangci-lint" + runs-on: ubuntu-20.04 + steps: + - uses: actions/setup-go@v2 + with: + go-version: "1.17" # NOTE: please use GO_VERSION once golangci supports go1.18+ + stable: ${{ env.GO_STABLE_VERSION }} + + - uses: actions/checkout@v2 + + - name: Restore tool cache + id: tool-cache + uses: actions/cache@v2.1.3 + with: + path: ${{ github.workspace }}/.tmp + key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }} + + - name: Restore go cache + id: go-cache + uses: actions/cache@v2.1.3 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-${{ env.GO_VERSION }}- + + - name: (cache-miss) Bootstrap all project dependencies + if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true' + run: make bootstrap + + - name: Bootstrap CI environment dependencies + run: make ci-bootstrap + + - name: Run static analysis + run: make static-analysis-golanci-lint + + Static-Analysis: # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline @@ -19,6 +59,7 @@ jobs: - uses: actions/setup-go@v2 with: go-version: ${{ env.GO_VERSION }} + stable: ${{ env.GO_STABLE_VERSION }} - uses: actions/checkout@v2 @@ -56,6 +97,7 @@ jobs: - uses: actions/setup-go@v2 with: go-version: ${{ env.GO_VERSION }} + stable: ${{ env.GO_STABLE_VERSION }} - uses: actions/checkout@v2 @@ -92,6 +134,16 @@ jobs: path: syft/pkg/cataloger/java/test-fixtures/java-builds/packages key: ${{ runner.os }}-unit-java-cache-${{ hashFiles( 'syft/pkg/cataloger/java/test-fixtures/java-builds/packages.fingerprint' ) }} + - name: Build cache key for go binary test-fixture blobs (for unit tests) + run: make go-binaries-fingerprint + + - name: Restore Go binary test-fixture cache + id: unit-go-binary-cache + uses: actions/cache@v2.1.3 + with: + path: syft/pkg/cataloger/golang/test-fixtures/archs/binaries + key: ${{ runner.os }}-unit-go-binaries-cache-${{ hashFiles( 'syft/pkg/cataloger/golang/test-fixtures/archs/binaries.fingerprint' ) }} + - name: Run unit tests run: make unit @@ -108,6 +160,7 @@ jobs: - uses: actions/setup-go@v2 with: go-version: ${{ env.GO_VERSION }} + stable: ${{ env.GO_STABLE_VERSION }} - uses: actions/checkout@v2 @@ -159,6 +212,7 @@ jobs: - uses: actions/setup-go@v2 with: go-version: ${{ env.GO_VERSION }} + stable: ${{ env.GO_STABLE_VERSION }} - uses: actions/checkout@v2 @@ -231,6 +285,7 @@ jobs: - uses: actions/setup-go@v2 with: go-version: ${{ env.GO_VERSION }} + stable: ${{ env.GO_STABLE_VERSION }} - uses: actions/checkout@v2 @@ -338,6 +393,7 @@ jobs: - uses: actions/setup-go@v2 with: go-version: ${{ env.GO_VERSION }} + stable: ${{ env.GO_STABLE_VERSION }} - uses: actions/checkout@v2 diff --git a/Makefile b/Makefile index 530673722..d73028949 100644 --- a/Makefile +++ b/Makefile @@ -122,7 +122,12 @@ bootstrap: $(RESULTSDIR) bootstrap-go bootstrap-tools ## Download and install al $(call title,Bootstrapping dependencies) .PHONY: static-analysis -static-analysis: lint check-go-mod-tidy check-licenses +static-analysis: check-go-mod-tidy check-licenses + + # NOTE: isolating golanci-lint so it runs over go1.17 in CI, since it is not compatible with + # go1.18+ +.PHONY: static-analysis-golanci-lint +static-analysis-golanci-lint: lint .PHONY: lint lint: ## Run gofmt + golangci lint checks @@ -214,6 +219,12 @@ java-packages-fingerprint: cd syft/pkg/cataloger/java/test-fixtures/java-builds && \ make packages.fingerprint +.PHONY: go-binaries-fingerprint +go-binaries-fingerprint: + $(call title,Go binaries test fixture fingerprint) + cd syft/pkg/cataloger/golang/test-fixtures/archs && \ + make binaries.fingerprint + .PHONY: fixtures fixtures: $(call title,Generating test fixtures) diff --git a/go.mod b/go.mod index 23dd6aafa..054d57cdf 100644 --- a/go.mod +++ b/go.mod @@ -9,11 +9,12 @@ require ( github.com/adrg/xdg v0.2.1 github.com/alecthomas/jsonschema v0.0.0-20210301060011-54c507b6f074 github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf + github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63 github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b github.com/anchore/packageurl-go v0.1.1-0.20220314153042-1bcd40e5206b - github.com/anchore/stereoscope v0.0.0-20220307154759-8a5a70c227d3 + github.com/anchore/stereoscope v0.0.0-20220315185520-25183ec78f40 github.com/antihax/optional v1.0.0 github.com/bmatcuk/doublestar/v4 v4.0.2 github.com/dustin/go-humanize v1.0.0 diff --git a/go.sum b/go.sum index 953177d73..459671027 100644 --- a/go.sum +++ b/go.sum @@ -276,6 +276,8 @@ github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:C github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg= github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf/go.mod h1:FaODhIA06mxO1E6R32JE0TL1JWZZkmjRIAd4ULvHUKk= +github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb h1:iDMnx6LIjtjZ46C0akqveX83WFzhpTD3eqOthawb5vU= +github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb/go.mod h1:DmTY2Mfcv38hsHbG78xMiTDdxFtkHpgYNVDPsF2TgHk= github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63 h1:C9W/LAydEz/qdUhx1MdjO9l8NEcFKYknkxDVyo9LAoM= github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63/go.mod h1:6qH8c6U/3CBVvDDDBZnPSTbTINq3cIdADUYTaVf75EM= github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0vW0nnNKJfJieyH/TZ9UYAnTZs5/gHTdAe8= @@ -284,8 +286,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= github.com/anchore/packageurl-go v0.1.1-0.20220314153042-1bcd40e5206b h1:YJWYt/6KQXR9JR46lLHrTTYi8rcye42tKcyjREA/hvA= github.com/anchore/packageurl-go v0.1.1-0.20220314153042-1bcd40e5206b/go.mod h1:Blo6OgJNiYF41ufcgHKkbCKF2MDOMlrqhXv/ij6ocR4= -github.com/anchore/stereoscope v0.0.0-20220307154759-8a5a70c227d3 h1:Kx2jlMdENAf4cVjYGYLI+fiavVhzhtmU89GUYDITJ1w= -github.com/anchore/stereoscope v0.0.0-20220307154759-8a5a70c227d3/go.mod h1:XESZQTgFETDBatmyoet6XZ0zVknoIMDSAhj2INj2a5w= +github.com/anchore/stereoscope v0.0.0-20220315185520-25183ec78f40 h1:OSyFMZgEwQW0wFyv10kEi9kmB52FQFRUQmc2H6d8LTY= +github.com/anchore/stereoscope v0.0.0-20220315185520-25183ec78f40/go.mod h1:OVVJ4y/L26pspeeHNvNa7GeQfoLPWxJe13iODl9x5l4= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= diff --git a/internal/constants.go b/internal/constants.go index 2d1b40024..143aafccc 100644 --- a/internal/constants.go +++ b/internal/constants.go @@ -6,5 +6,5 @@ const ( // JSONSchemaVersion is the current schema version output by the JSON encoder // This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment. - JSONSchemaVersion = "3.1.0" + JSONSchemaVersion = "3.1.1" ) diff --git a/internal/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden b/internal/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden index 1689c4c56..9503585be 100644 --- a/internal/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden +++ b/internal/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden @@ -88,7 +88,7 @@ } }, "schema": { - "version": "3.1.0", - "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.1.0.json" + "version": "3.1.1", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.1.1.json" } } diff --git a/internal/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden b/internal/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden index 96a208e04..1f0540fb2 100644 --- a/internal/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden +++ b/internal/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden @@ -184,7 +184,7 @@ } }, "schema": { - "version": "3.1.0", - "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.1.0.json" + "version": "3.1.1", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.1.1.json" } } diff --git a/internal/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden b/internal/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden index ba6f66d7f..ff3fb05a5 100644 --- a/internal/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden +++ b/internal/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden @@ -111,7 +111,7 @@ } }, "schema": { - "version": "3.1.0", - "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.1.0.json" + "version": "3.1.1", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.1.1.json" } } diff --git a/schema/json/schema-3.1.1.json b/schema/json/schema-3.1.1.json new file mode 100644 index 000000000..e23a86a0c --- /dev/null +++ b/schema/json/schema-3.1.1.json @@ -0,0 +1,1237 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Document", + "definitions": { + "ApkFileRecord": { + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string" + }, + "ownerUid": { + "type": "string" + }, + "ownerGid": { + "type": "string" + }, + "permissions": { + "type": "string" + }, + "digest": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Digest" + } + }, + "additionalProperties": true, + "type": "object" + }, + "ApkMetadata": { + "required": [ + "package", + "originPackage", + "maintainer", + "version", + "license", + "architecture", + "url", + "description", + "size", + "installedSize", + "pullDependencies", + "pullChecksum", + "gitCommitOfApkPort", + "files" + ], + "properties": { + "package": { + "type": "string" + }, + "originPackage": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "version": { + "type": "string" + }, + "license": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "installedSize": { + "type": "integer" + }, + "pullDependencies": { + "type": "string" + }, + "pullChecksum": { + "type": "string" + }, + "gitCommitOfApkPort": { + "type": "string" + }, + "files": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/ApkFileRecord" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object" + }, + "CargoPackageMetadata": { + "required": [ + "name", + "version", + "source", + "checksum", + "dependencies" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "source": { + "type": "string" + }, + "checksum": { + "type": "string" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Classification": { + "required": [ + "class", + "metadata" + ], + "properties": { + "class": { + "type": "string" + }, + "metadata": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Coordinates": { + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string" + }, + "layerID": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Descriptor": { + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "configuration": { + "additionalProperties": true + } + }, + "additionalProperties": true, + "type": "object" + }, + "Digest": { + "required": [ + "algorithm", + "value" + ], + "properties": { + "algorithm": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Document": { + "required": [ + "artifacts", + "artifactRelationships", + "source", + "distro", + "descriptor", + "schema" + ], + "properties": { + "artifacts": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Package" + }, + "type": "array" + }, + "artifactRelationships": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Relationship" + }, + "type": "array" + }, + "files": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/File" + }, + "type": "array" + }, + "secrets": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Secrets" + }, + "type": "array" + }, + "source": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Source" + }, + "distro": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/LinuxRelease" + }, + "descriptor": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Descriptor" + }, + "schema": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Schema" + } + }, + "additionalProperties": true, + "type": "object" + }, + "DpkgFileRecord": { + "required": [ + "path", + "isConfigFile" + ], + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/definitions/Digest" + }, + "isConfigFile": { + "type": "boolean" + } + }, + "additionalProperties": true, + "type": "object" + }, + "DpkgMetadata": { + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ], + "properties": { + "package": { + "type": "string" + }, + "source": { + "type": "string" + }, + "version": { + "type": "string" + }, + "sourceVersion": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "installedSize": { + "type": "integer" + }, + "files": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/DpkgFileRecord" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object" + }, + "File": { + "required": [ + "id", + "location" + ], + "properties": { + "id": { + "type": "string" + }, + "location": { + "$ref": "#/definitions/Coordinates" + }, + "metadata": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/FileMetadataEntry" + }, + "contents": { + "type": "string" + }, + "digests": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Digest" + }, + "type": "array" + }, + "classifications": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Classification" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object" + }, + "FileMetadataEntry": { + "required": [ + "mode", + "type", + "userID", + "groupID", + "mimeType" + ], + "properties": { + "mode": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "linkDestination": { + "type": "string" + }, + "userID": { + "type": "integer" + }, + "groupID": { + "type": "integer" + }, + "mimeType": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "GemMetadata": { + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array" + }, + "authors": { + "items": { + "type": "string" + }, + "type": "array" + }, + "licenses": { + "items": { + "type": "string" + }, + "type": "array" + }, + "homepage": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "GolangBinMetadata": { + "required": [ + "goCompiledVersion", + "architecture", + "h1Digest" + ], + "properties": { + "goBuildSettings": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "goCompiledVersion": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "h1Digest": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "JavaManifest": { + "properties": { + "main": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "namedSections": { + "patternProperties": { + ".*": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + } + }, + "additionalProperties": true, + "type": "object" + }, + "JavaMetadata": { + "required": [ + "virtualPath" + ], + "properties": { + "virtualPath": { + "type": "string" + }, + "manifest": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/JavaManifest" + }, + "pomProperties": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PomProperties" + }, + "pomProject": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PomProject" + } + }, + "additionalProperties": true, + "type": "object" + }, + "LinuxRelease": { + "properties": { + "prettyName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "idLike": { + "items": { + "type": "string" + }, + "type": "array" + }, + "version": { + "type": "string" + }, + "versionID": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "variantID": { + "type": "string" + }, + "homeURL": { + "type": "string" + }, + "supportURL": { + "type": "string" + }, + "bugReportURL": { + "type": "string" + }, + "privacyPolicyURL": { + "type": "string" + }, + "cpeName": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "NpmPackageJSONMetadata": { + "required": [ + "name", + "version", + "author", + "licenses", + "homepage", + "description", + "url" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array" + }, + "author": { + "type": "string" + }, + "licenses": { + "items": { + "type": "string" + }, + "type": "array" + }, + "homepage": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Package": { + "required": [ + "id", + "name", + "version", + "type", + "foundBy", + "locations", + "licenses", + "language", + "cpes", + "purl" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "foundBy": { + "type": "string" + }, + "locations": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Coordinates" + }, + "type": "array" + }, + "licenses": { + "items": { + "type": "string" + }, + "type": "array" + }, + "language": { + "type": "string" + }, + "cpes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "purl": { + "type": "string" + }, + "metadataType": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "type": "null" + }, + { + "$ref": "#/definitions/ApkMetadata" + }, + { + "$ref": "#/definitions/CargoPackageMetadata" + }, + { + "$ref": "#/definitions/DpkgMetadata" + }, + { + "$ref": "#/definitions/GemMetadata" + }, + { + "$ref": "#/definitions/GolangBinMetadata" + }, + { + "$ref": "#/definitions/JavaMetadata" + }, + { + "$ref": "#/definitions/NpmPackageJSONMetadata" + }, + { + "$ref": "#/definitions/PhpComposerJSONMetadata" + }, + { + "$ref": "#/definitions/PythonPackageMetadata" + }, + { + "$ref": "#/definitions/RpmdbMetadata" + } + ] + } + }, + "additionalProperties": true, + "type": "object" + }, + "PhpComposerAuthors": { + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "homepage": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PhpComposerExternalReference": { + "required": [ + "type", + "url", + "reference" + ], + "properties": { + "type": { + "type": "string" + }, + "url": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "shasum": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PhpComposerJSONMetadata": { + "required": [ + "name", + "version", + "source", + "dist" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "source": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PhpComposerExternalReference" + }, + "dist": { + "$ref": "#/definitions/PhpComposerExternalReference" + }, + "require": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "provide": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "require-dev": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "suggest": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "type": { + "type": "string" + }, + "notification-url": { + "type": "string" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array" + }, + "authors": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PhpComposerAuthors" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array" + }, + "time": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PomParent": { + "required": [ + "groupId", + "artifactId", + "version" + ], + "properties": { + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PomProject": { + "required": [ + "path", + "groupId", + "artifactId", + "version", + "name" + ], + "properties": { + "path": { + "type": "string" + }, + "parent": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PomParent" + }, + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PomProperties": { + "required": [ + "path", + "name", + "groupId", + "artifactId", + "version", + "extraFields" + ], + "properties": { + "path": { + "type": "string" + }, + "name": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + }, + "extraFields": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PythonDirectURLOriginInfo": { + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string" + }, + "commitId": { + "type": "string" + }, + "vcs": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PythonFileDigest": { + "required": [ + "algorithm", + "value" + ], + "properties": { + "algorithm": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PythonFileRecord": { + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PythonFileDigest" + }, + "size": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PythonPackageMetadata": { + "required": [ + "name", + "version", + "license", + "author", + "authorEmail", + "platform", + "sitePackagesRootPath" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "license": { + "type": "string" + }, + "author": { + "type": "string" + }, + "authorEmail": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "files": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PythonFileRecord" + }, + "type": "array" + }, + "sitePackagesRootPath": { + "type": "string" + }, + "topLevelPackages": { + "items": { + "type": "string" + }, + "type": "array" + }, + "directUrlOrigin": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PythonDirectURLOriginInfo" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Relationship": { + "required": [ + "parent", + "child", + "type" + ], + "properties": { + "parent": { + "type": "string" + }, + "child": { + "type": "string" + }, + "type": { + "type": "string" + }, + "metadata": { + "additionalProperties": true + } + }, + "additionalProperties": true, + "type": "object" + }, + "RpmdbFileRecord": { + "required": [ + "path", + "mode", + "size", + "digest", + "userName", + "groupName", + "flags" + ], + "properties": { + "path": { + "type": "string" + }, + "mode": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "digest": { + "$ref": "#/definitions/Digest" + }, + "userName": { + "type": "string" + }, + "groupName": { + "type": "string" + }, + "flags": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "RpmdbMetadata": { + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "license", + "vendor", + "files" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "epoch": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string" + }, + "release": { + "type": "string" + }, + "sourceRpm": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "license": { + "type": "string" + }, + "vendor": { + "type": "string" + }, + "files": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/RpmdbFileRecord" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Schema": { + "required": [ + "version", + "url" + ], + "properties": { + "version": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "SearchResult": { + "required": [ + "classification", + "lineNumber", + "lineOffset", + "seekPosition", + "length" + ], + "properties": { + "classification": { + "type": "string" + }, + "lineNumber": { + "type": "integer" + }, + "lineOffset": { + "type": "integer" + }, + "seekPosition": { + "type": "integer" + }, + "length": { + "type": "integer" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Secrets": { + "required": [ + "location", + "secrets" + ], + "properties": { + "location": { + "$ref": "#/definitions/Coordinates" + }, + "secrets": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/SearchResult" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Source": { + "required": [ + "type", + "target" + ], + "properties": { + "type": { + "type": "string" + }, + "target": { + "additionalProperties": true + } + }, + "additionalProperties": true, + "type": "object" + } + } +} diff --git a/syft/pkg/cataloger/golang/binary_cataloger.go b/syft/pkg/cataloger/golang/binary_cataloger.go index 2ddf7d3b1..7d51f63ce 100644 --- a/syft/pkg/cataloger/golang/binary_cataloger.go +++ b/syft/pkg/cataloger/golang/binary_cataloger.go @@ -4,10 +4,12 @@ Package golang provides a concrete Cataloger implementation for go.mod files. package golang import ( + "bytes" "fmt" + "io" + "io/ioutil" "github.com/anchore/syft/internal" - "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/pkg" @@ -38,19 +40,51 @@ func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []arti } for _, location := range fileMatches { - r, err := resolver.FileContentsByLocation(location) + readerCloser, err := resolver.FileContentsByLocation(location) if err != nil { - return pkgs, nil, fmt.Errorf("failed to resolve file contents by location=%q: %w", location.RealPath, err) + log.Warnf("golang cataloger: opening file: %v", err) + continue } - goPkgs, err := parseGoBin(location, r, openExe) + reader, err := getUnionReader(readerCloser) if err != nil { - log.Warnf("could not parse possible go binary at %q: %+v", location.RealPath, err) + return nil, nil, err } - internal.CloseAndLogError(r, location.RealPath) - pkgs = append(pkgs, goPkgs...) + mods, archs := scanFile(reader, location.RealPath) + internal.CloseAndLogError(readerCloser, location.RealPath) + + for i, mod := range mods { + pkgs = append(pkgs, buildGoPkgInfo(location, mod, archs[i])...) + } } return pkgs, nil, nil } + +func getUnionReader(readerCloser io.ReadCloser) (unionReader, error) { + reader, ok := readerCloser.(unionReader) + if ok { + return reader, nil + } + log.Debugf("golang cataloger: unable to use stereoscope file, reading entire contents") + + b, err := ioutil.ReadAll(readerCloser) + if err != nil { + return nil, fmt.Errorf("unable to read contents from go binary: %w", err) + } + + bytesReader := bytes.NewReader(b) + + reader = struct { + io.ReadCloser + io.ReaderAt + io.Seeker + }{ + ReadCloser: io.NopCloser(bytesReader), + ReaderAt: bytesReader, + Seeker: bytesReader, + } + + return reader, nil +} diff --git a/syft/pkg/cataloger/golang/binary_cataloger_test.go b/syft/pkg/cataloger/golang/binary_cataloger_test.go new file mode 100644 index 000000000..41ad714df --- /dev/null +++ b/syft/pkg/cataloger/golang/binary_cataloger_test.go @@ -0,0 +1,30 @@ +package golang + +import ( + "io" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_getUnionReader_notUnionReader(t *testing.T) { + expectedContents := "this is a test" + reader := io.NopCloser(strings.NewReader(expectedContents)) + + // make certain that the test fixture does not implement the union reader + _, ok := reader.(unionReader) + require.False(t, ok) + + actual, err := getUnionReader(reader) + require.NoError(t, err) + + _, ok = actual.(unionReader) + require.True(t, ok) + + b, err := io.ReadAll(actual) + require.NoError(t, err) + + assert.Equal(t, expectedContents, string(b)) +} diff --git a/syft/pkg/cataloger/golang/exe.go b/syft/pkg/cataloger/golang/exe.go deleted file mode 100644 index 838d91f01..000000000 --- a/syft/pkg/cataloger/golang/exe.go +++ /dev/null @@ -1,354 +0,0 @@ -// This code was copied from the Go std library. -// https://github.com/golang/go/blob/master/src/cmd/go/internal/version/exe.go -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//nolint -package golang - -import ( - "bytes" - "debug/elf" - "debug/macho" - "debug/pe" - "fmt" - "io" - "strings" -) - -// An exe is a generic interface to an OS executable (ELF, Mach-O, PE, XCOFF). -type exe interface { - // Close closes the underlying file. - Close() error - - // ReadData reads and returns up to size byte starting at virtual address addr. - ReadData(addr, size uint64) ([]byte, error) - - // ArchName returns a string that represents the CPU architecture of the executable. - ArchName() string - - // DataStart returns the writable data segment start address. - DataStart() uint64 -} - -// openExe opens file and returns it as an exe. -// we changed this signature from accpeting a string -// to a ReadCloser so we could adapt the code to the -// stereoscope api. We removed the file open methods. -func openExe(file io.ReadCloser) ([]exe, error) { - /* - f, err := os.Open(file) - if err != nil { - return nil, err - } - data := make([]byte, 16) - if _, err := io.ReadFull(f, data); err != nil { - return nil, err - } - f.Seek(0, 0) - */ - data, err := io.ReadAll(file) - if err != nil { - return nil, err - } - - r := bytes.NewReader(data) - f := io.NewSectionReader(r, 0, int64(len(data))) - if bytes.HasPrefix(data, []byte("\x7FELF")) { - e, err := elf.NewFile(f) - if err != nil { - return nil, err - } - - return []exe{&elfExe{file, e}}, nil - } - - if bytes.HasPrefix(data, []byte("MZ")) { - e, err := pe.NewFile(f) - if err != nil { - return nil, err - } - return []exe{&peExe{file, e}}, nil - } - - if bytes.HasPrefix(data, []byte("\xFE\xED\xFA")) || bytes.HasPrefix(data[1:], []byte("\xFA\xED\xFE")) { - e, err := macho.NewFile(f) - if err != nil { - return nil, err - } - return []exe{&machoExe{file, e}}, nil - } - - // adding macho multi-architecture support (both for 64bit and 32 bit)... this case is not in the stdlib yet - if bytes.HasPrefix(data, []byte("\xCA\xFE\xBA\xBE")) || bytes.HasPrefix(data, []byte("\xCA\xFE\xBA\xBF")) { - fatExe, err := macho.NewFatFile(f) - if err != nil { - return nil, err - } - var exes []exe - for _, arch := range fatExe.Arches { - exes = append(exes, &machoExe{file, arch.File}) - } - return exes, nil - } - - return nil, fmt.Errorf("unrecognized executable format") -} - -// elfExe is the ELF implementation of the exe interface. -// updated os to be io.ReadCloser to interopt with stereoscope -type elfExe struct { - os io.ReadCloser - f *elf.File -} - -func (x *elfExe) Close() error { - return x.os.Close() -} - -func (x *elfExe) ArchName() string { - return cleanElfArch(x.f.Machine) -} - -func cleanElfArch(machine elf.Machine) string { - return strings.TrimPrefix(strings.ToLower(machine.String()), "em_") -} - -func (x *elfExe) ReadData(addr, size uint64) ([]byte, error) { - for _, prog := range x.f.Progs { - if prog.Vaddr <= addr && addr <= prog.Vaddr+prog.Filesz-1 { - n := prog.Vaddr + prog.Filesz - addr - if n > size { - n = size - } - data := make([]byte, n) - _, err := prog.ReadAt(data, int64(addr-prog.Vaddr)) - if err != nil { - return nil, err - } - return data, nil - } - } - return nil, fmt.Errorf("address not mapped") -} - -func (x *elfExe) DataStart() uint64 { - for _, s := range x.f.Sections { - if s.Name == ".go.buildinfo" { - return s.Addr - } - } - for _, p := range x.f.Progs { - if p.Type == elf.PT_LOAD && p.Flags&(elf.PF_X|elf.PF_W) == elf.PF_W { - return p.Vaddr - } - } - return 0 -} - -// peExe is the PE (Windows Portable Executable) implementation of the exe interface. -type peExe struct { - os io.ReadCloser - f *pe.File -} - -func (x *peExe) Close() error { - return x.os.Close() -} - -func (x *peExe) ArchName() string { - // from: debug/pe/pe.go - switch x.f.Machine { - case pe.IMAGE_FILE_MACHINE_AM33: - return "amd33" - case pe.IMAGE_FILE_MACHINE_AMD64: - return "amd64" - case pe.IMAGE_FILE_MACHINE_ARM: - return "arm" - case pe.IMAGE_FILE_MACHINE_ARMNT: - return "armnt" - case pe.IMAGE_FILE_MACHINE_ARM64: - return "arm64" - case pe.IMAGE_FILE_MACHINE_EBC: - return "ebc" - case pe.IMAGE_FILE_MACHINE_I386: - return "i386" - case pe.IMAGE_FILE_MACHINE_IA64: - return "ia64" - case pe.IMAGE_FILE_MACHINE_M32R: - return "m32r" - case pe.IMAGE_FILE_MACHINE_MIPS16: - return "mips16" - case pe.IMAGE_FILE_MACHINE_MIPSFPU: - return "mipsfpu" - case pe.IMAGE_FILE_MACHINE_MIPSFPU16: - return "mipsfpu16" - case pe.IMAGE_FILE_MACHINE_POWERPC: - return "ppc" - case pe.IMAGE_FILE_MACHINE_POWERPCFP: - return "ppcfp" - case pe.IMAGE_FILE_MACHINE_R4000: - return "r4000" - case pe.IMAGE_FILE_MACHINE_SH3: - return "sh3" - case pe.IMAGE_FILE_MACHINE_SH3DSP: - return "sh3dsp" - case pe.IMAGE_FILE_MACHINE_SH4: - return "sh4" - case pe.IMAGE_FILE_MACHINE_SH5: - return "sh5" - case pe.IMAGE_FILE_MACHINE_THUMB: - return "thumb" - case pe.IMAGE_FILE_MACHINE_WCEMIPSV2: - return "wcemipsv2" - default: - return fmt.Sprintf("unknown-pe-machine-%d", x.f.Machine) - } -} - -func (x *peExe) imageBase() uint64 { - switch oh := x.f.OptionalHeader.(type) { - case *pe.OptionalHeader32: - return uint64(oh.ImageBase) - case *pe.OptionalHeader64: - return oh.ImageBase - } - return 0 -} - -func (x *peExe) ReadData(addr, size uint64) ([]byte, error) { - addr -= x.imageBase() - for _, sect := range x.f.Sections { - if uint64(sect.VirtualAddress) <= addr && addr <= uint64(sect.VirtualAddress+sect.Size-1) { - n := uint64(sect.VirtualAddress+sect.Size) - addr - if n > size { - n = size - } - data := make([]byte, n) - _, err := sect.ReadAt(data, int64(addr-uint64(sect.VirtualAddress))) - if err != nil { - return nil, err - } - return data, nil - } - } - return nil, fmt.Errorf("address not mapped") -} - -func (x *peExe) DataStart() uint64 { - // Assume data is first writable section. - const ( - IMAGE_SCN_CNT_CODE = 0x00000020 - IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040 - IMAGE_SCN_CNT_UNINITIALIZED_DATA = 0x00000080 - IMAGE_SCN_MEM_EXECUTE = 0x20000000 - IMAGE_SCN_MEM_READ = 0x40000000 - IMAGE_SCN_MEM_WRITE = 0x80000000 - IMAGE_SCN_MEM_DISCARDABLE = 0x2000000 - IMAGE_SCN_LNK_NRELOC_OVFL = 0x1000000 - IMAGE_SCN_ALIGN_32BYTES = 0x600000 - ) - for _, sect := range x.f.Sections { - if sect.VirtualAddress != 0 && sect.Size != 0 && - sect.Characteristics&^IMAGE_SCN_ALIGN_32BYTES == IMAGE_SCN_CNT_INITIALIZED_DATA|IMAGE_SCN_MEM_READ|IMAGE_SCN_MEM_WRITE { - return uint64(sect.VirtualAddress) + x.imageBase() - } - } - return 0 -} - -// machoExe is the Mach-O (Apple macOS/iOS) implementation of the exe interface. -type machoExe struct { - os io.ReadCloser - f *macho.File -} - -func (x *machoExe) Close() error { - return x.os.Close() -} - -func (x *machoExe) ArchName() string { - return cleanMachoArch(x.f.Cpu) -} - -func cleanMachoArch(cpu macho.Cpu) string { - return strings.TrimPrefix(strings.ToLower(cpu.String()), "cpu") -} - -func (x *machoExe) ReadData(addr, size uint64) ([]byte, error) { - for _, load := range x.f.Loads { - seg, ok := load.(*macho.Segment) - if !ok { - continue - } - if seg.Addr <= addr && addr <= seg.Addr+seg.Filesz-1 { - if seg.Name == "__PAGEZERO" { - continue - } - n := seg.Addr + seg.Filesz - addr - if n > size { - n = size - } - data := make([]byte, n) - _, err := seg.ReadAt(data, int64(addr-seg.Addr)) - if err != nil { - return nil, err - } - return data, nil - } - } - return nil, fmt.Errorf("address not mapped") -} - -func (x *machoExe) DataStart() uint64 { - // Look for section named "__go_buildinfo". - for _, sec := range x.f.Sections { - if sec.Name == "__go_buildinfo" { - return sec.Addr - } - } - // Try the first non-empty writable segment. - const RW = 3 - for _, load := range x.f.Loads { - seg, ok := load.(*macho.Segment) - if ok && seg.Addr != 0 && seg.Filesz != 0 && seg.Prot == RW && seg.Maxprot == RW { - return seg.Addr - } - } - return 0 -} - -/* -// xcoffExe is the XCOFF (AIX eXtended COFF) implementation of the exe interface. -type xcoffExe struct { - os *os.File - f *xcoff.File -} - -func (x *xcoffExe) Close() error { - return x.os.Close() -} - -func (x *xcoffExe) ReadData(addr, size uint64) ([]byte, error) { - for _, sect := range x.f.Sections { - if uint64(sect.VirtualAddress) <= addr && addr <= uint64(sect.VirtualAddress+sect.Size-1) { - n := uint64(sect.VirtualAddress+sect.Size) - addr - if n > size { - n = size - } - data := make([]byte, n) - _, err := sect.ReadAt(data, int64(addr-uint64(sect.VirtualAddress))) - if err != nil { - return nil, err - } - return data, nil - } - } - return nil, fmt.Errorf("address not mapped") -} - -func (x *xcoffExe) DataStart() uint64 { - return x.f.SectionByType(xcoff.STYP_DATA).VirtualAddress -} -*/ diff --git a/syft/pkg/cataloger/golang/exe_test.go b/syft/pkg/cataloger/golang/exe_test.go deleted file mode 100644 index 5a5a6d39c..000000000 --- a/syft/pkg/cataloger/golang/exe_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package golang - -import ( - "debug/elf" - "debug/macho" - "testing" - - "github.com/stretchr/testify/assert" -) - -func Test_cleanElfArch(t *testing.T) { - tests := []struct { - machine elf.Machine - want string - }{ - { - machine: elf.EM_X86_64, - want: "x86_64", - }, - } - for _, test := range tests { - t.Run(test.machine.String(), func(t *testing.T) { - assert.Equalf(t, test.want, cleanElfArch(test.machine), "cleanElfArch(%v)", test.machine) - }) - } -} - -func Test_cleanMachoArch(t *testing.T) { - tests := []struct { - cpu macho.Cpu - want string - }{ - { - cpu: macho.CpuAmd64, - want: "amd64", - }, - } - for _, test := range tests { - t.Run(test.cpu.String(), func(t *testing.T) { - assert.Equalf(t, test.want, cleanMachoArch(test.cpu), "cleanMachoArch(%v)", test.cpu) - }) - } -} diff --git a/syft/pkg/cataloger/golang/internal/README.md b/syft/pkg/cataloger/golang/internal/README.md new file mode 100644 index 000000000..58cdf17ce --- /dev/null +++ b/syft/pkg/cataloger/golang/internal/README.md @@ -0,0 +1,4 @@ +xcoff +----- + +The code in this package comes from: https://github.com/golang/go/tree/master/src/internal/xcoff -- it was copied over to add support for [xcoff](https://en.wikipedia.org/wiki/XCOFF) binaries. Golang keeps this package as internal, forbidding its external use. diff --git a/syft/pkg/cataloger/golang/internal/xcoff/file.go b/syft/pkg/cataloger/golang/internal/xcoff/file.go new file mode 100644 index 000000000..76a39a34f --- /dev/null +++ b/syft/pkg/cataloger/golang/internal/xcoff/file.go @@ -0,0 +1,688 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package xcoff implements access to XCOFF (Extended Common Object File Format) files. + +//nolint //this is an internal golang lib +package xcoff + +import ( + "encoding/binary" + "fmt" + "io" + "os" + "strings" +) + +// SectionHeader holds information about an XCOFF section header. +type SectionHeader struct { + Name string + VirtualAddress uint64 + Size uint64 + Type uint32 + Relptr uint64 + Nreloc uint32 +} + +type Section struct { + SectionHeader + Relocs []Reloc + io.ReaderAt + sr *io.SectionReader +} + +// AuxiliaryCSect holds information about an XCOFF symbol in an AUX_CSECT entry. +type AuxiliaryCSect struct { + Length int64 + StorageMappingClass int + SymbolType int +} + +// AuxiliaryFcn holds information about an XCOFF symbol in an AUX_FCN entry. +type AuxiliaryFcn struct { + Size int64 +} + +type Symbol struct { + Name string + Value uint64 + SectionNumber int + StorageClass int + AuxFcn AuxiliaryFcn + AuxCSect AuxiliaryCSect +} + +type Reloc struct { + VirtualAddress uint64 + Symbol *Symbol + Signed bool + InstructionFixed bool + Length uint8 + Type uint8 +} + +// ImportedSymbol holds information about an imported XCOFF symbol. +type ImportedSymbol struct { + Name string + Library string +} + +// FileHeader holds information about an XCOFF file header. +type FileHeader struct { + TargetMachine uint16 +} + +// A File represents an open XCOFF file. +type File struct { + FileHeader + Sections []*Section + Symbols []*Symbol + StringTable []byte + LibraryPaths []string + + closer io.Closer +} + +// Open opens the named file using os.Open and prepares it for use as an XCOFF binary. +func Open(name string) (*File, error) { + f, err := os.Open(name) + if err != nil { + return nil, err + } + ff, err := NewFile(f) + if err != nil { + f.Close() + return nil, err + } + ff.closer = f + return ff, nil +} + +// Close closes the File. +// If the File was created using NewFile directly instead of Open, +// Close has no effect. +func (f *File) Close() error { + var err error + if f.closer != nil { + err = f.closer.Close() + f.closer = nil + } + return err +} + +// Section returns the first section with the given name, or nil if no such +// section exists. +// Xcoff have section's name limited to 8 bytes. Some sections like .gosymtab +// can be trunked but this method will still find them. +func (f *File) Section(name string) *Section { + for _, s := range f.Sections { + if s.Name == name || (len(name) > 8 && s.Name == name[:8]) { + return s + } + } + return nil +} + +// SectionByType returns the first section in f with the +// given type, or nil if there is no such section. +func (f *File) SectionByType(typ uint32) *Section { + for _, s := range f.Sections { + if s.Type == typ { + return s + } + } + return nil +} + +// cstring converts ASCII byte sequence b to string. +// It stops once it finds 0 or reaches end of b. +func cstring(b []byte) string { + var i int + for i = 0; i < len(b) && b[i] != 0; i++ { + } + return string(b[:i]) +} + +// getString extracts a string from an XCOFF string table. +func getString(st []byte, offset uint32) (string, bool) { + if offset < 4 || int(offset) >= len(st) { + return "", false + } + return cstring(st[offset:]), true +} + +// NewFile creates a new File for accessing an XCOFF binary in an underlying reader. +func NewFile(r io.ReaderAt) (*File, error) { + sr := io.NewSectionReader(r, 0, 1<<63-1) + // Read XCOFF target machine + var magic uint16 + if err := binary.Read(sr, binary.BigEndian, &magic); err != nil { + return nil, err + } + if magic != U802TOCMAGIC && magic != U64_TOCMAGIC { + return nil, fmt.Errorf("unrecognised XCOFF magic: 0x%x", magic) + } + + f := new(File) + f.TargetMachine = magic + + // Read XCOFF file header + if _, err := sr.Seek(0, os.SEEK_SET); err != nil { + return nil, err + } + var nscns uint16 + var symptr uint64 + var nsyms int32 + var opthdr uint16 + var hdrsz int + switch f.TargetMachine { + case U802TOCMAGIC: + fhdr := new(FileHeader32) + if err := binary.Read(sr, binary.BigEndian, fhdr); err != nil { + return nil, err + } + nscns = fhdr.Fnscns + symptr = uint64(fhdr.Fsymptr) + nsyms = fhdr.Fnsyms + opthdr = fhdr.Fopthdr + hdrsz = FILHSZ_32 + case U64_TOCMAGIC: + fhdr := new(FileHeader64) + if err := binary.Read(sr, binary.BigEndian, fhdr); err != nil { + return nil, err + } + nscns = fhdr.Fnscns + symptr = fhdr.Fsymptr + nsyms = fhdr.Fnsyms + opthdr = fhdr.Fopthdr + hdrsz = FILHSZ_64 + } + + if symptr == 0 || nsyms <= 0 { + return nil, fmt.Errorf("no symbol table") + } + + // Read string table (located right after symbol table). + offset := symptr + uint64(nsyms)*SYMESZ + if _, err := sr.Seek(int64(offset), os.SEEK_SET); err != nil { + return nil, err + } + // The first 4 bytes contain the length (in bytes). + var l uint32 + if err := binary.Read(sr, binary.BigEndian, &l); err != nil { + return nil, err + } + if l > 4 { + if _, err := sr.Seek(int64(offset), os.SEEK_SET); err != nil { + return nil, err + } + f.StringTable = make([]byte, l) + if _, err := io.ReadFull(sr, f.StringTable); err != nil { + return nil, err + } + } + + // Read section headers + if _, err := sr.Seek(int64(hdrsz)+int64(opthdr), os.SEEK_SET); err != nil { + return nil, err + } + f.Sections = make([]*Section, nscns) + for i := 0; i < int(nscns); i++ { + var scnptr uint64 + s := new(Section) + switch f.TargetMachine { + case U802TOCMAGIC: + shdr := new(SectionHeader32) + if err := binary.Read(sr, binary.BigEndian, shdr); err != nil { + return nil, err + } + s.Name = cstring(shdr.Sname[:]) + s.VirtualAddress = uint64(shdr.Svaddr) + s.Size = uint64(shdr.Ssize) + scnptr = uint64(shdr.Sscnptr) + s.Type = shdr.Sflags + s.Relptr = uint64(shdr.Srelptr) + s.Nreloc = uint32(shdr.Snreloc) + case U64_TOCMAGIC: + shdr := new(SectionHeader64) + if err := binary.Read(sr, binary.BigEndian, shdr); err != nil { + return nil, err + } + s.Name = cstring(shdr.Sname[:]) + s.VirtualAddress = shdr.Svaddr + s.Size = shdr.Ssize + scnptr = shdr.Sscnptr + s.Type = shdr.Sflags + s.Relptr = shdr.Srelptr + s.Nreloc = shdr.Snreloc + } + r2 := r + if scnptr == 0 { // .bss must have all 0s + r2 = zeroReaderAt{} + } + s.sr = io.NewSectionReader(r2, int64(scnptr), int64(s.Size)) + s.ReaderAt = s.sr + f.Sections[i] = s + } + + // Symbol map needed by relocation + var idxToSym = make(map[int]*Symbol) + + // Read symbol table + if _, err := sr.Seek(int64(symptr), os.SEEK_SET); err != nil { + return nil, err + } + f.Symbols = make([]*Symbol, 0) + for i := 0; i < int(nsyms); i++ { + var numaux int + var ok, needAuxFcn bool + sym := new(Symbol) + switch f.TargetMachine { + case U802TOCMAGIC: + se := new(SymEnt32) + if err := binary.Read(sr, binary.BigEndian, se); err != nil { + return nil, err + } + numaux = int(se.Nnumaux) + sym.SectionNumber = int(se.Nscnum) + sym.StorageClass = int(se.Nsclass) + sym.Value = uint64(se.Nvalue) + needAuxFcn = se.Ntype&SYM_TYPE_FUNC != 0 && numaux > 1 + zeroes := binary.BigEndian.Uint32(se.Nname[:4]) + if zeroes != 0 { + sym.Name = cstring(se.Nname[:]) + } else { + offset := binary.BigEndian.Uint32(se.Nname[4:]) + sym.Name, ok = getString(f.StringTable, offset) + if !ok { + goto skip + } + } + case U64_TOCMAGIC: + se := new(SymEnt64) + if err := binary.Read(sr, binary.BigEndian, se); err != nil { + return nil, err + } + numaux = int(se.Nnumaux) + sym.SectionNumber = int(se.Nscnum) + sym.StorageClass = int(se.Nsclass) + sym.Value = se.Nvalue + needAuxFcn = se.Ntype&SYM_TYPE_FUNC != 0 && numaux > 1 + sym.Name, ok = getString(f.StringTable, se.Noffset) + if !ok { + goto skip + } + } + if sym.StorageClass != C_EXT && sym.StorageClass != C_WEAKEXT && sym.StorageClass != C_HIDEXT { + goto skip + } + // Must have at least one csect auxiliary entry. + if numaux < 1 || i+numaux >= int(nsyms) { + goto skip + } + + if sym.SectionNumber > int(nscns) { + goto skip + } + if sym.SectionNumber == 0 { + sym.Value = 0 + } else { + sym.Value -= f.Sections[sym.SectionNumber-1].VirtualAddress + } + + idxToSym[i] = sym + + // If this symbol is a function, it must retrieve its size from + // its AUX_FCN entry. + // It can happen that a function symbol doesn't have any AUX_FCN. + // In this case, needAuxFcn is false and their size will be set to 0. + if needAuxFcn { + switch f.TargetMachine { + case U802TOCMAGIC: + aux := new(AuxFcn32) + if err := binary.Read(sr, binary.BigEndian, aux); err != nil { + return nil, err + } + sym.AuxFcn.Size = int64(aux.Xfsize) + case U64_TOCMAGIC: + aux := new(AuxFcn64) + if err := binary.Read(sr, binary.BigEndian, aux); err != nil { + return nil, err + } + sym.AuxFcn.Size = int64(aux.Xfsize) + } + } + + // Read csect auxiliary entry (by convention, it is the last). + if !needAuxFcn { + if _, err := sr.Seek(int64(numaux-1)*SYMESZ, os.SEEK_CUR); err != nil { + return nil, err + } + } + i += numaux + numaux = 0 + switch f.TargetMachine { + case U802TOCMAGIC: + aux := new(AuxCSect32) + if err := binary.Read(sr, binary.BigEndian, aux); err != nil { + return nil, err + } + sym.AuxCSect.SymbolType = int(aux.Xsmtyp & 0x7) + sym.AuxCSect.StorageMappingClass = int(aux.Xsmclas) + sym.AuxCSect.Length = int64(aux.Xscnlen) + case U64_TOCMAGIC: + aux := new(AuxCSect64) + if err := binary.Read(sr, binary.BigEndian, aux); err != nil { + return nil, err + } + sym.AuxCSect.SymbolType = int(aux.Xsmtyp & 0x7) + sym.AuxCSect.StorageMappingClass = int(aux.Xsmclas) + sym.AuxCSect.Length = int64(aux.Xscnlenhi)<<32 | int64(aux.Xscnlenlo) + } + f.Symbols = append(f.Symbols, sym) + skip: + i += numaux // Skip auxiliary entries + if _, err := sr.Seek(int64(numaux)*SYMESZ, os.SEEK_CUR); err != nil { + return nil, err + } + } + + // Read relocations + // Only for .data or .text section + for _, sect := range f.Sections { + if sect.Type != STYP_TEXT && sect.Type != STYP_DATA { + continue + } + sect.Relocs = make([]Reloc, sect.Nreloc) + if sect.Relptr == 0 { + continue + } + if _, err := sr.Seek(int64(sect.Relptr), os.SEEK_SET); err != nil { + return nil, err + } + for i := uint32(0); i < sect.Nreloc; i++ { + switch f.TargetMachine { + case U802TOCMAGIC: + rel := new(Reloc32) + if err := binary.Read(sr, binary.BigEndian, rel); err != nil { + return nil, err + } + sect.Relocs[i].VirtualAddress = uint64(rel.Rvaddr) + sect.Relocs[i].Symbol = idxToSym[int(rel.Rsymndx)] + sect.Relocs[i].Type = rel.Rtype + sect.Relocs[i].Length = rel.Rsize&0x3F + 1 + + if rel.Rsize&0x80 != 0 { + sect.Relocs[i].Signed = true + } + if rel.Rsize&0x40 != 0 { + sect.Relocs[i].InstructionFixed = true + } + + case U64_TOCMAGIC: + rel := new(Reloc64) + if err := binary.Read(sr, binary.BigEndian, rel); err != nil { + return nil, err + } + sect.Relocs[i].VirtualAddress = rel.Rvaddr + sect.Relocs[i].Symbol = idxToSym[int(rel.Rsymndx)] + sect.Relocs[i].Type = rel.Rtype + sect.Relocs[i].Length = rel.Rsize&0x3F + 1 + if rel.Rsize&0x80 != 0 { + sect.Relocs[i].Signed = true + } + if rel.Rsize&0x40 != 0 { + sect.Relocs[i].InstructionFixed = true + } + } + } + } + + return f, nil +} + +// zeroReaderAt is ReaderAt that reads 0s. +type zeroReaderAt struct{} + +// ReadAt writes len(p) 0s into p. +func (w zeroReaderAt) ReadAt(p []byte, off int64) (n int, err error) { + for i := range p { + p[i] = 0 + } + return len(p), nil +} + +// Data reads and returns the contents of the XCOFF section s. +func (s *Section) Data() ([]byte, error) { + dat := make([]byte, s.sr.Size()) + n, err := s.sr.ReadAt(dat, 0) + if n == len(dat) { + err = nil + } + return dat[:n], err +} + +// CSect reads and returns the contents of a csect. +// func (f *File) CSect(name string) []byte { +// for _, sym := range f.Symbols { +// if sym.Name == name && sym.AuxCSect.SymbolType == XTY_SD { +// if i := sym.SectionNumber - 1; 0 <= i && i < len(f.Sections) { +// s := f.Sections[i] +// if sym.Value+uint64(sym.AuxCSect.Length) <= s.Size { +// dat := make([]byte, sym.AuxCSect.Length) +// _, err := s.sr.ReadAt(dat, int64(sym.Value)) +// if err != nil { +// return nil +// } +// return dat +// } +// } +// break +// } +// } +// return nil +// } + +// func (f *File) DWARF() (*dwarf.Data, error) { +// // There are many other DWARF sections, but these +// // are the ones the debug/dwarf package uses. +// // Don't bother loading others. +// var subtypes = [...]uint32{SSUBTYP_DWABREV, SSUBTYP_DWINFO, SSUBTYP_DWLINE, SSUBTYP_DWRNGES, SSUBTYP_DWSTR} +// var dat [len(subtypes)][]byte +// for i, subtype := range subtypes { +// s := f.SectionByType(STYP_DWARF | subtype) +// if s != nil { +// b, err := s.Data() +// if err != nil && uint64(len(b)) < s.Size { +// return nil, err +// } +// dat[i] = b +// } +// } + +// abbrev, info, line, ranges, str := dat[0], dat[1], dat[2], dat[3], dat[4] +// return dwarf.New(abbrev, nil, nil, info, line, nil, ranges, str) +// } + +// readImportID returns the import file IDs stored inside the .loader section. +// Library name pattern is either path/base/member or base/member +func (f *File) readImportIDs(s *Section) ([]string, error) { + // Read loader header + if _, err := s.sr.Seek(0, os.SEEK_SET); err != nil { + return nil, err + } + var istlen uint32 + var nimpid int32 + var impoff uint64 + switch f.TargetMachine { + case U802TOCMAGIC: + lhdr := new(LoaderHeader32) + if err := binary.Read(s.sr, binary.BigEndian, lhdr); err != nil { + return nil, err + } + istlen = lhdr.Listlen + nimpid = lhdr.Lnimpid + impoff = uint64(lhdr.Limpoff) + case U64_TOCMAGIC: + lhdr := new(LoaderHeader64) + if err := binary.Read(s.sr, binary.BigEndian, lhdr); err != nil { + return nil, err + } + istlen = lhdr.Listlen + nimpid = lhdr.Lnimpid + impoff = lhdr.Limpoff + } + + // Read loader import file ID table + if _, err := s.sr.Seek(int64(impoff), os.SEEK_SET); err != nil { + return nil, err + } + table := make([]byte, istlen) + if _, err := io.ReadFull(s.sr, table); err != nil { + return nil, err + } + + offset := 0 + // First import file ID is the default LIBPATH value + libpath := cstring(table[offset:]) + f.LibraryPaths = strings.Split(libpath, ":") + offset += len(libpath) + 3 // 3 null bytes + all := make([]string, 0) + for i := 1; i < int(nimpid); i++ { + impidpath := cstring(table[offset:]) + offset += len(impidpath) + 1 + impidbase := cstring(table[offset:]) + offset += len(impidbase) + 1 + impidmem := cstring(table[offset:]) + offset += len(impidmem) + 1 + var path string + if len(impidpath) > 0 { + path = impidpath + "/" + impidbase + "/" + impidmem + } else { + path = impidbase + "/" + impidmem + } + all = append(all, path) + } + + return all, nil +} + +// ImportedSymbols returns the names of all symbols +// referred to by the binary f that are expected to be +// satisfied by other libraries at dynamic load time. +// It does not return weak symbols. +func (f *File) ImportedSymbols() ([]ImportedSymbol, error) { + s := f.SectionByType(STYP_LOADER) + if s == nil { + return nil, nil + } + // Read loader header + if _, err := s.sr.Seek(0, os.SEEK_SET); err != nil { + return nil, err + } + var stlen uint32 + var stoff uint64 + var nsyms int32 + var symoff uint64 + switch f.TargetMachine { + case U802TOCMAGIC: + lhdr := new(LoaderHeader32) + if err := binary.Read(s.sr, binary.BigEndian, lhdr); err != nil { + return nil, err + } + stlen = lhdr.Lstlen + stoff = uint64(lhdr.Lstoff) + nsyms = lhdr.Lnsyms + symoff = LDHDRSZ_32 + case U64_TOCMAGIC: + lhdr := new(LoaderHeader64) + if err := binary.Read(s.sr, binary.BigEndian, lhdr); err != nil { + return nil, err + } + stlen = lhdr.Lstlen + stoff = lhdr.Lstoff + nsyms = lhdr.Lnsyms + symoff = lhdr.Lsymoff + } + + // Read loader section string table + if _, err := s.sr.Seek(int64(stoff), os.SEEK_SET); err != nil { + return nil, err + } + st := make([]byte, stlen) + if _, err := io.ReadFull(s.sr, st); err != nil { + return nil, err + } + + // Read imported libraries + libs, err := f.readImportIDs(s) + if err != nil { + return nil, err + } + + // Read loader symbol table + if _, err := s.sr.Seek(int64(symoff), os.SEEK_SET); err != nil { + return nil, err + } + all := make([]ImportedSymbol, 0) + for i := 0; i < int(nsyms); i++ { + var name string + var ifile int32 + var ok bool + switch f.TargetMachine { + case U802TOCMAGIC: + ldsym := new(LoaderSymbol32) + if err := binary.Read(s.sr, binary.BigEndian, ldsym); err != nil { + return nil, err + } + if ldsym.Lsmtype&0x40 == 0 { + continue // Imported symbols only + } + zeroes := binary.BigEndian.Uint32(ldsym.Lname[:4]) + if zeroes != 0 { + name = cstring(ldsym.Lname[:]) + } else { + offset := binary.BigEndian.Uint32(ldsym.Lname[4:]) + name, ok = getString(st, offset) + if !ok { + continue + } + } + ifile = ldsym.Lifile + case U64_TOCMAGIC: + ldsym := new(LoaderSymbol64) + if err := binary.Read(s.sr, binary.BigEndian, ldsym); err != nil { + return nil, err + } + if ldsym.Lsmtype&0x40 == 0 { + continue // Imported symbols only + } + name, ok = getString(st, ldsym.Loffset) + if !ok { + continue + } + ifile = ldsym.Lifile + } + var sym ImportedSymbol + sym.Name = name + if ifile >= 1 && int(ifile) <= len(libs) { + sym.Library = libs[ifile-1] + } + all = append(all, sym) + } + + return all, nil +} + +// ImportedLibraries returns the names of all libraries +// referred to by the binary f that are expected to be +// linked with the binary at dynamic link time. +func (f *File) ImportedLibraries() ([]string, error) { + s := f.SectionByType(STYP_LOADER) + if s == nil { + return nil, nil + } + all, err := f.readImportIDs(s) + return all, err +} diff --git a/syft/pkg/cataloger/golang/internal/xcoff/file_test.go b/syft/pkg/cataloger/golang/internal/xcoff/file_test.go new file mode 100644 index 000000000..a6722e945 --- /dev/null +++ b/syft/pkg/cataloger/golang/internal/xcoff/file_test.go @@ -0,0 +1,102 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xcoff + +import ( + "reflect" + "testing" +) + +type fileTest struct { + file string + hdr FileHeader + sections []*SectionHeader + needed []string +} + +var fileTests = []fileTest{ + { + "testdata/gcc-ppc32-aix-dwarf2-exec", + FileHeader{U802TOCMAGIC}, + []*SectionHeader{ + {".text", 0x10000290, 0x00000bbd, STYP_TEXT, 0x7ae6, 0x36}, + {".data", 0x20000e4d, 0x00000437, STYP_DATA, 0x7d02, 0x2b}, + {".bss", 0x20001284, 0x0000021c, STYP_BSS, 0, 0}, + {".loader", 0x00000000, 0x000004b3, STYP_LOADER, 0, 0}, + {".dwline", 0x00000000, 0x000000df, STYP_DWARF | SSUBTYP_DWLINE, 0x7eb0, 0x7}, + {".dwinfo", 0x00000000, 0x00000314, STYP_DWARF | SSUBTYP_DWINFO, 0x7ef6, 0xa}, + {".dwabrev", 0x00000000, 0x000000d6, STYP_DWARF | SSUBTYP_DWABREV, 0, 0}, + {".dwarnge", 0x00000000, 0x00000020, STYP_DWARF | SSUBTYP_DWARNGE, 0x7f5a, 0x2}, + {".dwloc", 0x00000000, 0x00000074, STYP_DWARF | SSUBTYP_DWLOC, 0, 0}, + {".debug", 0x00000000, 0x00005e4f, STYP_DEBUG, 0, 0}, + }, + []string{"libc.a/shr.o"}, + }, + { + "testdata/gcc-ppc64-aix-dwarf2-exec", + FileHeader{U64_TOCMAGIC}, + []*SectionHeader{ + {".text", 0x10000480, 0x00000afd, STYP_TEXT, 0x8322, 0x34}, + {".data", 0x20000f7d, 0x000002f3, STYP_DATA, 0x85fa, 0x25}, + {".bss", 0x20001270, 0x00000428, STYP_BSS, 0, 0}, + {".loader", 0x00000000, 0x00000535, STYP_LOADER, 0, 0}, + {".dwline", 0x00000000, 0x000000b4, STYP_DWARF | SSUBTYP_DWLINE, 0x8800, 0x4}, + {".dwinfo", 0x00000000, 0x0000036a, STYP_DWARF | SSUBTYP_DWINFO, 0x8838, 0x7}, + {".dwabrev", 0x00000000, 0x000000b5, STYP_DWARF | SSUBTYP_DWABREV, 0, 0}, + {".dwarnge", 0x00000000, 0x00000040, STYP_DWARF | SSUBTYP_DWARNGE, 0x889a, 0x2}, + {".dwloc", 0x00000000, 0x00000062, STYP_DWARF | SSUBTYP_DWLOC, 0, 0}, + {".debug", 0x00000000, 0x00006605, STYP_DEBUG, 0, 0}, + }, + []string{"libc.a/shr_64.o"}, + }, +} + +func TestOpen(t *testing.T) { + for i := range fileTests { + tt := &fileTests[i] + + f, err := Open(tt.file) + if err != nil { + t.Error(err) + continue + } + if !reflect.DeepEqual(f.FileHeader, tt.hdr) { + t.Errorf("open %s:\n\thave %#v\n\twant %#v\n", tt.file, f.FileHeader, tt.hdr) + continue + } + + for i, sh := range f.Sections { + if i >= len(tt.sections) { + break + } + have := &sh.SectionHeader + want := tt.sections[i] + if !reflect.DeepEqual(have, want) { + t.Errorf("open %s, section %d:\n\thave %#v\n\twant %#v\n", tt.file, i, have, want) + } + } + tn := len(tt.sections) + fn := len(f.Sections) + if tn != fn { + t.Errorf("open %s: len(Sections) = %d, want %d", tt.file, fn, tn) + } + tl := tt.needed + fl, err := f.ImportedLibraries() + if err != nil { + t.Error(err) + } + if !reflect.DeepEqual(tl, fl) { + t.Errorf("open %s: loader import = %v, want %v", tt.file, tl, fl) + } + } +} + +func TestOpenFailure(t *testing.T) { + filename := "file.go" // not an XCOFF object file + _, err := Open(filename) // don't crash + if err == nil { + t.Errorf("open %s: succeeded unexpectedly", filename) + } +} diff --git a/syft/pkg/cataloger/golang/internal/xcoff/testdata/gcc-ppc32-aix-dwarf2-exec b/syft/pkg/cataloger/golang/internal/xcoff/testdata/gcc-ppc32-aix-dwarf2-exec new file mode 100644 index 000000000..810e21a0d Binary files /dev/null and b/syft/pkg/cataloger/golang/internal/xcoff/testdata/gcc-ppc32-aix-dwarf2-exec differ diff --git a/syft/pkg/cataloger/golang/internal/xcoff/testdata/gcc-ppc64-aix-dwarf2-exec b/syft/pkg/cataloger/golang/internal/xcoff/testdata/gcc-ppc64-aix-dwarf2-exec new file mode 100644 index 000000000..707d01ebd Binary files /dev/null and b/syft/pkg/cataloger/golang/internal/xcoff/testdata/gcc-ppc64-aix-dwarf2-exec differ diff --git a/syft/pkg/cataloger/golang/internal/xcoff/xcoff.go b/syft/pkg/cataloger/golang/internal/xcoff/xcoff.go new file mode 100644 index 000000000..96cf01d25 --- /dev/null +++ b/syft/pkg/cataloger/golang/internal/xcoff/xcoff.go @@ -0,0 +1,373 @@ +// The code in this package comes from: +// https://github.com/golang/go/tree/master/src/internal/xcoff +// it was copied over to add support for xcoff binaries. +// Golang keeps this package as internal, forbidding its external use. + +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//nolint // this is an internal golang lib +package xcoff + +// File Header. +type FileHeader32 struct { + Fmagic uint16 // Target machine + Fnscns uint16 // Number of sections + Ftimedat int32 // Time and date of file creation + Fsymptr uint32 // Byte offset to symbol table start + Fnsyms int32 // Number of entries in symbol table + Fopthdr uint16 // Number of bytes in optional header + Fflags uint16 // Flags +} + +type FileHeader64 struct { + Fmagic uint16 // Target machine + Fnscns uint16 // Number of sections + Ftimedat int32 // Time and date of file creation + Fsymptr uint64 // Byte offset to symbol table start + Fopthdr uint16 // Number of bytes in optional header + Fflags uint16 // Flags + Fnsyms int32 // Number of entries in symbol table +} + +const ( + FILHSZ_32 = 20 + FILHSZ_64 = 24 +) +const ( + U802TOCMAGIC = 0737 // AIX 32-bit XCOFF + U64_TOCMAGIC = 0767 // AIX 64-bit XCOFF +) + +// Flags that describe the type of the object file. +const ( + F_RELFLG = 0x0001 + F_EXEC = 0x0002 + F_LNNO = 0x0004 + F_FDPR_PROF = 0x0010 + F_FDPR_OPTI = 0x0020 + F_DSA = 0x0040 + F_VARPG = 0x0100 + F_DYNLOAD = 0x1000 + F_SHROBJ = 0x2000 + F_LOADONLY = 0x4000 +) + +// Section Header. +type SectionHeader32 struct { + Sname [8]byte // Section name + Spaddr uint32 // Physical address + Svaddr uint32 // Virtual address + Ssize uint32 // Section size + Sscnptr uint32 // Offset in file to raw data for section + Srelptr uint32 // Offset in file to relocation entries for section + Slnnoptr uint32 // Offset in file to line number entries for section + Snreloc uint16 // Number of relocation entries + Snlnno uint16 // Number of line number entries + Sflags uint32 // Flags to define the section type +} + +type SectionHeader64 struct { + Sname [8]byte // Section name + Spaddr uint64 // Physical address + Svaddr uint64 // Virtual address + Ssize uint64 // Section size + Sscnptr uint64 // Offset in file to raw data for section + Srelptr uint64 // Offset in file to relocation entries for section + Slnnoptr uint64 // Offset in file to line number entries for section + Snreloc uint32 // Number of relocation entries + Snlnno uint32 // Number of line number entries + Sflags uint32 // Flags to define the section type + Spad uint32 // Needs to be 72 bytes long +} + +// Flags defining the section type. +const ( + STYP_DWARF = 0x0010 + STYP_TEXT = 0x0020 + STYP_DATA = 0x0040 + STYP_BSS = 0x0080 + STYP_EXCEPT = 0x0100 + STYP_INFO = 0x0200 + STYP_TDATA = 0x0400 + STYP_TBSS = 0x0800 + STYP_LOADER = 0x1000 + STYP_DEBUG = 0x2000 + STYP_TYPCHK = 0x4000 + STYP_OVRFLO = 0x8000 +) +const ( + SSUBTYP_DWINFO = 0x10000 // DWARF info section + SSUBTYP_DWLINE = 0x20000 // DWARF line-number section + SSUBTYP_DWPBNMS = 0x30000 // DWARF public names section + SSUBTYP_DWPBTYP = 0x40000 // DWARF public types section + SSUBTYP_DWARNGE = 0x50000 // DWARF aranges section + SSUBTYP_DWABREV = 0x60000 // DWARF abbreviation section + SSUBTYP_DWSTR = 0x70000 // DWARF strings section + SSUBTYP_DWRNGES = 0x80000 // DWARF ranges section + SSUBTYP_DWLOC = 0x90000 // DWARF location lists section + SSUBTYP_DWFRAME = 0xA0000 // DWARF frames section + SSUBTYP_DWMAC = 0xB0000 // DWARF macros section +) + +// Symbol Table Entry. +type SymEnt32 struct { + Nname [8]byte // Symbol name + Nvalue uint32 // Symbol value + Nscnum int16 // Section number of symbol + Ntype uint16 // Basic and derived type specification + Nsclass int8 // Storage class of symbol + Nnumaux int8 // Number of auxiliary entries +} + +type SymEnt64 struct { + Nvalue uint64 // Symbol value + Noffset uint32 // Offset of the name in string table or .debug section + Nscnum int16 // Section number of symbol + Ntype uint16 // Basic and derived type specification + Nsclass int8 // Storage class of symbol + Nnumaux int8 // Number of auxiliary entries +} + +const SYMESZ = 18 + +const ( + // Nscnum + N_DEBUG = -2 + N_ABS = -1 + N_UNDEF = 0 + + //Ntype + SYM_V_INTERNAL = 0x1000 + SYM_V_HIDDEN = 0x2000 + SYM_V_PROTECTED = 0x3000 + SYM_V_EXPORTED = 0x4000 + SYM_TYPE_FUNC = 0x0020 // is function +) + +// Storage Class. +const ( + C_NULL = 0 // Symbol table entry marked for deletion + C_EXT = 2 // External symbol + C_STAT = 3 // Static symbol + C_BLOCK = 100 // Beginning or end of inner block + C_FCN = 101 // Beginning or end of function + C_FILE = 103 // Source file name and compiler information + C_HIDEXT = 107 // Unnamed external symbol + C_BINCL = 108 // Beginning of include file + C_EINCL = 109 // End of include file + C_WEAKEXT = 111 // Weak external symbol + C_DWARF = 112 // DWARF symbol + C_GSYM = 128 // Global variable + C_LSYM = 129 // Automatic variable allocated on stack + C_PSYM = 130 // Argument to subroutine allocated on stack + C_RSYM = 131 // Register variable + C_RPSYM = 132 // Argument to function or procedure stored in register + C_STSYM = 133 // Statically allocated symbol + C_BCOMM = 135 // Beginning of common block + C_ECOML = 136 // Local member of common block + C_ECOMM = 137 // End of common block + C_DECL = 140 // Declaration of object + C_ENTRY = 141 // Alternate entry + C_FUN = 142 // Function or procedure + C_BSTAT = 143 // Beginning of static block + C_ESTAT = 144 // End of static block + C_GTLS = 145 // Global thread-local variable + C_STTLS = 146 // Static thread-local variable +) + +// File Auxiliary Entry +type AuxFile64 struct { + Xfname [8]byte // Name or offset inside string table + Xftype uint8 // Source file string type + Xauxtype uint8 // Type of auxiliary entry +} + +// Function Auxiliary Entry +type AuxFcn32 struct { + Xexptr uint32 // File offset to exception table entry + Xfsize uint32 // Size of function in bytes + Xlnnoptr uint32 // File pointer to line number + Xendndx uint32 // Symbol table index of next entry + Xpad uint16 // Unused +} +type AuxFcn64 struct { + Xlnnoptr uint64 // File pointer to line number + Xfsize uint32 // Size of function in bytes + Xendndx uint32 // Symbol table index of next entry + Xpad uint8 // Unused + Xauxtype uint8 // Type of auxiliary entry +} + +type AuxSect64 struct { + Xscnlen uint64 // section length + Xnreloc uint64 // Num RLDs + pad uint8 + Xauxtype uint8 // Type of auxiliary entry +} + +// csect Auxiliary Entry. +type AuxCSect32 struct { + Xscnlen int32 // Length or symbol table index + Xparmhash uint32 // Offset of parameter type-check string + Xsnhash uint16 // .typchk section number + Xsmtyp uint8 // Symbol alignment and type + Xsmclas uint8 // Storage-mapping class + Xstab uint32 // Reserved + Xsnstab uint16 // Reserved +} + +type AuxCSect64 struct { + Xscnlenlo uint32 // Lower 4 bytes of length or symbol table index + Xparmhash uint32 // Offset of parameter type-check string + Xsnhash uint16 // .typchk section number + Xsmtyp uint8 // Symbol alignment and type + Xsmclas uint8 // Storage-mapping class + Xscnlenhi int32 // Upper 4 bytes of length or symbol table index + Xpad uint8 // Unused + Xauxtype uint8 // Type of auxiliary entry +} + +// Auxiliary type +// const ( +// _AUX_EXCEPT = 255 +// _AUX_FCN = 254 +// _AUX_SYM = 253 +// _AUX_FILE = 252 +// _AUX_CSECT = 251 +// _AUX_SECT = 250 +// ) + +// Symbol type field. +const ( + XTY_ER = 0 // External reference + XTY_SD = 1 // Section definition + XTY_LD = 2 // Label definition + XTY_CM = 3 // Common csect definition +) + +// Defines for File auxiliary definitions: x_ftype field of x_file +const ( + XFT_FN = 0 // Source File Name + XFT_CT = 1 // Compile Time Stamp + XFT_CV = 2 // Compiler Version Number + XFT_CD = 128 // Compiler Defined Information +) + +// Storage-mapping class. +const ( + XMC_PR = 0 // Program code + XMC_RO = 1 // Read-only constant + XMC_DB = 2 // Debug dictionary table + XMC_TC = 3 // TOC entry + XMC_UA = 4 // Unclassified + XMC_RW = 5 // Read/Write data + XMC_GL = 6 // Global linkage + XMC_XO = 7 // Extended operation + XMC_SV = 8 // 32-bit supervisor call descriptor + XMC_BS = 9 // BSS class + XMC_DS = 10 // Function descriptor + XMC_UC = 11 // Unnamed FORTRAN common + XMC_TC0 = 15 // TOC anchor + XMC_TD = 16 // Scalar data entry in the TOC + XMC_SV64 = 17 // 64-bit supervisor call descriptor + XMC_SV3264 = 18 // Supervisor call descriptor for both 32-bit and 64-bit + XMC_TL = 20 // Read/Write thread-local data + XMC_UL = 21 // Read/Write thread-local data (.tbss) + XMC_TE = 22 // TOC entry +) + +// Loader Header. +type LoaderHeader32 struct { + Lversion int32 // Loader section version number + Lnsyms int32 // Number of symbol table entries + Lnreloc int32 // Number of relocation table entries + Listlen uint32 // Length of import file ID string table + Lnimpid int32 // Number of import file IDs + Limpoff uint32 // Offset to start of import file IDs + Lstlen uint32 // Length of string table + Lstoff uint32 // Offset to start of string table +} + +type LoaderHeader64 struct { + Lversion int32 // Loader section version number + Lnsyms int32 // Number of symbol table entries + Lnreloc int32 // Number of relocation table entries + Listlen uint32 // Length of import file ID string table + Lnimpid int32 // Number of import file IDs + Lstlen uint32 // Length of string table + Limpoff uint64 // Offset to start of import file IDs + Lstoff uint64 // Offset to start of string table + Lsymoff uint64 // Offset to start of symbol table + Lrldoff uint64 // Offset to start of relocation entries +} + +const ( + LDHDRSZ_32 = 32 + LDHDRSZ_64 = 56 +) + +// Loader Symbol. +type LoaderSymbol32 struct { + Lname [8]byte // Symbol name or byte offset into string table + Lvalue uint32 // Address field + Lscnum int16 // Section number containing symbol + Lsmtype int8 // Symbol type, export, import flags + Lsmclas int8 // Symbol storage class + Lifile int32 // Import file ID; ordinal of import file IDs + Lparm uint32 // Parameter type-check field +} + +type LoaderSymbol64 struct { + Lvalue uint64 // Address field + Loffset uint32 // Byte offset into string table of symbol name + Lscnum int16 // Section number containing symbol + Lsmtype int8 // Symbol type, export, import flags + Lsmclas int8 // Symbol storage class + Lifile int32 // Import file ID; ordinal of import file IDs + Lparm uint32 // Parameter type-check field +} + +type Reloc32 struct { + Rvaddr uint32 // (virtual) address of reference + Rsymndx uint32 // Index into symbol table + Rsize uint8 // Sign and reloc bit len + Rtype uint8 // Toc relocation type +} + +type Reloc64 struct { + Rvaddr uint64 // (virtual) address of reference + Rsymndx uint32 // Index into symbol table + Rsize uint8 // Sign and reloc bit len + Rtype uint8 // Toc relocation type +} + +const ( + R_POS = 0x00 // A(sym) Positive Relocation + R_NEG = 0x01 // -A(sym) Negative Relocation + R_REL = 0x02 // A(sym-*) Relative to self + R_TOC = 0x03 // A(sym-TOC) Relative to TOC + R_TRL = 0x12 // A(sym-TOC) TOC Relative indirect load. + + R_TRLA = 0x13 // A(sym-TOC) TOC Rel load address. modifiable inst + R_GL = 0x05 // A(external TOC of sym) Global Linkage + R_TCL = 0x06 // A(local TOC of sym) Local object TOC address + R_RL = 0x0C // A(sym) Pos indirect load. modifiable instruction + R_RLA = 0x0D // A(sym) Pos Load Address. modifiable instruction + R_REF = 0x0F // AL0(sym) Non relocating ref. No garbage collect + R_BA = 0x08 // A(sym) Branch absolute. Cannot modify instruction + R_RBA = 0x18 // A(sym) Branch absolute. modifiable instruction + R_BR = 0x0A // A(sym-*) Branch rel to self. non modifiable + R_RBR = 0x1A // A(sym-*) Branch rel to self. modifiable instr + + R_TLS = 0x20 // General-dynamic reference to TLS symbol + R_TLS_IE = 0x21 // Initial-exec reference to TLS symbol + R_TLS_LD = 0x22 // Local-dynamic reference to TLS symbol + R_TLS_LE = 0x23 // Local-exec reference to TLS symbol + R_TLSM = 0x24 // Module reference to TLS symbol + R_TLSML = 0x25 // Module reference to local (own) module + + R_TOCU = 0x30 // Relative to TOC - high order bits + R_TOCL = 0x31 // Relative to TOC - low order bits +) diff --git a/syft/pkg/cataloger/golang/parse_go_bin.go b/syft/pkg/cataloger/golang/parse_go_bin.go index bf18c3ba4..3a1c3f017 100644 --- a/syft/pkg/cataloger/golang/parse_go_bin.go +++ b/syft/pkg/cataloger/golang/parse_go_bin.go @@ -1,26 +1,53 @@ +//nolint package golang import ( - "bufio" + "bytes" + "debug/elf" + "debug/macho" + "debug/pe" + "errors" "fmt" "io" + "runtime/debug" "strings" + "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/golang/internal/xcoff" "github.com/anchore/syft/syft/source" ) -const ( - packageIdentifier = "dep" - replaceIdentifier = "=>" +const GOARCH = "GOARCH" + +var ( + // errUnrecognizedFormat is returned when a given executable file doesn't + // appear to be in a known format, or it breaks the rules of that format, + // or when there are I/O errors reading the file. + errUnrecognizedFormat = errors.New("unrecognized file format") ) -type exeOpener func(file io.ReadCloser) ([]exe, error) +func makeGoMainPackage(mod *debug.BuildInfo, arch string, location source.Location) pkg.Package { + gbs := getBuildSettings(mod.Settings) + main := newGoBinaryPackage(&mod.Main, mod.GoVersion, arch, location, gbs) + main.Version = "" + + if v, ok := gbs["vcs.revision"]; ok { + main.Version = v + } + + return main +} + +func newGoBinaryPackage(dep *debug.Module, goVersion, architecture string, location source.Location, buildSettings map[string]string) pkg.Package { + if dep.Replace != nil { + dep = dep.Replace + } -func newGoBinaryPackage(name, version, h1Digest, goVersion, architecture string, location source.Location) pkg.Package { p := pkg.Package{ - Name: name, - Version: version, + FoundBy: catalogerName, + Name: dep.Path, + Version: dep.Version, Language: pkg.Go, Type: pkg.GoModulePkg, Locations: []source.Location{ @@ -29,8 +56,9 @@ func newGoBinaryPackage(name, version, h1Digest, goVersion, architecture string, MetadataType: pkg.GolangBinMetadataType, Metadata: pkg.GolangBinMetadata{ GoCompiledVersion: goVersion, - H1Digest: h1Digest, + H1Digest: dep.Sum, Architecture: architecture, + BuildSettings: buildSettings, }, } @@ -39,57 +67,128 @@ func newGoBinaryPackage(name, version, h1Digest, goVersion, architecture string, return p } -func parseGoBin(location source.Location, reader io.ReadCloser, opener exeOpener) (pkgs []pkg.Package, err error) { - var exes []exe - // it has been found that there are stdlib paths within openExe that can panic. We want to prevent this behavior - // bubbling up and halting execution. For this reason we try to recover from any panic and return an error. - defer func() { - if r := recover(); r != nil { - err = fmt.Errorf("recovered from panic while parse go binary at %q: %+v", location.RealPath, r) - } - }() - - // Identify if bin was compiled by go - exes, err = opener(reader) - if err != nil { - return pkgs, err +// getArchs finds a binary architecture by two ways: +// 1) reading build info from binaries compiled by go1.18+ +// 2) reading file headers from binaries compiled by < go1.18 +func getArchs(readers []io.ReaderAt, builds []*debug.BuildInfo) []string { + if len(readers) != len(builds) { + log.Warnf("golang cataloger: bin parsing: number of builds and readers doesn't match") + return nil } - for _, x := range exes { - goVersion, mod := findVers(x) - pkgs = append(pkgs, buildGoPkgInfo(location, mod, goVersion, x.ArchName())...) + if len(readers) == 0 || len(builds) == 0 { + log.Warnf("golang cataloger: bin parsing: %d readers and %d build info items", len(readers), len(builds)) + return nil } - return pkgs, err -} -func buildGoPkgInfo(location source.Location, mod, goVersion, arch string) []pkg.Package { - pkgsSlice := make([]pkg.Package, 0) - scanner := bufio.NewScanner(strings.NewReader(mod)) + archs := make([]string, len(builds)) + for i, build := range builds { + archs[i] = getGOARCH(build.Settings) + } - // filter mod dependencies: [dep, name, version, sha] - for scanner.Scan() { - fields := strings.Fields(scanner.Text()) + // if architecture was found via build settings return + if archs[0] != "" { + return archs + } - // must have dep, name, version - if len(fields) < 3 { + for i, r := range readers { + a, err := getGOARCHFromBin(r) + if err != nil { + log.Warnf("golang cataloger: bin parsing: getting arch from binary: %v", err) continue } - name := fields[1] - version := fields[2] - h1Digest := "" - // if dep is *not* vendored, it'll also have h1digest - if len(fields) >= 4 { - h1Digest = fields[3] - } + archs[i] = a + } + return archs +} - if fields[0] == packageIdentifier { - pkgsSlice = append(pkgsSlice, newGoBinaryPackage(name, version, h1Digest, goVersion, arch, location)) - } - if fields[0] == replaceIdentifier { - // replace the previous entry in the package slice - pkgsSlice[len(pkgsSlice)-1] = newGoBinaryPackage(name, version, h1Digest, goVersion, arch, location) +func getGOARCH(settings []debug.BuildSetting) string { + for _, s := range settings { + if s.Key == GOARCH { + return s.Value } } - return pkgsSlice + + return "" +} + +func getGOARCHFromBin(r io.ReaderAt) (string, error) { + // Read the first bytes of the file to identify the format, then delegate to + // a format-specific function to load segment and section headers. + ident := make([]byte, 16) + if n, err := r.ReadAt(ident, 0); n < len(ident) || err != nil { + return "", fmt.Errorf("unrecognized file format: %w", err) + } + + var arch string + switch { + case bytes.HasPrefix(ident, []byte("\x7FELF")): + f, err := elf.NewFile(r) + if err != nil { + return "", fmt.Errorf("unrecognized file format: %w", err) + } + arch = f.Machine.String() + case bytes.HasPrefix(ident, []byte("MZ")): + f, err := pe.NewFile(r) + if err != nil { + return "", fmt.Errorf("unrecognized file format: %w", err) + } + arch = fmt.Sprintf("%d", f.Machine) + case bytes.HasPrefix(ident, []byte("\xFE\xED\xFA")) || bytes.HasPrefix(ident[1:], []byte("\xFA\xED\xFE")): + f, err := macho.NewFile(r) + if err != nil { + return "", fmt.Errorf("unrecognized file format: %w", err) + } + arch = f.Cpu.String() + case bytes.HasPrefix(ident, []byte{0x01, 0xDF}) || bytes.HasPrefix(ident, []byte{0x01, 0xF7}): + f, err := xcoff.NewFile(r) + if err != nil { + return "", fmt.Errorf("unrecognized file format: %w", err) + } + arch = fmt.Sprintf("%d", f.FileHeader.TargetMachine) + default: + return "", errUnrecognizedFormat + } + + arch = strings.Replace(arch, "EM_", "", 1) + arch = strings.Replace(arch, "Cpu", "", 1) + arch = strings.ToLower(arch) + + return arch, nil +} + +func getBuildSettings(settings []debug.BuildSetting) map[string]string { + m := make(map[string]string) + for _, s := range settings { + m[s.Key] = s.Value + } + return m +} + +func buildGoPkgInfo(location source.Location, mod *debug.BuildInfo, arch string) []pkg.Package { + var pkgs []pkg.Package + if mod == nil { + return pkgs + } + + for _, dep := range mod.Deps { + if dep == nil { + continue + } + + pkgs = append(pkgs, newGoBinaryPackage(dep, mod.GoVersion, arch, location, nil)) + } + + // NOTE(jonasagx): this use happened originally while creating unit tests. It might never + // happen in the wild, but I kept it as a safeguard against empty modules. + var empty debug.Module + if mod.Main == empty { + return pkgs + } + + main := makeGoMainPackage(mod, arch, location) + pkgs = append(pkgs, main) + + return pkgs } diff --git a/syft/pkg/cataloger/golang/parse_go_bin_test.go b/syft/pkg/cataloger/golang/parse_go_bin_test.go index c74b869b1..2cd1cfcbd 100644 --- a/syft/pkg/cataloger/golang/parse_go_bin_test.go +++ b/syft/pkg/cataloger/golang/parse_go_bin_test.go @@ -1,38 +1,254 @@ package golang import ( + "bufio" "io" + "os" + "os/exec" + "path/filepath" + "runtime/debug" + "strconv" + "syscall" "testing" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +// make will run the default make target for the given test fixture path +func runMakeTarget(t *testing.T, fixtureName string) { + cwd, err := os.Getwd() + require.NoError(t, err) + fixtureDir := filepath.Join(cwd, "test-fixtures/", fixtureName) + + t.Logf("Generating Fixture in %q", fixtureDir) + + cmd := exec.Command("make") + cmd.Dir = fixtureDir + + stderr, err := cmd.StderrPipe() + require.NoError(t, err) + + stdout, err := cmd.StdoutPipe() + require.NoError(t, err) + + err = cmd.Start() + require.NoError(t, err) + + show := func(label string, reader io.ReadCloser) { + scanner := bufio.NewScanner(reader) + scanner.Split(bufio.ScanLines) + for scanner.Scan() { + t.Logf("%s: %s", label, scanner.Text()) + } + } + go show("out", stdout) + go show("err", stderr) + + if err := cmd.Wait(); err != nil { + if exiterr, ok := err.(*exec.ExitError); ok { + // The program has exited with an exit code != 0 + + // This works on both Unix and Windows. Although package + // syscall is generally platform dependent, WaitStatus is + // defined for both Unix and Windows and in both cases has + // an ExitStatus() method with the same signature. + if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { + if status.ExitStatus() != 0 { + t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus()) + } + } + } else { + t.Fatalf("unable to get generate fixture result: %+v", err) + } + } +} + +func Test_getGOARCHFromBin(t *testing.T) { + runMakeTarget(t, "archs") + + tests := []struct { + name string + filepath string + expected string + }{ + { + name: "pe", + filepath: "test-fixtures/archs/binaries/hello-win-amd64", + // see: https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#machine-types + expected: strconv.Itoa(0x8664), + }, + { + name: "elf-ppc64", + filepath: "test-fixtures/archs/binaries/hello-linux-ppc64le", + expected: "ppc64", + }, + { + name: "mach-o-arm64", + filepath: "test-fixtures/archs/binaries/hello-mach-o-arm64", + expected: "arm64", + }, + { + name: "linux-arm", + filepath: "test-fixtures/archs/binaries/hello-linux-arm", + expected: "arm", + }, + { + name: "xcoff-32bit", + filepath: "internal/xcoff/testdata/gcc-ppc32-aix-dwarf2-exec", + expected: strconv.Itoa(0x1DF), + }, + { + name: "xcoff-64bit", + filepath: "internal/xcoff/testdata/gcc-ppc64-aix-dwarf2-exec", + expected: strconv.Itoa(0x1F7), + }, + } + + for _, tt := range tests { + f, err := os.Open(tt.filepath) + require.NoError(t, err) + arch, err := getGOARCHFromBin(f) + require.NoError(t, err, "test name: %s", tt.name) + assert.Equal(t, tt.expected, arch) + } + +} + func TestBuildGoPkgInfo(t *testing.T) { const ( - goCompiledVersion = "1.17" + goCompiledVersion = "1.18" archDetails = "amd64" ) + buildSettings := map[string]string{ + "GOARCH": "amd64", + "GOOS": "darwin", + "GOAMD64": "v1", + } + + expectedMain := pkg.Package{ + Name: "github.com/anchore/syft", + FoundBy: catalogerName, + Language: pkg.Go, + Type: pkg.GoModulePkg, + Locations: []source.Location{ + { + Coordinates: source.Coordinates{ + RealPath: "/a-path", + FileSystemID: "layer-id", + }, + }, + }, + MetadataType: pkg.GolangBinMetadataType, + Metadata: pkg.GolangBinMetadata{ + GoCompiledVersion: goCompiledVersion, + Architecture: archDetails, + BuildSettings: buildSettings, + }, + } + tests := []struct { name string - mod string + mod *debug.BuildInfo + arch string expected []pkg.Package }{ { - name: "buildGoPkgInfo parses a blank mod string and returns no packages", - mod: "", - expected: make([]pkg.Package, 0), + name: "buildGoPkgInfo parses a nil mod", + mod: nil, + expected: []pkg.Package(nil), }, { - name: "buildGoPkgInfo parses a populated mod string and returns packages but no source info", - mod: `path github.com/anchore/syft mod github.com/anchore/syft (devel) - dep github.com/adrg/xdg v0.2.1 h1:VSVdnH7cQ7V+B33qSJHTCRlNgra1607Q8PzEmnvb2Ic= - dep github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg= - dep github.com/anchore/client-go v1.2.3`, + name: "buildGoPkgInfo parses a blank mod and returns no packages", + mod: &debug.BuildInfo{}, + expected: []pkg.Package(nil), + }, + { + name: "buildGoPkgInfo parses a mod without main module", + arch: archDetails, + mod: &debug.BuildInfo{ + GoVersion: goCompiledVersion, + Settings: []debug.BuildSetting{ + {Key: "GOARCH", Value: archDetails}, + {Key: "GOOS", Value: "darwin"}, + {Key: "GOAMD64", Value: "v1"}, + }, + Deps: []*debug.Module{ + { + Path: "github.com/adrg/xdg", + Version: "v0.2.1", + Sum: "h1:VSVdnH7cQ7V+B33qSJHTCRlNgra1607Q8PzEmnvb2Ic=", + }, + }, + }, expected: []pkg.Package{ { Name: "github.com/adrg/xdg", + FoundBy: catalogerName, + Version: "v0.2.1", + Language: pkg.Go, + Type: pkg.GoModulePkg, + Locations: []source.Location{ + { + Coordinates: source.Coordinates{ + RealPath: "/a-path", + FileSystemID: "layer-id", + }, + }, + }, + MetadataType: pkg.GolangBinMetadataType, + Metadata: pkg.GolangBinMetadata{ + GoCompiledVersion: goCompiledVersion, + Architecture: archDetails, + H1Digest: "h1:VSVdnH7cQ7V+B33qSJHTCRlNgra1607Q8PzEmnvb2Ic=", + }, + }, + }, + }, + { + name: "buildGoPkgInfo parses a mod without packages", + arch: archDetails, + mod: &debug.BuildInfo{ + GoVersion: goCompiledVersion, + Main: debug.Module{Path: "github.com/anchore/syft"}, + Settings: []debug.BuildSetting{ + {Key: "GOARCH", Value: archDetails}, + {Key: "GOOS", Value: "darwin"}, + {Key: "GOAMD64", Value: "v1"}, + }, + }, + expected: []pkg.Package{expectedMain}, + }, + { + name: "buildGoPkgInfo parses a populated mod string and returns packages but no source info", + arch: archDetails, + mod: &debug.BuildInfo{ + GoVersion: goCompiledVersion, + Main: debug.Module{Path: "github.com/anchore/syft"}, + Settings: []debug.BuildSetting{ + {Key: "GOARCH", Value: archDetails}, + {Key: "GOOS", Value: "darwin"}, + {Key: "GOAMD64", Value: "v1"}, + }, + Deps: []*debug.Module{ + { + Path: "github.com/adrg/xdg", + Version: "v0.2.1", + Sum: "h1:VSVdnH7cQ7V+B33qSJHTCRlNgra1607Q8PzEmnvb2Ic=", + }, + { + Path: "github.com/anchore/client-go", + Version: "v0.0.0-20210222170800-9c70f9b80bcf", + Sum: "h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg=", + }, + }, + }, + expected: []pkg.Package{ + { + Name: "github.com/adrg/xdg", + FoundBy: catalogerName, Version: "v0.2.1", Language: pkg.Go, Type: pkg.GoModulePkg, @@ -53,6 +269,7 @@ func TestBuildGoPkgInfo(t *testing.T) { }, { Name: "github.com/anchore/client-go", + FoundBy: catalogerName, Version: "v0.0.0-20210222170800-9c70f9b80bcf", Language: pkg.Go, Type: pkg.GoModulePkg, @@ -71,59 +288,42 @@ func TestBuildGoPkgInfo(t *testing.T) { H1Digest: "h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg=", }, }, - { - Name: "github.com/anchore/client-go", - Version: "v1.2.3", - Language: pkg.Go, - Type: pkg.GoModulePkg, - Locations: []source.Location{ - { - Coordinates: source.Coordinates{ - RealPath: "/a-path", - FileSystemID: "layer-id", - }, - }, - }, - MetadataType: pkg.GolangBinMetadataType, - Metadata: pkg.GolangBinMetadata{ - GoCompiledVersion: goCompiledVersion, - Architecture: archDetails, - }, - }, + expectedMain, }, }, { name: "buildGoPkgInfo parses a populated mod string and returns packages when a replace directive exists", - mod: `path github.com/anchore/test - mod github.com/anchore/test (devel) - dep golang.org/x/net v0.0.0-20211006190231-62292e806868 h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k= - dep golang.org/x/sys v0.0.0-20211006194710-c8a6f5223071 h1:PjhxBct4MZii8FFR8+oeS7QOvxKOTZXgk63EU2XpfJE= - dep golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 - => golang.org/x/term v0.0.0-20210916214954-140adaaadfaf h1:Ihq/mm/suC88gF8WFcVwk+OV6Tq+wyA1O0E5UEvDglI= - dep github.com/anchore/client-go v1.2.3`, - expected: []pkg.Package{ - { - Name: "golang.org/x/net", - Version: "v0.0.0-20211006190231-62292e806868", - Language: pkg.Go, - Type: pkg.GoModulePkg, - Locations: []source.Location{ - { - Coordinates: source.Coordinates{ - RealPath: "/a-path", - FileSystemID: "layer-id", - }, + arch: archDetails, + mod: &debug.BuildInfo{ + GoVersion: goCompiledVersion, + Main: debug.Module{Path: "github.com/anchore/syft"}, + Settings: []debug.BuildSetting{ + {Key: "GOARCH", Value: archDetails}, + {Key: "GOOS", Value: "darwin"}, + {Key: "GOAMD64", Value: "v1"}, + }, + Deps: []*debug.Module{ + { + Path: "golang.org/x/sys", + Version: "v0.0.0-20211006194710-c8a6f5223071", + Sum: "h1:PjhxBct4MZii8FFR8+oeS7QOvxKOTZXgk63EU2XpfJE=", + }, + { + Path: "golang.org/x/term", + Version: "v0.0.0-20210927222741-03fcf44c2211", + Sum: "h1:PjhxBct4MZii8FFR8+oeS7QOvxKOTZXgk63EU2XpfJE=", + Replace: &debug.Module{ + Path: "golang.org/x/term", + Version: "v0.0.0-20210916214954-140adaaadfaf", + Sum: "h1:Ihq/mm/suC88gF8WFcVwk+OV6Tq+wyA1O0E5UEvDglI=", }, }, - MetadataType: pkg.GolangBinMetadataType, - Metadata: pkg.GolangBinMetadata{ - GoCompiledVersion: goCompiledVersion, - Architecture: archDetails, - H1Digest: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k=", - }, }, + }, + expected: []pkg.Package{ { Name: "golang.org/x/sys", + FoundBy: catalogerName, Version: "v0.0.0-20211006194710-c8a6f5223071", Language: pkg.Go, Type: pkg.GoModulePkg, @@ -132,18 +332,15 @@ func TestBuildGoPkgInfo(t *testing.T) { Coordinates: source.Coordinates{ RealPath: "/a-path", FileSystemID: "layer-id", - }, - }, - }, + }}}, MetadataType: pkg.GolangBinMetadataType, Metadata: pkg.GolangBinMetadata{ GoCompiledVersion: goCompiledVersion, Architecture: archDetails, - H1Digest: "h1:PjhxBct4MZii8FFR8+oeS7QOvxKOTZXgk63EU2XpfJE=", - }, - }, + H1Digest: "h1:PjhxBct4MZii8FFR8+oeS7QOvxKOTZXgk63EU2XpfJE="}}, { Name: "golang.org/x/term", + FoundBy: catalogerName, Version: "v0.0.0-20210916214954-140adaaadfaf", Language: pkg.Go, Type: pkg.GoModulePkg, @@ -159,28 +356,9 @@ func TestBuildGoPkgInfo(t *testing.T) { Metadata: pkg.GolangBinMetadata{ GoCompiledVersion: goCompiledVersion, Architecture: archDetails, - H1Digest: "h1:Ihq/mm/suC88gF8WFcVwk+OV6Tq+wyA1O0E5UEvDglI=", - }, - }, - { - Name: "github.com/anchore/client-go", - Version: "v1.2.3", - Language: pkg.Go, - Type: pkg.GoModulePkg, - Locations: []source.Location{ - { - Coordinates: source.Coordinates{ - RealPath: "/a-path", - FileSystemID: "layer-id", - }, - }, - }, - MetadataType: pkg.GolangBinMetadataType, - Metadata: pkg.GolangBinMetadata{ - GoCompiledVersion: goCompiledVersion, - Architecture: archDetails, - }, + H1Digest: "h1:Ihq/mm/suC88gF8WFcVwk+OV6Tq+wyA1O0E5UEvDglI="}, }, + expectedMain, }, }, } @@ -197,30 +375,8 @@ func TestBuildGoPkgInfo(t *testing.T) { FileSystemID: "layer-id", }, } - pkgs := buildGoPkgInfo(location, test.mod, goCompiledVersion, archDetails) + pkgs := buildGoPkgInfo(location, test.mod, test.arch) assert.Equal(t, test.expected, pkgs) }) } } - -func Test_parseGoBin_recoversFromPanic(t *testing.T) { - freakOut := func(file io.ReadCloser) ([]exe, error) { - panic("baaahhh!") - } - tests := []struct { - name string - wantPkgs []pkg.Package - wantErr assert.ErrorAssertionFunc - }{ - { - name: "recovers from panic", - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - pkgs, err := parseGoBin(source.NewLocation("some/path"), nil, freakOut) - assert.Error(t, err) - assert.Nil(t, pkgs) - }) - } -} diff --git a/syft/pkg/cataloger/golang/scan_bin.go b/syft/pkg/cataloger/golang/scan_bin.go new file mode 100644 index 000000000..f7359a568 --- /dev/null +++ b/syft/pkg/cataloger/golang/scan_bin.go @@ -0,0 +1,65 @@ +//nolint +package golang + +import ( + "debug/buildinfo" + "io" + "runtime/debug" + + macho "github.com/anchore/go-macholibre" + "github.com/anchore/syft/internal/log" +) + +// unionReader is a single interface with all reading functions used by golang bin +// cataloger. +type unionReader interface { + io.Reader + io.ReaderAt + io.Seeker + io.Closer +} + +// scanFile scans file to try to report the Go and module versions. +func scanFile(reader unionReader, filename string) ([]*debug.BuildInfo, []string) { + // NOTE: multiple readers are returned to cover universal binaries, which are files + // with more than one binary + readers, err := getReaders(reader) + if err != nil { + log.Warnf("golang cataloger: opening binary: %v", err) + return nil, nil + } + + var builds []*debug.BuildInfo + for _, r := range readers { + bi, err := buildinfo.Read(r) + if err != nil { + log.Warnf("golang cataloger: scanning file %s: %v", filename, err) + return nil, nil + } + builds = append(builds, bi) + } + + archs := getArchs(readers, builds) + + return builds, archs +} + +// getReaders extracts one or more io.ReaderAt objects representing binaries that can be processed (multiple binaries in the case for multi-architecture binaries). +func getReaders(f unionReader) ([]io.ReaderAt, error) { + if macho.IsUniversalMachoBinary(f) { + machoReaders, err := macho.ExtractReaders(f) + if err != nil { + log.Debugf("extracting readers: %v", err) + return nil, err + } + + var readers []io.ReaderAt + for _, e := range machoReaders { + readers = append(readers, e.Reader) + } + + return readers, nil + } + + return []io.ReaderAt{f}, nil +} diff --git a/syft/pkg/cataloger/golang/test-fixtures/archs/.gitignore b/syft/pkg/cataloger/golang/test-fixtures/archs/.gitignore new file mode 100644 index 000000000..3155ad3e0 --- /dev/null +++ b/syft/pkg/cataloger/golang/test-fixtures/archs/.gitignore @@ -0,0 +1 @@ +binaries/ \ No newline at end of file diff --git a/syft/pkg/cataloger/golang/test-fixtures/archs/Makefile b/syft/pkg/cataloger/golang/test-fixtures/archs/Makefile new file mode 100644 index 000000000..60eee7ff9 --- /dev/null +++ b/syft/pkg/cataloger/golang/test-fixtures/archs/Makefile @@ -0,0 +1,29 @@ +DESTINATION=binaries + +all: $(DESTINATION)/hello-mach-o-arm64 $(DESTINATION)/hello-linux-arm $(DESTINATION)/hello-linux-ppc64le $(DESTINATION)/hello-win-amd64 + +$(DESTINATION)/hello-mach-o-arm64: + mkdir -p $(DESTINATION) + GOARCH=arm64 GOOS=darwin ./src/build.sh $(DESTINATION)/hello-mach-o-arm64 + +$(DESTINATION)/hello-linux-arm: + mkdir -p $(DESTINATION) + GOARCH=arm GOOS=linux ./src/build.sh $(DESTINATION)/hello-linux-arm + +$(DESTINATION)/hello-linux-ppc64le: + mkdir -p $(DESTINATION) + GOARCH=ppc64le GOOS=linux ./src/build.sh $(DESTINATION)/hello-linux-ppc64le + +$(DESTINATION)/hello-win-amd64: + mkdir -p $(DESTINATION) + GOARCH=amd64 GOOS=windows ./src/build.sh $(DESTINATION)/hello-win-amd64 + +# we need a way to determine if CI should bust the test cache based on the source material +$(DESTINATION).fingerprint: clean + mkdir -p $(DESTINATION) + find src -type f -exec sha256sum {} \; | sort | tee /dev/stderr | tee $(DESTINATION).fingerprint + sha256sum $(DESTINATION).fingerprint + +.PHONY: clean +clean: + rm -f $(DESTINATION)/* diff --git a/syft/pkg/cataloger/golang/test-fixtures/archs/src/build.sh b/syft/pkg/cataloger/golang/test-fixtures/archs/src/build.sh new file mode 100755 index 000000000..8a3919470 --- /dev/null +++ b/syft/pkg/cataloger/golang/test-fixtures/archs/src/build.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -uxe + +# note: this can be easily done in a 1-liner, however circle CI does NOT allow volume mounts from the host in docker executors (since they are on remote hosts, where the host files are inaccessible) +# note: gocache override is so we can run docker build not as root in a container without permission issues + +BINARY=$1 +CTRID=$(docker create -e GOOS="${GOOS}" -e GOARCH="${GOARCH}" -u "$(id -u):$(id -g)" -e GOCACHE=/tmp -w /src golang:1.17 go build -o main main.go) + +function cleanup() { + docker rm "${CTRID}" +} + +trap cleanup EXIT +set +e + +# note: pwd = parent directory (archs) +docker cp "$(pwd)/src" "${CTRID}:/" +docker start -a "${CTRID}" +docker cp "${CTRID}:/src/main" "$BINARY" diff --git a/syft/pkg/cataloger/golang/test-fixtures/archs/src/go.mod b/syft/pkg/cataloger/golang/test-fixtures/archs/src/go.mod new file mode 100644 index 000000000..d49c920e3 --- /dev/null +++ b/syft/pkg/cataloger/golang/test-fixtures/archs/src/go.mod @@ -0,0 +1,3 @@ +module arch/v1 + +go 1.17 diff --git a/syft/pkg/cataloger/golang/test-fixtures/archs/src/main.go b/syft/pkg/cataloger/golang/test-fixtures/archs/src/main.go new file mode 100644 index 000000000..d592354fb --- /dev/null +++ b/syft/pkg/cataloger/golang/test-fixtures/archs/src/main.go @@ -0,0 +1,5 @@ +package main + +func main() { + println("свобода!") +} diff --git a/syft/pkg/cataloger/golang/version.go b/syft/pkg/cataloger/golang/version.go deleted file mode 100644 index 6306a2da9..000000000 --- a/syft/pkg/cataloger/golang/version.go +++ /dev/null @@ -1,226 +0,0 @@ -// This code was copied from the Go std library. -// https://github.com/golang/go/blob/master/src/cmd/go/internal/version/version.go -// Copyright 2011 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package version implements the ``go version'' command. -package golang - -import ( - "bytes" - "encoding/binary" - // "cmd/go/internal/base" -) - -/* -var CmdVersion = &base.Command{ - UsageLine: "go version [-m] [-v] [file ...]", - Short: "print Go version", - Long: `Version prints the build information for Go executables. - -Go version reports the Go version used to build each of the named -executable files. - -If no files are named on the command line, go version prints its own -version information. - -If a directory is named, go version walks that directory, recursively, -looking for recognized Go binaries and reporting their versions. -By default, go version does not report unrecognized files found -during a directory scan. The -v flag causes it to report unrecognized files. - -The -m flag causes go version to print each executable's embedded -module version information, when available. In the output, the module -information consists of multiple lines following the version line, each -indented by a leading tab character. - -See also: go doc runtime/debug.BuildInfo. -`, -} - -func init() { - CmdVersion.Run = runVersion // break init cycle -} - -var ( - versionM = CmdVersion.Flag.Bool("m", false, "") - versionV = CmdVersion.Flag.Bool("v", false, "") -) - -func runVersion(ctx context.Context, cmd *base.Command, args []string) { - if len(args) == 0 { - // If any of this command's flags were passed explicitly, error - // out, because they only make sense with arguments. - // - // Don't error if the flags came from GOFLAGS, since that can be - // a reasonable use case. For example, imagine GOFLAGS=-v to - // turn "verbose mode" on for all Go commands, which should not - // break "go version". - var argOnlyFlag string - if !base.InGOFLAGS("-m") && *versionM { - argOnlyFlag = "-m" - } else if !base.InGOFLAGS("-v") && *versionV { - argOnlyFlag = "-v" - } - if argOnlyFlag != "" { - fmt.Fprintf(os.Stderr, "go: 'go version' only accepts %s flag with arguments\n", argOnlyFlag) - base.SetExitStatus(2) - return - } - fmt.Printf("go version %s %s/%s\n", runtime.Version(), runtime.GOOS, runtime.GOARCH) - return - } - - for _, arg := range args { - info, err := os.Stat(arg) - if err != nil { - fmt.Fprintf(os.Stderr, "%v\n", err) - base.SetExitStatus(1) - continue - } - if info.IsDir() { - scanDir(arg) - } else { - scanFile(arg, info, true) - } - } -} - -// scanDir scans a directory for executables to run scanFile on. -func scanDir(dir string) { - filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { - if d.Type().IsRegular() || d.Type()&fs.ModeSymlink != 0 { - info, err := d.Info() - if err != nil { - if *versionV { - fmt.Fprintf(os.Stderr, "%s: %v\n", path, err) - } - return nil - } - scanFile(path, info, *versionV) - } - return nil - }) -} - -// isExe reports whether the file should be considered executable. -func isExe(file string, info fs.FileInfo) bool { - if runtime.GOOS == "windows" { - return strings.HasSuffix(strings.ToLower(file), ".exe") - } - return info.Mode().IsRegular() && info.Mode()&0111 != 0 -} - -// scanFile scans file to try to report the Go and module versions. -// If mustPrint is true, scanFile will report any error reading file. -// Otherwise (mustPrint is false, because scanFile is being called -// by scanDir) scanFile prints nothing for non-Go executables. -func scanFile(file string, info fs.FileInfo, mustPrint bool) { - if info.Mode()&fs.ModeSymlink != 0 { - // Accept file symlinks only. - i, err := os.Stat(file) - if err != nil || !i.Mode().IsRegular() { - if mustPrint { - fmt.Fprintf(os.Stderr, "%s: symlink\n", file) - } - return - } - info = i - } - - if !isExe(file, info) { - if mustPrint { - fmt.Fprintf(os.Stderr, "%s: not executable file\n", file) - } - return - } - - x, err := openExe(file) - if err != nil { - if mustPrint { - fmt.Fprintf(os.Stderr, "%s: %v\n", file, err) - } - return - } - defer x.Close() - - vers, mod := findVers(x) - if vers == "" { - if mustPrint { - fmt.Fprintf(os.Stderr, "%s: go version not found\n", file) - } - return - } - - fmt.Printf("%s: %s\n", file, vers) - if *versionM && mod != "" { - fmt.Printf("\t%s\n", strings.ReplaceAll(mod[:len(mod)-1], "\n", "\n\t")) - } -} -*/ - -// The build info blob left by the linker is identified by -// a 16-byte header, consisting of buildInfoMagic (14 bytes), -// the binary's pointer size (1 byte), -// and whether the binary is big endian (1 byte). -var buildInfoMagic = []byte("\xff Go buildinf:") - -// findVers finds and returns the Go version and module version information -// in the executable x. -func findVers(x exe) (vers, mod string) { - // Read the first 64kB of text to find the build info blob. - text := x.DataStart() - data, err := x.ReadData(text, 64*1024) - if err != nil { - return - } - for ; !bytes.HasPrefix(data, buildInfoMagic); data = data[32:] { - if len(data) < 32 { - return - } - } - - // Decode the blob. - ptrSize := int(data[14]) - bigEndian := data[15] != 0 - var bo binary.ByteOrder - if bigEndian { - bo = binary.BigEndian - } else { - bo = binary.LittleEndian - } - var readPtr func([]byte) uint64 - if ptrSize == 4 { - readPtr = func(b []byte) uint64 { return uint64(bo.Uint32(b)) } - } else { - readPtr = bo.Uint64 - } - vers = readString(x, ptrSize, readPtr, readPtr(data[16:])) - if vers == "" { - return - } - mod = readString(x, ptrSize, readPtr, readPtr(data[16+ptrSize:])) - if len(mod) >= 33 && mod[len(mod)-17] == '\n' { - // Strip module framing. - mod = mod[16 : len(mod)-16] - } else { - mod = "" - } - return vers, mod -} - -// readString returns the string at address addr in the executable x. -func readString(x exe, ptrSize int, readPtr func([]byte) uint64, addr uint64) string { - hdr, err := x.ReadData(addr, uint64(2*ptrSize)) - if err != nil || len(hdr) < 2*ptrSize { - return "" - } - dataAddr := readPtr(hdr) - dataLen := readPtr(hdr[ptrSize:]) - data, err := x.ReadData(dataAddr, dataLen) - if err != nil || uint64(len(data)) < dataLen { - return "" - } - return string(data) -} diff --git a/syft/pkg/golang_bin_metadata.go b/syft/pkg/golang_bin_metadata.go index e77a4b0f0..b7ba10f8c 100644 --- a/syft/pkg/golang_bin_metadata.go +++ b/syft/pkg/golang_bin_metadata.go @@ -2,7 +2,8 @@ package pkg // GolangBinMetadata represents all captured data for a Golang Binary type GolangBinMetadata struct { - GoCompiledVersion string `json:"goCompiledVersion" cyclonedx:"goCompiledVersion"` - Architecture string `json:"architecture" cyclonedx:"architecture"` - H1Digest string `json:"h1Digest" cyclonedx:"h1Digest"` + BuildSettings map[string]string `json:"goBuildSettings,omitempty" cyclonedx:"goBuildSettings"` + GoCompiledVersion string `json:"goCompiledVersion" cyclonedx:"goCompiledVersion"` + Architecture string `json:"architecture" cyclonedx:"architecture"` + H1Digest string `json:"h1Digest" cyclonedx:"h1Digest"` } diff --git a/syft/source/image_squash_resolver.go b/syft/source/image_squash_resolver.go index ba584897e..8977023a1 100644 --- a/syft/source/image_squash_resolver.go +++ b/syft/source/image_squash_resolver.go @@ -135,7 +135,7 @@ func (r *imageSquashResolver) RelativeFileByPath(_ Location, path string) *Locat return &paths[0] } -// FileContentsByLocation fetches file contents for a single file reference, irregardless of the source layer. +// FileContentsByLocation fetches file contents for a single file reference, regardless of the source layer. // If the path does not exist an error is returned. func (r *imageSquashResolver) FileContentsByLocation(location Location) (io.ReadCloser, error) { entry, err := r.img.FileCatalog.Get(location.ref) diff --git a/test/integration/regression_go_bin_scanner_arch_test.go b/test/integration/regression_go_bin_scanner_arch_test.go index f5e21727e..5b364d8f9 100644 --- a/test/integration/regression_go_bin_scanner_arch_test.go +++ b/test/integration/regression_go_bin_scanner_arch_test.go @@ -9,9 +9,9 @@ import ( func TestRegressionGoArchDiscovery(t *testing.T) { const ( - expectedELFPkg = 3 - expectedWINPkg = 3 - expectedMACOSPkg = 3 + expectedELFPkg = 4 + expectedWINPkg = 4 + expectedMACOSPkg = 4 ) // This is a regression test to make sure the way we detect go binary packages // stays consistent and reproducible as the tool chain evolves