mirror of
https://github.com/anchore/syft.git
synced 2026-10-13 22:57:19 +02:00
Compare commits
218
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3549b04e18 | ||
|
|
c9760d2341 | ||
|
|
74c9380248 | ||
|
|
7ed733c3fb | ||
|
|
a39c600913 | ||
|
|
a2020fe1c7 | ||
|
|
89824f0ae7 | ||
|
|
b361427043 | ||
|
|
c79a57b6a1 | ||
|
|
2c97ff1b24 | ||
|
|
e760a7cad4 | ||
|
|
e1ae4e1112 | ||
|
|
beb70891e5 | ||
|
|
e0b61a3ae3 | ||
|
|
4c38ee1932 | ||
|
|
6be0a9abc4 | ||
|
|
ea1f4cba38 | ||
|
|
c8982b887d | ||
|
|
6ad4873a33 | ||
|
|
052e4ca9a3 | ||
|
|
41e133e2cf | ||
|
|
a85e034afc | ||
|
|
d5380013ae | ||
|
|
281a9b87de | ||
|
|
5ea3387cbc | ||
|
|
bf1f0ceea3 | ||
|
|
568b7601bb | ||
|
|
7fdb08c0b6 | ||
|
|
47e1cee5a5 | ||
|
|
a0c5b8aa8d | ||
|
|
ab5fa0a664 | ||
|
|
07ad8a5573 | ||
|
|
703edff876 | ||
|
|
a05608a4c8 | ||
|
|
41aa6f6753 | ||
|
|
1a70ffe2fa | ||
|
|
33c5e40431 | ||
|
|
32946ec41f | ||
|
|
bfe63f83db | ||
|
|
9bf4c5bdf9 | ||
|
|
f01056d111 | ||
|
|
09b24bdb47 | ||
|
|
ae1fa09e02 | ||
|
|
6b0f924426 | ||
|
|
6d56087289 | ||
|
|
1d718f3311 | ||
|
|
9e3150b7ee | ||
|
|
d950ac1fae | ||
|
|
baca32f04a | ||
|
|
155738aba7 | ||
|
|
2b72158b0b | ||
|
|
a80679beba | ||
|
|
b0c74d4104 | ||
|
|
afe28a2fc0 | ||
|
|
d37ed567a8 | ||
|
|
e556ceb4a8 | ||
|
|
d8538e7d8b | ||
|
|
cd19ac956c | ||
|
|
d1a523fef5 | ||
|
|
e1e3d002bc | ||
|
|
a0f7148608 | ||
|
|
5b96d1d69d | ||
|
|
6c666383e7 | ||
|
|
b9710a1e79 | ||
|
|
023a14f869 | ||
|
|
439a063d08 | ||
|
|
c95893209d | ||
|
|
7e02bdfe45 | ||
|
|
479cf5aff2 | ||
|
|
f12788da78 | ||
|
|
67709362b6 | ||
|
|
55526dbde0 | ||
|
|
af167ba0c1 | ||
|
|
00e1329bd1 | ||
|
|
9aca8167b8 | ||
|
|
759909f611 | ||
|
|
7014cb023f | ||
|
|
a033ae525f | ||
|
|
1c22325385 | ||
|
|
75ad5c6c74 | ||
|
|
d2641dfa39 | ||
|
|
365325376a | ||
|
|
153f2321ce | ||
|
|
7bf7bcc461 | ||
|
|
6a21b5e5e2 | ||
|
|
6480c8a425 | ||
|
|
89842bd2f6 | ||
|
|
4a60c41f38 | ||
|
|
2e100f33f3 | ||
|
|
b444f0c2ed | ||
|
|
102d362daf | ||
|
|
66c78d44af | ||
|
|
78a4ab8ced | ||
|
|
25ca33d20e | ||
|
|
60ca241593 | ||
|
|
0f475c8bcd | ||
|
|
199394934d | ||
|
|
8a22d394ed | ||
|
|
bbef262b8f | ||
|
|
4e06a7ab32 | ||
|
|
e5711e9b42 | ||
|
|
f69b1db099 | ||
|
|
fe1ea443c2 | ||
|
|
bfcbf266df | ||
|
|
a400c675fc | ||
|
|
7c154e7c37 | ||
|
|
4c93394bc2 | ||
|
|
3e4e82f03e | ||
|
|
793b0a346f | ||
|
|
a0dac519db | ||
|
|
34f5e521c1 | ||
|
|
774b1e97b9 | ||
|
|
538430d65d | ||
|
|
5db3a9bf55 | ||
|
|
efc2f0012c | ||
|
|
c5c1454848 | ||
|
|
f5c765192c | ||
|
|
728feea620 | ||
|
|
45fb52dca1 | ||
|
|
45bf8b14ab | ||
|
|
9478cd974b | ||
|
|
0d9ea69a66 | ||
|
|
bee78c0b16 | ||
|
|
88bbcbe9c6 | ||
|
|
e0680eb704 | ||
|
|
16f851c5d9 | ||
|
|
d5ca1ad543 | ||
|
|
8be463911c | ||
|
|
44b7b0947c | ||
|
|
675075e882 | ||
|
|
31b2c4c090 | ||
|
|
07029ead8a | ||
|
|
f4de1e863c | ||
|
|
538b4a2194 | ||
|
|
fc74b07369 | ||
|
|
6627c5214c | ||
|
|
c0f32e1dba | ||
|
|
e923db2a94 | ||
|
|
0c98a364d5 | ||
|
|
4343d04652 | ||
|
|
065ac13ab7 | ||
|
|
e9a8bc5ab9 | ||
|
|
6d790ec6ec | ||
|
|
1d5bcc553a | ||
|
|
d22914baf5 | ||
|
|
760bd9a50a | ||
|
|
2d1ada1d00 | ||
|
|
8ffe15c710 | ||
|
|
89948dfa51 | ||
|
|
1a58f27f87 | ||
|
|
450cd72da5 | ||
|
|
5056c7f861 | ||
|
|
4ae8f73583 | ||
|
|
18e789c4fd | ||
|
|
7d4680bc08 | ||
|
|
231f04ae0e | ||
|
|
3b82a3724a | ||
|
|
337a2754e5 | ||
|
|
190f3068d8 | ||
|
|
bd013fe99a | ||
|
+1 |
c732052cf1 | ||
|
|
8f1d45830d | ||
|
|
ea7dc8f468 | ||
|
|
ff6a8b1802 | ||
|
|
a77d24e379 | ||
|
|
b96d3d20af | ||
|
|
5461a92337 | ||
|
|
b9604cbf30 | ||
|
|
9217f2099f | ||
|
|
605a275dd3 | ||
|
|
e1483e0285 | ||
|
|
0a36dabf23 | ||
|
|
64b71ec04c | ||
|
|
8629080e80 | ||
|
|
f0998de717 | ||
|
|
261ab7c1fd | ||
|
|
8232f5bd1b | ||
|
|
21d50d7c31 | ||
|
|
c28b90717b | ||
|
|
323fd3e34c | ||
|
|
af4d19f81d | ||
|
|
d820c3436b | ||
|
|
409642c8f0 | ||
|
|
3abbd940e3 | ||
|
|
22f6f8f880 | ||
|
|
6005fb3c20 | ||
|
|
b87b919149 | ||
|
|
a51994d102 | ||
|
|
333b951be3 | ||
|
|
90c733d24d | ||
|
|
dacc2f61f9 | ||
|
|
06b01aaa40 | ||
|
|
e1762a2dda | ||
|
|
c5cbc89cb1 | ||
|
|
7bc15e3d82 | ||
|
|
c6cd66357a | ||
|
|
04e989d761 | ||
|
|
b6f7532b0f | ||
|
|
2531bfd8cb | ||
|
|
1fcdb67698 | ||
|
|
f986327257 | ||
|
|
67e0f7e3f9 | ||
|
|
169220ba81 | ||
|
|
1df4779b48 | ||
|
|
3a7f1f27a6 | ||
|
|
8e78fd57b8 | ||
|
|
b503690889 | ||
|
|
cc07df0347 | ||
|
|
2b8f4bc028 | ||
|
|
98c97e24a2 | ||
|
|
6f4da8c797 | ||
|
|
647196055d | ||
|
|
39441f1999 | ||
|
|
507987c193 | ||
|
|
7e4bf7f8c2 | ||
|
|
bc18e3ab8c | ||
|
|
c4eb071324 | ||
|
|
cbcf8bd542 |
+8
-8
@@ -2,7 +2,7 @@ tools:
|
||||
# we want to use a pinned version of binny to manage the toolchain (so binny manages itself!)
|
||||
- name: binny
|
||||
version:
|
||||
want: v0.9.0
|
||||
want: v0.11.0
|
||||
method: github-release
|
||||
with:
|
||||
repo: anchore/binny
|
||||
@@ -26,7 +26,7 @@ tools:
|
||||
# used for linting
|
||||
- name: golangci-lint
|
||||
version:
|
||||
want: v2.4.0
|
||||
want: v2.7.2
|
||||
method: github-release
|
||||
with:
|
||||
repo: golangci/golangci-lint
|
||||
@@ -42,7 +42,7 @@ tools:
|
||||
# used for signing the checksums file at release
|
||||
- name: cosign
|
||||
version:
|
||||
want: v2.5.3
|
||||
want: v3.0.3
|
||||
method: github-release
|
||||
with:
|
||||
repo: sigstore/cosign
|
||||
@@ -58,7 +58,7 @@ tools:
|
||||
# used to release all artifacts
|
||||
- name: goreleaser
|
||||
version:
|
||||
want: v2.11.2
|
||||
want: v2.13.1
|
||||
method: github-release
|
||||
with:
|
||||
repo: goreleaser/goreleaser
|
||||
@@ -90,7 +90,7 @@ tools:
|
||||
# used for running all local and CI tasks
|
||||
- name: task
|
||||
version:
|
||||
want: v3.44.1
|
||||
want: v3.45.5
|
||||
method: github-release
|
||||
with:
|
||||
repo: go-task/task
|
||||
@@ -98,7 +98,7 @@ tools:
|
||||
# used for triggering a release
|
||||
- name: gh
|
||||
version:
|
||||
want: v2.78.0
|
||||
want: v2.83.2
|
||||
method: github-release
|
||||
with:
|
||||
repo: cli/cli
|
||||
@@ -106,7 +106,7 @@ tools:
|
||||
# used to upload test fixture cache
|
||||
- name: oras
|
||||
version:
|
||||
want: v1.2.3
|
||||
want: v1.3.0
|
||||
method: github-release
|
||||
with:
|
||||
repo: oras-project/oras
|
||||
@@ -114,7 +114,7 @@ tools:
|
||||
# used to upload test fixture cache
|
||||
- name: yq
|
||||
version:
|
||||
want: v4.47.1
|
||||
want: v4.50.1
|
||||
method: github-release
|
||||
with:
|
||||
repo: mikefarah/yq
|
||||
|
||||
@@ -9,6 +9,9 @@ permit:
|
||||
- Unlicense
|
||||
|
||||
ignore-packages:
|
||||
# https://github.com/sorairolake/lzip-go/blob/34a2615d2abf740175c6b0a835baa08364e09430/go.sum.license#L3
|
||||
# has `SPDX-License-Identifier: Apache-2.0 OR MIT`, both of which are acceptable
|
||||
- github.com/sorairolake/lzip-go
|
||||
# packageurl-go is released under the MIT license located in the root of the repo at /mit.LICENSE
|
||||
- github.com/anchore/packageurl-go
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
# note: go mod and build is automatically cached on default with v4+
|
||||
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
|
||||
if: inputs.go-version != ''
|
||||
with:
|
||||
go-version: ${{ inputs.go-version }}
|
||||
@@ -37,7 +37,7 @@ runs:
|
||||
- name: Restore tool cache
|
||||
if: inputs.tools == 'true'
|
||||
id: tool-cache
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
with:
|
||||
path: ${{ github.workspace }}/.tool
|
||||
key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-tool-${{ hashFiles('.binny.yaml') }}
|
||||
@@ -63,7 +63,7 @@ runs:
|
||||
|
||||
- name: Restore ORAS cache from github actions
|
||||
if: inputs.download-test-fixture-cache == 'true'
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
with:
|
||||
path: ${{ github.workspace }}/.tmp/oras-cache
|
||||
key: ${{ inputs.cache-key-prefix }}-oras-cache
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
# Description
|
||||
## Description
|
||||
|
||||
Please include a summary of the changes along with any relevant motivation and context,
|
||||
or link to an issue where this is explained.
|
||||
<!-- Please include a summary of the changes along with any relevant motivation and context -->
|
||||
|
||||
<!-- If this completes an issue, please include: -->
|
||||
<!-- If CLI output changed, show an example (before/after if helpful) -->
|
||||
|
||||
- Fixes
|
||||
<!-- If this changes application or API configuration, describe new/changed/removed options -->
|
||||
|
||||
## Type of change
|
||||
|
||||
@@ -18,8 +17,12 @@ or link to an issue where this is explained.
|
||||
- [ ] Chore (improve the developer experience, fix a test flake, etc, without changing the visible behavior of Syft)
|
||||
- [ ] Performance (make Syft run faster or use less memory, without changing visible behavior much)
|
||||
|
||||
# Checklist:
|
||||
## Checklist
|
||||
|
||||
- [ ] I have added unit tests that cover changed behavior
|
||||
- [ ] I have tested my code in common scenarios and confirmed there are no regressions
|
||||
- [ ] I have added comments to my code, particularly in hard-to-understand sections
|
||||
|
||||
## Issue references
|
||||
|
||||
<!-- If this fixes an issue, include "Fixes #<issue-number>" or otherwise list the issue references -->
|
||||
|
||||
@@ -24,7 +24,7 @@ while IFS= read -r -d '' file; do
|
||||
echo "File $file is greater than ${size} bytes."
|
||||
found_large_files=1
|
||||
fi
|
||||
done < <(find "$directory" -type f -print0)
|
||||
done < <(git ls-files -z "$directory")
|
||||
|
||||
if [ "$found_large_files" -eq 1 ]; then
|
||||
echo "Script failed: Some files are greater than ${size} bytes."
|
||||
|
||||
@@ -36,18 +36,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 #v5.5.0
|
||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c #v6.1.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@3c3833e0f8c1c83d449a7478aa59c036a9165498 #v3.29.5
|
||||
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 #v3.29.5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@3c3833e0f8c1c83d449a7478aa59c036a9165498 #v3.29.5
|
||||
uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 #v3.29.5
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -72,4 +72,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@3c3833e0f8c1c83d449a7478aa59c036a9165498 #v3.29.5
|
||||
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 #v3.29.5
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -9,16 +9,35 @@ on:
|
||||
version:
|
||||
description: tag the latest commit on main with the given version (prefixed with v)
|
||||
required: true
|
||||
phase:
|
||||
description: the specific workflow phase to run or all
|
||||
required: true
|
||||
default: "all"
|
||||
type: choice
|
||||
options:
|
||||
- "all"
|
||||
- "install-script-only"
|
||||
|
||||
jobs:
|
||||
quality-gate:
|
||||
environment: release
|
||||
runs-on: ubuntu-24.04
|
||||
if: ${{ github.event.inputs.phase == 'all' }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
|
||||
- name: Validate Apple notarization credentials
|
||||
run: .tool/quill submission list
|
||||
env:
|
||||
QUILL_NOTARY_ISSUER: ${{ secrets.APPLE_NOTARY_ISSUER }}
|
||||
QUILL_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }}
|
||||
QUILL_NOTARY_KEY: ${{ secrets.APPLE_NOTARY_KEY }}
|
||||
|
||||
- name: Check if running on main
|
||||
if: github.ref != 'refs/heads/main'
|
||||
# we are using the following flag when running `cosign blob-verify` for checksum signature verification:
|
||||
@@ -110,13 +129,14 @@ jobs:
|
||||
release:
|
||||
needs: [quality-gate]
|
||||
runs-on: ubuntu-24.04
|
||||
if: ${{ github.event.inputs.phase == 'all' }}
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
# required for goreleaser signs section with cosign
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
@@ -161,13 +181,13 @@ jobs:
|
||||
# for updating brew formula in anchore/homebrew-syft
|
||||
GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }}
|
||||
|
||||
- uses: anchore/sbom-action@da167eac915b4e86f08b264dbdbc867b61be6f0c #v0.20.5
|
||||
- uses: anchore/sbom-action@43a17d6e7add2b5535efe4dcae9952337c479a93 #v0.20.11
|
||||
continue-on-error: true
|
||||
with:
|
||||
file: go.mod
|
||||
artifact-name: sbom.spdx.json
|
||||
|
||||
- uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e #v3.18.0
|
||||
- uses: 8398a7/action-slack@77eaa4f1c608a7d68b38af4e3f739dcd8cba273e #v3.19.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
@@ -179,7 +199,7 @@ jobs:
|
||||
|
||||
release-install-script:
|
||||
needs: [release]
|
||||
if: ${{ needs.release.result == 'success' }}
|
||||
if: ${{ always() && (needs.release.result == 'success' || github.event.inputs.phase == 'install-script-only') }}
|
||||
uses: "anchore/workflows/.github/workflows/release-install-script.yaml@main"
|
||||
with:
|
||||
tag: ${{ github.event.inputs.version }}
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
permissions:
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'anchore' # only run for main repo (not forks)
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -31,13 +31,13 @@ jobs:
|
||||
with:
|
||||
repos: ${{ github.event.inputs.repos }}
|
||||
|
||||
- uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0
|
||||
- uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf #v2.2.1
|
||||
id: generate-token
|
||||
with:
|
||||
app_id: ${{ secrets.TOKEN_APP_ID }}
|
||||
private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
|
||||
app-id: ${{ secrets.TOKEN_APP_ID }}
|
||||
private-key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e #v7.0.8
|
||||
- uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 #v8.0.0
|
||||
with:
|
||||
signoff: true
|
||||
delete-branch: true
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'anchore/syft' # only run for main repo
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -45,13 +45,13 @@ jobs:
|
||||
echo "\`\`\`"
|
||||
} >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0
|
||||
- uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf #v2.2.1
|
||||
id: generate-token
|
||||
with:
|
||||
app_id: ${{ secrets.TOKEN_APP_ID }}
|
||||
private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
|
||||
app-id: ${{ secrets.TOKEN_APP_ID }}
|
||||
private-key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e #v7.0.8
|
||||
- uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 #v8.0.0
|
||||
with:
|
||||
signoff: true
|
||||
delete-branch: true
|
||||
|
||||
@@ -14,28 +14,45 @@ env:
|
||||
jobs:
|
||||
upgrade-cpe-dictionary-index:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
if: github.repository == 'anchore/syft' # only run for main repo
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
id: bootstrap
|
||||
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
- name: Login to GitHub Container Registry
|
||||
run: echo "${{ secrets.GITHUB_TOKEN }}" | .tool/oras login ghcr.io -u "$ACTOR" --password-stdin
|
||||
env:
|
||||
ACTOR: ${{ github.actor }}
|
||||
|
||||
- run: |
|
||||
make generate-cpe-dictionary-index
|
||||
- name: Pull CPE cache from registry
|
||||
run: make generate:cpe-index:cache:pull
|
||||
|
||||
- uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0
|
||||
- name: Update CPE cache from NVD API
|
||||
run: make generate:cpe-index:cache:update
|
||||
env:
|
||||
NVD_API_KEY: ${{ secrets.NVD_API_KEY }}
|
||||
|
||||
- name: Generate CPE dictionary index
|
||||
run: make generate:cpe-index:build
|
||||
|
||||
- name: Push updated CPE cache to registry
|
||||
run: make generate:cpe-index:cache:push
|
||||
|
||||
- uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf #v2.2.1
|
||||
id: generate-token
|
||||
with:
|
||||
app_id: ${{ secrets.TOKEN_APP_ID }}
|
||||
private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
|
||||
app-id: ${{ secrets.TOKEN_APP_ID }}
|
||||
private-key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e #v7.0.8
|
||||
- uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 #v8.0.0
|
||||
with:
|
||||
signoff: true
|
||||
delete-branch: true
|
||||
@@ -47,7 +64,7 @@ jobs:
|
||||
Update CPE dictionary index based on the latest available CPE dictionary
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e #v3.18.0
|
||||
- uses: 8398a7/action-slack@77eaa4f1c608a7d68b38af4e3f739dcd8cba273e #v3.19.0
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
fields: workflow,eventName,job
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
name: PR to update SPDX license list
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 6 * * 1" # every monday at 6 AM UTC
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
SLACK_NOTIFICATIONS: true
|
||||
|
||||
jobs:
|
||||
upgrade-spdx-license-list:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'anchore/syft' # only run for main repo
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
|
||||
- run: |
|
||||
make generate-license-list
|
||||
|
||||
- uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0
|
||||
id: generate-token
|
||||
with:
|
||||
app_id: ${{ secrets.TOKEN_APP_ID }}
|
||||
private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 #v8.0.0
|
||||
with:
|
||||
signoff: true
|
||||
delete-branch: true
|
||||
branch: auto/latest-spdx-license-list
|
||||
labels: dependencies
|
||||
commit-message: "chore(deps): update SPDX license list"
|
||||
title: "chore(deps): update SPDX license list"
|
||||
body: |
|
||||
Update SPDX license list based on the latest available list from spdx.org
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- uses: 8398a7/action-slack@77eaa4f1c608a7d68b38af4e3f739dcd8cba273e #v3.19.0
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
fields: workflow,eventName,job
|
||||
text: Syft SPDX license list update failed
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
|
||||
if: ${{ failure() && env.SLACK_NOTIFICATIONS == 'true' }}
|
||||
@@ -23,12 +23,12 @@ jobs:
|
||||
contents: read
|
||||
security-events: write # for uploading SARIF results
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run zizmor"
|
||||
uses: zizmorcore/zizmor-action@5ca5fc7a4779c5263a3ffa0e1f693009994446d1 # v0.1.2
|
||||
uses: zizmorcore/zizmor-action@e673c3917a1aef3c65c972347ed84ccd013ecda4 # v0.2.0
|
||||
with:
|
||||
config-file: .github/zizmor.yml
|
||||
sarif-upload: true
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
name: "Validations"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
@@ -17,7 +21,7 @@ jobs:
|
||||
name: "Static analysis"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -34,7 +38,7 @@ jobs:
|
||||
# we need more storage than what's on the default runner
|
||||
runs-on: ubuntu-22.04-4core-16gb
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -52,7 +56,7 @@ jobs:
|
||||
name: "Integration tests"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -72,7 +76,7 @@ jobs:
|
||||
name: "Build snapshot artifacts"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -90,7 +94,7 @@ jobs:
|
||||
# why not use actions/upload-artifact? It is very slow (3 minutes to upload ~600MB of data, vs 10 seconds with this approach).
|
||||
# see https://github.com/actions/upload-artifact/issues/199 for more info
|
||||
- name: Upload snapshot artifacts
|
||||
uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 #v4.2.4
|
||||
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb #v5.0.1
|
||||
with:
|
||||
# we need to preserve the snapshot data itself as well as the task data that confirms if the
|
||||
# snapshot build is stale or not. Otherwise the downstream jobs will attempt to rebuild the snapshot
|
||||
@@ -107,7 +111,7 @@ jobs:
|
||||
needs: [Build-Snapshot-Artifacts]
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -118,7 +122,7 @@ jobs:
|
||||
|
||||
- name: Download snapshot build
|
||||
id: snapshot-cache
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 #v4.2.4
|
||||
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb #v5.0.1
|
||||
with:
|
||||
path: |
|
||||
snapshot
|
||||
@@ -133,7 +137,7 @@ jobs:
|
||||
|
||||
- run: npm install @actions/artifact@2.2.2
|
||||
|
||||
- uses: actions/github-script@v7
|
||||
- uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const { readdirSync } = require('fs')
|
||||
@@ -164,7 +168,7 @@ jobs:
|
||||
needs: [Build-Snapshot-Artifacts]
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -175,7 +179,7 @@ jobs:
|
||||
|
||||
- name: Download snapshot build
|
||||
id: snapshot-cache
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 #v4.2.4
|
||||
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb #v5.0.1
|
||||
with:
|
||||
path: |
|
||||
snapshot
|
||||
@@ -210,9 +214,9 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
|
||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -225,7 +229,7 @@ jobs:
|
||||
|
||||
- name: Download snapshot build
|
||||
id: snapshot-cache
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 #v4.2.4
|
||||
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb #v5.0.1
|
||||
with:
|
||||
path: |
|
||||
snapshot
|
||||
@@ -251,7 +255,7 @@ jobs:
|
||||
needs: [Build-Snapshot-Artifacts]
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -262,7 +266,7 @@ jobs:
|
||||
|
||||
- name: Download snapshot build
|
||||
id: snapshot-cache
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 #v4.2.4
|
||||
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb #v5.0.1
|
||||
with:
|
||||
path: |
|
||||
snapshot
|
||||
@@ -291,7 +295,7 @@ jobs:
|
||||
- Cli-Linux
|
||||
- Upload-Snapshot-Artifacts
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
go.work
|
||||
go.work.sum
|
||||
.tool-versions
|
||||
.python-version
|
||||
|
||||
# app configuration
|
||||
/.syft.yaml
|
||||
@@ -16,6 +17,8 @@ bin/
|
||||
/snapshot
|
||||
/.tool
|
||||
/.task
|
||||
/generate
|
||||
/specs
|
||||
|
||||
# changelog generation
|
||||
CHANGELOG.md
|
||||
@@ -70,3 +73,5 @@ cosign.pub
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
|
||||
|
||||
@@ -49,6 +49,12 @@ linters:
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
# internal/os contains OS feature detection logic; the name reflects its purpose
|
||||
- linters:
|
||||
- revive
|
||||
path: internal/os/
|
||||
text: "var-naming: avoid package names that conflict"
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
|
||||
+2
-1
@@ -42,7 +42,7 @@ builds:
|
||||
dir: ./cmd/syft
|
||||
binary: syft
|
||||
goos: [windows]
|
||||
goarch: [amd64]
|
||||
goarch: [amd64, arm64]
|
||||
mod_timestamp: *build-timestamp
|
||||
ldflags: *build-ldflags
|
||||
|
||||
@@ -337,6 +337,7 @@ signs:
|
||||
certificate: "${artifact}.pem"
|
||||
args:
|
||||
- "sign-blob"
|
||||
- "--use-signing-config=false"
|
||||
- "--oidc-issuer=https://token.actions.githubusercontent.com"
|
||||
- "--output-certificate=${certificate}"
|
||||
- "--output-signature=${signature}"
|
||||
|
||||
+3
-126
@@ -1,128 +1,5 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
# Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
All contributors for any Anchore project must follow the [Contributor Covenant Code of Conduct](https://oss.anchore.com/docs/contributing/code-of-conduct/).
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
[opensource@anchore.com](mailto:opensource@anchore.com).
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
**TLDR:** Be kind, be respectful, and assume good intentions. We're all here to build great software together.
|
||||
|
||||
+7
-148
@@ -1,154 +1,13 @@
|
||||
[#](#) Contributing to Syft
|
||||
# Contributing
|
||||
|
||||
If you are looking to contribute to this project and want to open a GitHub pull request ("PR"), there are a few guidelines of what we are looking for in patches. Make sure you go through this document and ensure that your code proposal is aligned.
|
||||
Thank you for your interest in contributing to Syft!
|
||||
|
||||
## Setting up your environment
|
||||
Please see the [contribution guide](https://oss.anchore.com/docs/contributing/syft/) for development requirements and helpful tips to get started developing in the repo. For a deeper dive, please see the [architecture docs](https://oss.anchore.com/docs/architecture/syft/).
|
||||
|
||||
Before you can contribute to Syft, you need to configure your development environment.
|
||||
**Have a question or need help?** Check out our [issues and discussions guide](https://oss.anchore.com/docs/contributing/issues-and-discussions/) to find the right place to start a conversation.
|
||||
|
||||
### Debian setup
|
||||
**Ready to submit code?** Our [pull request guide](https://oss.anchore.com/docs/contributing/pull-requests/) covers everything from title conventions to the review process. Don't forget that ***all commits require a [sign-off](https://oss.anchore.com/docs/contributing/sign-off/)***.
|
||||
|
||||
You will need to install Go. The version on https://go.dev works best, using the system golang doesn't always work the way you might expect.
|
||||
**Found a security issue?** Please do **not** open a public issue. Instead, see our [security policy](https://oss.anchore.com/docs/contributing/security/) for how to report vulnerabilities responsibly.
|
||||
|
||||
Refer to the go.mod file in the root of this repo for the recommended version of Go to install.
|
||||
|
||||
You will also need Docker. There's no reason the system packages shouldn't work, but we used the official Docker package. You can find instructions for installing Docker in Debian [here](https://docs.docker.com/engine/install/debian/).
|
||||
|
||||
You also need to install some Debian packages
|
||||
|
||||
```sh
|
||||
sudo apt-get install build-essential zip bc libxml2-utils git
|
||||
```
|
||||
|
||||
## Configuring Git
|
||||
|
||||
You will need to configure your git client with your name and email address. This is easily done from the command line.
|
||||
|
||||
```text
|
||||
$ git config --global user.name "John Doe"
|
||||
$ git config --global user.email "john.doe@example.com"
|
||||
```
|
||||
|
||||
This username and email address will matter later in this guide.
|
||||
|
||||
## Fork the repo
|
||||
|
||||
You should fork the Syft repo using the "Fork" button at the top right of the Syft GitHub [site](https://github.com/anchore/syft/). You will be doing your development in your fork, then submit a pull request to Syft. There are many resources how to use GitHub effectively, we will not cover those here.
|
||||
|
||||
## Adding a feature or fix
|
||||
|
||||
If you look at the Syft [Issue](https://github.com/anchore/syft/issues) there are plenty of bugs and feature requests. Maybe look at the [good first issue](https://github.com/anchore/syft/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) list if you're not sure where to start.
|
||||
|
||||
## Commit guidelines
|
||||
|
||||
In the Syft project we like commits and pull requests (PR) to be easy to understand and review. Open source thrives best when everything happening is over documented and small enough to be understood.
|
||||
|
||||
### Granular commits
|
||||
|
||||
Please try to make every commit as simple as possible, but no simpler. The idea is that each commit should be a logical unit of code. Try not to commit too many tiny changes, for example every line changed in a file as a separate commit. And also try not to make a commit enormous, for example committing all your work at the end of the day.
|
||||
|
||||
Rather than try to follow a strict guide on what is or is not best, we try to be flexible and simple in this space. Do what makes the most sense for the changes you are trying to include.
|
||||
|
||||
### Commit title and description
|
||||
|
||||
Remember that the message you leave for a commit is for the reviewer in the present, and for someone (maybe you) changing something in the future. Please make sure the title and description used is easy to understand and explains what was done. Jokes and clever comments generally don't age well in commit messages. Just the facts please.
|
||||
|
||||
## Sign off your work
|
||||
|
||||
The `sign-off` is an added line at the end of the explanation for the commit, certifying that you wrote it or otherwise have the right to submit it as an open-source patch. By submitting a contribution, you agree to be bound by the terms of the DCO Version 1.1 and Apache License Version 2.0.
|
||||
|
||||
Signing off a commit certifies the below Developer's Certificate of Origin (DCO):
|
||||
|
||||
```text
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
```
|
||||
|
||||
All contributions to this project are licensed under the [Apache License Version 2.0, January 2004](http://www.apache.org/licenses/).
|
||||
|
||||
When committing your change, you can add the required line manually so that it looks like this:
|
||||
|
||||
```text
|
||||
Signed-off-by: John Doe <john.doe@example.com>
|
||||
```
|
||||
|
||||
Creating a signed-off commit is then possible with `-s` or `--signoff`:
|
||||
|
||||
```text
|
||||
$ git commit -s -m "this is a commit message"
|
||||
```
|
||||
|
||||
To double-check that the commit was signed-off, look at the log output:
|
||||
|
||||
```text
|
||||
$ git log -1
|
||||
commit 37ceh170e4hb283bb73d958f2036ee5k07e7fde7 (HEAD -> issue-35, origin/main, main)
|
||||
Author: John Doe <john.doe@example.com>
|
||||
Date: Mon Aug 1 11:27:13 2020 -0400
|
||||
|
||||
this is a commit message
|
||||
|
||||
Signed-off-by: John Doe <john.doe@example.com>
|
||||
```
|
||||
|
||||
## Test your changes
|
||||
|
||||
This project has a `Makefile` which includes many helpers running both unit and integration tests. You can run `make help` to see all the options. Although PRs will have automatic checks for these, it is useful to run them locally, ensuring they pass before submitting changes. Ensure you've bootstrapped once before running tests:
|
||||
|
||||
```text
|
||||
$ make bootstrap
|
||||
```
|
||||
|
||||
You only need to bootstrap once. After the bootstrap process, you can run the tests as many times as needed:
|
||||
|
||||
```text
|
||||
$ make unit
|
||||
$ make integration
|
||||
```
|
||||
|
||||
You can also run `make all` to run a more extensive test suite, but there is additional configuration that will be needed for those tests to run correctly. We will not cover the extra steps here.
|
||||
|
||||
## Pull Request
|
||||
|
||||
If you made it this far and all the tests are passing, it's time to submit a Pull Request (PR) for Syft. Submitting a PR is always a scary moment as what happens next can be an unknown. The Syft project strives to be easy to work with, we appreciate all contributions. Nobody is going to yell at you or try to make you feel bad. We love contributions and know how scary that first PR can be.
|
||||
|
||||
### PR Title and Description
|
||||
|
||||
Just like the commit title and description mentioned above, the PR title and description is very important for letting others know what's happening. Please include any details you think a reviewer will need to more properly review your PR.
|
||||
|
||||
A PR that is very large or poorly described has a higher likelihood of being pushed to the end of the list. Reviewers like PRs they can understand and quickly review.
|
||||
|
||||
### What to expect next
|
||||
|
||||
Please be patient with the project. We try to review PRs in a timely manner, but this is highly dependent on all the other tasks we have going on. It's OK to ask for a status update every week or two, it's not OK to ask for a status update every day.
|
||||
|
||||
It's very likely the reviewer will have questions and suggestions for changes to your PR. If your changes don't match the current style and flow of the other code, expect a request to change what you've done.
|
||||
|
||||
## Document your changes
|
||||
|
||||
And lastly, when proposed changes are modifying user-facing functionality or output, it is expected the PR will include updates to the documentation as well. Syft is not a project that is heavy on documentation. This will mostly be updating the README and help for the tool.
|
||||
|
||||
If nobody knows new features exist, they can't use them!
|
||||
**Want to help improve the docs?** Check out the [anchore/oss-docs](https://github.com/anchore/oss-docs) repository.
|
||||
|
||||
-392
@@ -1,392 +0,0 @@
|
||||
# Developing
|
||||
|
||||
## Getting started
|
||||
|
||||
In order to test and develop in this repo you will need the following dependencies installed:
|
||||
- Golang
|
||||
- docker
|
||||
- make
|
||||
- Python (>= 3.9)
|
||||
|
||||
### Docker settings for getting started
|
||||
Make sure you've updated your docker settings so the default docker socket path is available.
|
||||
|
||||
Go to:
|
||||
|
||||
docker -> settings -> advanced
|
||||
|
||||
Make sure:
|
||||
|
||||
```
|
||||
Allow the default Docker socket to be used
|
||||
```
|
||||
|
||||
is checked.
|
||||
|
||||
Also double check that the docker context being used is the default context. If it is not, run:
|
||||
|
||||
`docker context use default`
|
||||
|
||||
After cloning the following step can help you get setup:
|
||||
1. run `make bootstrap` to download go mod dependencies, create the `/.tmp` dir, and download helper utilities.
|
||||
2. run `make` to view the selection of developer commands in the Makefile
|
||||
3. run `make build` to build the release snapshot binaries and packages
|
||||
4. for an even quicker start you can run `go run cmd/syft/main.go` to print the syft help.
|
||||
- this command `go run cmd/syft/main.go alpine:latest` will compile and run syft against `alpine:latest`
|
||||
5. view the README or syft help output for more output options
|
||||
|
||||
The main make tasks for common static analysis and testing are `lint`, `format`, `lint-fix`, `unit`, `integration`, and `cli`.
|
||||
|
||||
See `make help` for all the current make tasks.
|
||||
|
||||
### Internal Artifactory Settings
|
||||
|
||||
**Not always applicable**
|
||||
|
||||
Some companies have Artifactory setup internally as a solution for sourcing secure dependencies.
|
||||
If you're seeing an issue where the unit tests won't run because of the below error then this section might be relevant for your use case.
|
||||
|
||||
```
|
||||
[ERROR] [ERROR] Some problems were encountered while processing the POMs
|
||||
```
|
||||
|
||||
If you're dealing with an issue where the unit tests will not pull/build certain java fixtures check some of these settings:
|
||||
|
||||
- a `settings.xml` file should be available to help you communicate with your internal artifactory deployment
|
||||
- this can be moved to `syft/pkg/cataloger/java/test-fixtures/java-builds/example-jenkins-plugin/` to help build the unit test-fixtures
|
||||
- you'll also want to modify the `build-example-jenkins-plugin.sh` to use `settings.xml`
|
||||
|
||||
For more information on this setup and troubleshooting see [issue 1895](https://github.com/anchore/syft/issues/1895#issuecomment-1610085319)
|
||||
|
||||
|
||||
## Architecture
|
||||
|
||||
At a high level, this is the package structure of syft:
|
||||
```
|
||||
./cmd/syft/
|
||||
│ ├── cli/
|
||||
│ │ ├── cli.go // where all commands are wired up
|
||||
│ │ ├── commands/ // all command implementations
|
||||
│ │ ├── options/ // all command flags and configuration options
|
||||
│ │ └── ui/ // all handlers for events that are shown on the UI
|
||||
│ └── main.go // entrypoint for the application
|
||||
└── syft/ // the "core" syft library
|
||||
├── format/ // contains code to encode or decode to and from SBOM formats
|
||||
├── pkg/ // contains code to catalog packages from a source
|
||||
├── sbom/ // contains the definition of an SBOM
|
||||
└── source/ // contains code to create a source object for some input type (e.g. container image, directory, etc)
|
||||
```
|
||||
|
||||
Syft's core library is implemented in the `syft` package and subpackages, where the major packages are:
|
||||
|
||||
- the `syft/source` package produces a `source.Source` object that can be used to catalog a directory, container, and other source types.
|
||||
- the `syft` package contains a single function that can take a `source.Source` object and catalog it, producing an `sbom.SBOM` object
|
||||
- the `syft/format` package contains the ability to encode and decode SBOMs to and from different SBOM formats (such as SPDX and CycloneDX)
|
||||
|
||||
The `cmd` package at the highest level execution flow wires up [`spf13/cobra`](https://github.com/spf13/cobra) commands for execution in the main application:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant main as cmd/syft/main
|
||||
participant cli as cli.New()
|
||||
participant root as root.Execute()
|
||||
participant cmd as <command>.Execute()
|
||||
|
||||
main->>+cli:
|
||||
|
||||
Note right of cli: wire ALL CLI commands
|
||||
Note right of cli: add flags for ALL commands
|
||||
|
||||
cli-->>-main: root command
|
||||
|
||||
main->>+root:
|
||||
root->>+cmd:
|
||||
cmd-->>-root: (error)
|
||||
|
||||
root-->>-main: (error)
|
||||
|
||||
Note right of cmd: Execute SINGLE command from USER
|
||||
```
|
||||
|
||||
The `packages` command uses the core library to generate an SBOM for the given user input:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant source as source.New(ubuntu:latest)
|
||||
participant sbom as sbom.SBOM
|
||||
participant catalog as syft.CatalogPackages(src)
|
||||
participant encoder as syft.Encode(sbom, format)
|
||||
|
||||
Note right of source: use "ubuntu:latest" as SBOM input
|
||||
|
||||
source-->>+sbom: add source to SBOM struct
|
||||
source-->>+catalog: pass src to generate catalog
|
||||
catalog-->-sbom: add cataloging results onto SBOM
|
||||
sbom-->>encoder: pass SBOM and format desiered to syft encoder
|
||||
encoder-->>source: return bytes that are the SBOM of the original input
|
||||
|
||||
Note right of catalog: cataloger configuration is done based on src
|
||||
```
|
||||
|
||||
Additionally, here is a [gist of using syft as a library](https://gist.github.com/spiffcs/3027638b7ba904d07e482a712bc00d3d) to generate a SBOM for a docker image.
|
||||
|
||||
|
||||
### `pkg.Package` object
|
||||
|
||||
The `pkg.Package` object is a core data structure that represents a software package. Fields like `name` and `version` probably don't need
|
||||
a detailed explanation, but some of the other fields are worth a quick overview:
|
||||
|
||||
- `FoundBy`: the name of the cataloger that discovered this package (e.g. `python-pip-cataloger`).
|
||||
- `Locations`: these are the set of paths and layer ids that were parsed to discover this package (e.g. `python-pip-cataloger`).
|
||||
- `Language`: the language of the package (e.g. `python`).
|
||||
- `Type`: this is a high-level categorization of the ecosystem the package resides in. For instance, even if the package is a egg, wheel, or requirements.txt reference, it is still logically a "python" package. Not all package types align with a language (e.g. `rpm`) but it is common.
|
||||
- `Metadata`: specialized data for specific location(s) parsed. We should try and raise up as much raw information that seems useful. As a rule of thumb the object here should be as flat as possible and use the raw names and values from the underlying source material parsed.
|
||||
|
||||
When `pkg.Package` is serialized an additional `MetadataType` is shown. This is a label that helps consumers understand the datashape of the `Metadata` field.
|
||||
|
||||
By convention the `MetadataType` value should follow these rules of thumb:
|
||||
|
||||
- Only use lowercase letters, numbers, and hyphens. Use hyphens to separate words.
|
||||
- **Try to anchor the name in the ecosystem, language, or packaging tooling it belongs to**. For a package manager for a language ecosystem the language, framework or runtime should be used as a prefix. For instance `pubspec-lock` is an OK name, but `dart-pubspec-lock` is better. For an OS package manager this is not necessary (e.g. `apk-db-entry` is a good name, but `alpine-apk-db-entry` is not since `alpine` and the `a` in `apk` is redundant).
|
||||
- **Be as specific as possible to what the data represents**. For instance `ruby-gem` is NOT a good `MetadataType` value, but `ruby-gemspec` is. Why? Ruby gem information can come from a gemspec file or a Gemfile.lock, which are very different. The latter name provides more context as to what to expect.
|
||||
- **Should describe WHAT the data is, NOT HOW it's used**. For instance `r-description-installed-file` is NOT a good `MetadataType` value since it's trying to convey that we use the DESCRIPTION file in the R ecosystem to detect installed packages. Instead simply describe what the DESCRIPTION file is itself without context of how it's used: `r-description`.
|
||||
- **Use the `lock` suffix** to distinct between manifest files that loosely describe package version requirements vs files that strongly specify one and only one version of a package ("lock" files). These should only be used with respect to package managers that have the guide and lock distinction, but would not be appropriate otherwise (e.g. `rpm` does not have a guide vs lock, so `lock` should NOT be used to describe a db entry).
|
||||
- **Use the `archive` suffix to indicate a package archive** (e.g. rpm file, apk file, etc) that describes the contents of the package. For example an RPM file that was cataloged would have a `rpm-archive` metadata type (not to be confused with an RPM DB record entry which would be `rpm-db-entry`).
|
||||
- **Use the `entry` suffix** to indicate information about a package that was found as a single entry within file that has multiple package entries. If the entry was found within a DB or a flat-file store for an OS package manager, you should use `db-entry`.
|
||||
- **Should NOT contain the phrase `package`**, though exceptions are allowed (say if the canonical name literally has the phrase package in it).
|
||||
- **Should NOT contain have a `file` suffix** unless the canonical name has the term "file", such as a `pipfile` or `gemfile`. An example of a bad name for this rule is`ruby-gemspec-file`; a better name would be `ruby-gemspec`.
|
||||
- **Should NOT contain the exact filename+extensions**. For instance `pipfile.lock` shouldn't really be in the name, instead try and describe what the file is: `python-pipfile-lock` (but shouldn't this be `python-pip-lock` you might ask? No, since the `pip` package manger is not related to the `pipfile` project).
|
||||
- **Should NOT contain the phrase `metadata`**, unless the canonical name has this term.
|
||||
- **Should represent a single use case**. For example, trying to describe Hackage metadata with a single `HackageMetadata` struct (and thus `MetadataType`) is not allowed since it represents 3 mutually exclusive use cases: representing a `stack.yaml`, `stack.lock`, or `cabal.project` file. Instead, each of these should have their own struct types and `MetadataType` values.
|
||||
|
||||
There are other cases that are not covered by these rules... and that's ok! The goal is to provide a consistent naming scheme that is easy to understand and use when it's applicable. If the rules do not exactly apply in your situation then just use your best judgement (or amend these rules as needed whe new common cases come up).
|
||||
|
||||
What if the underlying parsed data represents multiple files? There are two approaches to this:
|
||||
- use the primary file to represent all the data. For instance, though the `dpkg-cataloger` looks at multiple files to get all information about a package, it's the `status` file that gets represented.
|
||||
- nest each individual file's data under the `Metadata` field. For instance, the `java-archive-cataloger` may find information from on or all of the files: `pom.xml`, `pom.properties`, and `MANIFEST.MF`. However, the metadata is simply `java-metadata' with each possibility as a nested optional field.
|
||||
|
||||
### Syft Catalogers
|
||||
|
||||
Catalogers are the way in which syft is able to identify and construct packages given a set a targeted list of files.
|
||||
For example, a cataloger can ask syft for all `package-lock.json` files in order to parse and raise up javascript packages
|
||||
(see [how file globs](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/javascript/cataloger.go#L16-L21) and
|
||||
[file parser functions](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/javascript/cataloger.go#L16-L21) are used
|
||||
for a quick example).
|
||||
|
||||
From a high level catalogers have the following properties:
|
||||
|
||||
- _They are independent from one another_. The java cataloger has no idea of the processes, assumptions, or results of the python cataloger, for example.
|
||||
|
||||
- _They do not know what source is being analyzed_. Are we analyzing a local directory? an image? if so, the squashed representation or all layers? The catalogers do not know the answers to these questions. Only that there is an interface to query for file paths and contents from an underlying "source" being scanned.
|
||||
|
||||
- _Packages created by the cataloger should not be mutated after they are created_. There is one exception made for adding CPEs to a package after the cataloging phase, but that will most likely be moved back into the cataloger in the future.
|
||||
|
||||
|
||||
Cataloger names should be unique and named with the following rules of thumb in mind:
|
||||
|
||||
- Must end with `-cataloger`
|
||||
- Use lowercase letters, numbers, and hyphens only
|
||||
- Use hyphens to separate words
|
||||
- Catalogers for language ecosystems should start with the language name (e.g. `python-` for a cataloger that raises up python packages)
|
||||
- Distinct between when the cataloger is searching for evidence of installed packages vs declared packages. For example, there are currently two different gemspec-based catalogers, the `ruby-gemspec-cataloger` and `ruby-installed-gemspec-cataloger`, where the latter requires that the gemspec is found within a `specifications` directory (which means it was installed, not just at the root of a source repo).
|
||||
|
||||
#### Building a new Cataloger
|
||||
|
||||
Catalogers must fulfill the [`pkg.Cataloger` interface](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger.go) in order to add packages to the SBOM.
|
||||
All catalogers should be added to:
|
||||
- the [global list of catalogers](https://github.com/anchore/syft/blob/9995950c70e849f9921919faffbfcf46401f71f3/syft/pkg/cataloger/cataloger.go#L92-L125)
|
||||
- at least one source-specific list, today the two lists are [directory catalogers and image catalogers](https://github.com/anchore/syft/blob/9995950c70e849f9921919faffbfcf46401f71f3/syft/pkg/cataloger/cataloger.go#L39-L89)
|
||||
|
||||
For reference, catalogers are [invoked within syft](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/catalog.go#L41-L100) one after the other, and can be invoked in parallel.
|
||||
|
||||
`generic.NewCataloger` is an abstraction syft used to make writing common components easier (see the [apkdb cataloger](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/apkdb/cataloger.go) for example usage).
|
||||
It takes the following information as input:
|
||||
- A `catalogerName` to identify the cataloger uniquely among all other catalogers.
|
||||
- Pairs of file globs as well as parser functions to parse those files. These parser functions return a slice of [`pkg.Package`](https://github.com/anchore/syft/blob/9995950c70e849f9921919faffbfcf46401f71f3/syft/pkg/package.go#L19) as well as a slice of [`artifact.Relationship`](https://github.com/anchore/syft/blob/9995950c70e849f9921919faffbfcf46401f71f3/syft/artifact/relationship.go#L31) to describe how the returned packages are related. See this [the apkdb cataloger parser function](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/apkdb/parse_apk_db.go#L22-L102) as an example.
|
||||
|
||||
Identified packages share a common `pkg.Package` struct so be sure that when the new cataloger is constructing a new package it is using the [`Package` struct](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/package.go#L16-L31).
|
||||
If you want to return more information than what is available on the `pkg.Package` struct then you can do so in the `pkg.Package.Metadata` section of the struct, which is unique for each [`pkg.Type`](https://github.com/anchore/syft/blob/v0.70.0/syft/pkg/type.go).
|
||||
See [the `pkg` package](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg) for examples of the different metadata types that are supported today.
|
||||
These are plugged into the `MetadataType` and `Metadata` fields in the above struct. `MetadataType` informs which type is being used. `Metadata` is an interface converted to that type.
|
||||
|
||||
Finally, here is an example of where the package construction is done within the apk cataloger:
|
||||
- [Calling the APK package constructor from the parser function](https://github.com/anchore/syft/blob/v0.70.0/syft/pkg/cataloger/apkdb/parse_apk_db.go#L106)
|
||||
- [The APK package constructor itself](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/apkdb/package.go#L12-L27)
|
||||
|
||||
Interested in building a new cataloger? Checkout the [list of issues with the `new-cataloger` label](https://github.com/anchore/syft/issues?q=is%3Aopen+is%3Aissue+label%3Anew-cataloger+no%3Aassignee)!
|
||||
If you have questions about implementing a cataloger feel free to file an issue or reach out to us [on discourse](https://anchore.com/discourse)!
|
||||
|
||||
|
||||
#### Searching for files
|
||||
|
||||
All catalogers are provided an instance of the [`file.Resolver`](https://github.com/anchore/syft/blob/v0.70.0/syft/source/file_resolver.go#L8) to interface with the image and search for files. The implementations for these
|
||||
abstractions leverage [`stereoscope`](https://github.com/anchore/stereoscope) in order to perform searching. Here is a
|
||||
rough outline how that works:
|
||||
|
||||
1. a stereoscope `file.Index` is searched based on the input given (a path, glob, or MIME type). The index is relatively fast to search, but requires results to be filtered down to the files that exist in the specific layer(s) of interest. This is done automatically by the `filetree.Searcher` abstraction. This abstraction will fallback to searching directly against the raw `filetree.FileTree` if the index does not contain the file(s) of interest. Note: the `filetree.Searcher` is used by the `file.Resolver` abstraction.
|
||||
2. Once the set of files are returned from the `filetree.Searcher` the results are filtered down further to return the most unique file results. For example, you may have requested for files by a glob that returns multiple results. These results are filtered down to deduplicate by real files, so if a result contains two references to the same file, say one accessed via symlink and one accessed via the real path, then the real path reference is returned and the symlink reference is filtered out. If both were accessed by symlink then the first (by lexical order) is returned. This is done automatically by the `file.Resolver` abstraction.
|
||||
3. By the time results reach the `pkg.Cataloger` you are guaranteed to have a set of unique files that exist in the layer(s) of interest (relative to what the resolver supports).
|
||||
|
||||
## Testing
|
||||
|
||||
### Testing commands
|
||||
|
||||
* `make help` shows a list of available commands
|
||||
* `make unit`, `make integration`, `make cli`, and `make acceptance` run those test suites (see below)
|
||||
* `make test` runs all those tests (and is therefore pretty slow)
|
||||
* `make fixtures` clears and re-fetches all test fixtures.
|
||||
* `go test ./syft/pkg/` for example can test particular packages, assuming fixtures are already made
|
||||
* `make clean-cache` cleans all test cache. Note that subsequent test runs will be slower after this
|
||||
|
||||
|
||||
### Levels of testing
|
||||
|
||||
- `unit`: The default level of test which is distributed throughout the repo are unit tests. Any `_test.go` file that
|
||||
does not reside somewhere within the `/test` directory is a unit test. Other forms of testing should be organized in
|
||||
the `/test` directory. These tests should focus on correctness of functionality in depth. % test coverage metrics
|
||||
only considers unit tests and no other forms of testing.
|
||||
|
||||
- `integration`: located within `cmd/syft/internal/test/integration`, these tests focus on the behavior surfaced by the common library
|
||||
entrypoints from the `syft` package and make light assertions about the results surfaced. Additionally, these tests
|
||||
tend to make diversity assertions for enum-like objects, ensuring that as enum values are added to a definition
|
||||
that integration tests will automatically fail if no test attempts to use that enum value. For more details see
|
||||
the "Data diversity and freshness assertions" section below.
|
||||
|
||||
- `cli`: located with in `test/cli`, these are tests that test the correctness of application behavior from a
|
||||
snapshot build. This should be used in cases where a unit or integration test will not do or if you are looking
|
||||
for in-depth testing of code in the `cmd/` package (such as testing the proper behavior of application configuration,
|
||||
CLI switches, and glue code before syft library calls).
|
||||
|
||||
- `acceptance`: located within `test/compare` and `test/install`, these are smoke-like tests that ensure that application
|
||||
packaging and installation works as expected. For example, during release we provide RPM packages as a download
|
||||
artifact. We also have an accompanying RPM acceptance test that installs the RPM from a snapshot build and ensures the
|
||||
output of a syft invocation matches canned expected output. New acceptance tests should be added for each release artifact
|
||||
and architecture supported (when possible).
|
||||
|
||||
### Data diversity and freshness assertions
|
||||
|
||||
It is important that tests against the codebase are flexible enough to begin failing when they do not cover "enough"
|
||||
of the objects under test. "Cover" in this case does not mean that some percentage of the code has been executed
|
||||
during testing, but instead that there is enough diversity of data input reflected in testing relative to the
|
||||
definitions available.
|
||||
|
||||
For instance, consider an enum-like value like so:
|
||||
```go
|
||||
type Language string
|
||||
|
||||
const (
|
||||
Java Language = "java"
|
||||
JavaScript Language = "javascript"
|
||||
Python Language = "python"
|
||||
Ruby Language = "ruby"
|
||||
Go Language = "go"
|
||||
)
|
||||
```
|
||||
|
||||
Say we have a test that exercises all the languages defined today:
|
||||
|
||||
```go
|
||||
func TestCatalogPackages(t *testing.T) {
|
||||
testTable := []struct {
|
||||
// ... the set of test cases that test all languages
|
||||
}
|
||||
for _, test := range cases {
|
||||
t.Run(test.name, func (t *testing.T) {
|
||||
// use inputFixturePath and assert that syft.CatalogPackages() returns the set of expected Package objects
|
||||
// ...
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Where each test case has a `inputFixturePath` that would result with packages from each language. This test is
|
||||
brittle since it does not assert that all languages were exercised directly and future modifications (such as
|
||||
adding a new language) won't be covered by any test cases.
|
||||
|
||||
To address this the enum-like object should have a definition of all objects that can be used in testing:
|
||||
|
||||
```go
|
||||
type Language string
|
||||
|
||||
// const( Java Language = ..., ... )
|
||||
|
||||
var AllLanguages = []Language{
|
||||
Java,
|
||||
JavaScript,
|
||||
Python,
|
||||
Ruby,
|
||||
Go,
|
||||
Rust,
|
||||
}
|
||||
```
|
||||
|
||||
Allowing testing to automatically fail when adding a new language:
|
||||
|
||||
```go
|
||||
func TestCatalogPackages(t *testing.T) {
|
||||
testTable := []struct {
|
||||
// ... the set of test cases that (hopefully) covers all languages
|
||||
}
|
||||
|
||||
// new stuff...
|
||||
observedLanguages := strset.New()
|
||||
|
||||
for _, test := range cases {
|
||||
t.Run(test.name, func (t *testing.T) {
|
||||
// use inputFixturePath and assert that syft.CatalogPackages() returns the set of expected Package objects
|
||||
// ...
|
||||
|
||||
// new stuff...
|
||||
for _, actualPkg := range actual {
|
||||
observedLanguages.Add(string(actualPkg.Language))
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
// new stuff...
|
||||
for _, expectedLanguage := range pkg.AllLanguages {
|
||||
if !observedLanguages.Contains(expectedLanguage) {
|
||||
t.Errorf("failed to test language=%q", expectedLanguage)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is a better test since it will fail when someone adds a new language but fails to write a test case that should
|
||||
exercise that new language. This method is ideal for integration-level testing, where testing correctness in depth
|
||||
is not needed (that is what unit tests are for) but instead testing in breadth to ensure that units are well integrated.
|
||||
|
||||
A similar case can be made for data freshness; if the quality of the results will be diminished if the input data
|
||||
is not kept up to date then a test should be written (when possible) to assert any input data is not stale.
|
||||
|
||||
An example of this is the static list of licenses that is stored in `internal/spdxlicense` for use by the SPDX
|
||||
presenters. This list is updated and published periodically by an external group and syft can grab and update this
|
||||
list by running `go generate ./...` from the root of the repo.
|
||||
|
||||
An integration test has been written to grabs the latest license list version externally and compares that version
|
||||
with the version generated in the codebase. If they differ, the test fails, indicating to someone that there is an
|
||||
action needed to update it.
|
||||
|
||||
**_The key takeaway is to try and write tests that fail when data assumptions change and not just when code changes.**_
|
||||
|
||||
### Snapshot tests
|
||||
|
||||
The format objects make a lot of use of "snapshot" testing, where you save the expected output bytes from a call into the
|
||||
git repository and during testing make a comparison of the actual bytes from the subject under test with the golden
|
||||
copy saved in the repo. The "golden" files are stored in the `test-fixtures/snapshot` directory relative to the go
|
||||
package under test and should always be updated by invoking `go test` on the specific test file with a specific CLI
|
||||
update flag provided.
|
||||
|
||||
Many of the `Format` tests make use of this approach, where the raw SBOM report is saved in the repo and the test
|
||||
compares that SBOM with what is generated from the latest presenter code. The following command can be used to
|
||||
update the golden files for the various snapshot tests:
|
||||
|
||||
```bash
|
||||
make update-format-golden-files
|
||||
```
|
||||
|
||||
These flags are defined at the top of the test files that have tests that use the snapshot files.
|
||||
|
||||
Snapshot testing is only as good as the manual verification of the golden snapshot file saved to the repo! Be careful
|
||||
and diligent when updating these files.
|
||||
|
||||
|
||||
@@ -18,170 +18,74 @@
|
||||
|
||||

|
||||
|
||||
## Introduction
|
||||
|
||||
Syft is a powerful and easy-to-use open-source tool for generating Software Bill of Materials (SBOMs) for container images and filesystems. It provides detailed visibility into the packages and dependencies in your software, helping you manage vulnerabilities, license compliance, and software supply chain security.
|
||||
|
||||
Syft development is sponsored by [Anchore](https://anchore.com/), and is released under the [Apache-2.0 License](https://github.com/anchore/syft?tab=Apache-2.0-1-ov-file). For commercial support options with Syft or Grype, please [contact Anchore](https://get.anchore.com/contact/).
|
||||
|
||||
## Features
|
||||
- Generates SBOMs for container images, filesystems, archives, and more to discover packages and libraries
|
||||
- Supports OCI, Docker and [Singularity](https://github.com/sylabs/singularity) image formats
|
||||
- Linux distribution identification
|
||||
- Works seamlessly with [Grype](https://github.com/anchore/grype) (a fast, modern vulnerability scanner)
|
||||
- Able to create signed SBOM attestations using the [in-toto specification](https://github.com/in-toto/attestation/blob/main/spec/README.md)
|
||||
- Convert between SBOM formats, such as CycloneDX, SPDX, and Syft's own format.
|
||||
|
||||
- Generates SBOMs for **container images**, **filesystems**, **archives** (see the docs for a full list of [supported scan targets](https://oss.anchore.com/docs/guides/sbom/scan-targets/))
|
||||
- Supports dozens of packaging ecosystems (e.g. Alpine (apk), Debian (dpkg), RPM, Go, Python, Java, JavaScript, Ruby, Rust, PHP, .NET, and [many more](https://oss.anchore.com/docs/capabilities/all-packages/))
|
||||
- Supports OCI, Docker, [Singularity](https://github.com/sylabs/singularity), and [more image formats](https://oss.anchore.com/docs/guides/sbom/scan-targets/)
|
||||
- Works seamlessly with [Grype](https://github.com/anchore/grype) for vulnerability scanning
|
||||
- Multiple output formats (**CycloneDX**, **SPDX**, **Syft JSON**, and [more](https://oss.anchore.com/docs/guides/sbom/formats/)) including the ability to [convert between SBOM formats](https://oss.anchore.com/docs/guides/sbom/conversion/)
|
||||
- Create signed SBOM attestations using the [in-toto specification](https://github.com/in-toto/attestation/blob/main/spec/README.md)
|
||||
|
||||
> [!TIP]
|
||||
> **New to Syft? Check out the [Getting Started guide](https://oss.anchore.com/docs/guides/sbom/getting-started/) for a walkthrough!**
|
||||
|
||||
## Installation
|
||||
|
||||
Syft binaries are provided for Linux, macOS and Windows.
|
||||
|
||||
### Recommended
|
||||
> ```bash
|
||||
> curl -sSfL https://get.anchore.io/syft | sudo sh -s -- -b /usr/local/bin
|
||||
> ```
|
||||
|
||||
Install script options:
|
||||
- `-b`: Specify a custom installation directory (defaults to `./bin`)
|
||||
- `-d`: More verbose logging levels (`-d` for debug, `-dd` for trace)
|
||||
- `-v`: Verify the signature of the downloaded artifact before installation (requires [`cosign`](https://github.com/sigstore/cosign) to be installed)
|
||||
|
||||
### Homebrew
|
||||
The quickest way to get up and going:
|
||||
```bash
|
||||
brew install syft
|
||||
curl -sSfL https://get.anchore.io/syft | sudo sh -s -- -b /usr/local/bin
|
||||
```
|
||||
|
||||
### Scoop
|
||||
> [!TIP]
|
||||
> **See [Installation docs](https://oss.anchore.com/docs/installation/syft/) for more ways to get Syft, including Homebrew, Docker, Scoop, Chocolatey, Nix, and more!**
|
||||
|
||||
```powershell
|
||||
scoop install syft
|
||||
```
|
||||
## The basics
|
||||
|
||||
### Chocolatey
|
||||
|
||||
The chocolatey distribution of Syft is community-maintained and not distributed by the Anchore team
|
||||
|
||||
```powershell
|
||||
choco install syft -y
|
||||
```
|
||||
|
||||
### Nix
|
||||
|
||||
**Note**: Nix packaging of Syft is [community maintained](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/sy/syft/package.nix). Syft is available in the [stable channel](https://wiki.nixos.org/wiki/Nix_channels#The_official_channels) since NixOS `22.05`.
|
||||
See the packages within a container image or directory:
|
||||
|
||||
```bash
|
||||
nix-env -i syft
|
||||
# container image
|
||||
syft alpine:latest
|
||||
|
||||
# directory
|
||||
syft ./my-project
|
||||
```
|
||||
|
||||
... or, just try it out in an ephemeral nix shell:
|
||||
To get an SBOM, specify one or more output formats:
|
||||
|
||||
```bash
|
||||
nix-shell -p syft
|
||||
# SBOM to stdout
|
||||
syft <image> -o cyclonedx-json
|
||||
|
||||
# Multiple SBOMs to files
|
||||
syft <image> -o spdx-json=./spdx.json -o cyclonedx-json=./cdx.json
|
||||
```
|
||||
|
||||
## Getting started
|
||||
|
||||
### SBOM
|
||||
> [!TIP]
|
||||
> **Check out the [Getting Started guide](https://oss.anchore.com/docs/guides/sbom/getting-started/)** to explore all of the capabilities and features.
|
||||
>
|
||||
> **Want to know all of the ins-and-outs of Syft?** Check out the [CLI docs](https://oss.anchore.com/docs/reference/syft/cli/), [configuration docs](https://oss.anchore.com/docs/reference/syft/configuration/), and [JSON schema](https://oss.anchore.com/docs/reference/syft/json/latest/).
|
||||
|
||||
To generate an SBOM for a container image:
|
||||
|
||||
```bash
|
||||
syft <image>
|
||||
```
|
||||
|
||||
The above output includes only software that is visible in the container (i.e., the squashed representation of the image). To include software from all image layers in the SBOM, regardless of its presence in the final image, provide `--scope all-layers`:
|
||||
|
||||
```bash
|
||||
syft <image> --scope all-layers
|
||||
```
|
||||
|
||||
### Output formats
|
||||
|
||||
The output format for Syft is configurable as well using the `-o` (or `--output`) option:
|
||||
|
||||
```
|
||||
syft <image> -o <format>
|
||||
```
|
||||
|
||||
Where the `formats` available are:
|
||||
- `syft-json`: Use this to get as much information out of Syft as possible!
|
||||
- `syft-text`: A row-oriented, human-and-machine-friendly output.
|
||||
- `cyclonedx-xml`: A XML report conforming to the [CycloneDX 1.6 specification](https://cyclonedx.org/specification/overview/).
|
||||
- `cyclonedx-xml@1.5`: A XML report conforming to the [CycloneDX 1.5 specification](https://cyclonedx.org/specification/overview/).
|
||||
- `cyclonedx-json`: A JSON report conforming to the [CycloneDX 1.6 specification](https://cyclonedx.org/specification/overview/).
|
||||
- `cyclonedx-json@1.5`: A JSON report conforming to the [CycloneDX 1.5 specification](https://cyclonedx.org/specification/overview/).
|
||||
- `spdx-tag-value`: A tag-value formatted report conforming to the [SPDX 2.3 specification](https://spdx.github.io/spdx-spec/v2.3/).
|
||||
- `spdx-tag-value@2.2`: A tag-value formatted report conforming to the [SPDX 2.2 specification](https://spdx.github.io/spdx-spec/v2.2.2/).
|
||||
- `spdx-json`: A JSON report conforming to the [SPDX 2.3 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.3/schemas/spdx-schema.json).
|
||||
- `spdx-json@2.2`: A JSON report conforming to the [SPDX 2.2 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json).
|
||||
- `github-json`: A JSON report conforming to GitHub's dependency snapshot format.
|
||||
- `syft-table`: A columnar summary (default).
|
||||
- `template`: Lets the user specify the output format. See ["Using templates"](#using-templates) below.
|
||||
|
||||
Note that flags using the @<version> can be used for earlier versions of each specification as well.
|
||||
|
||||
### Supported Ecosystems
|
||||
|
||||
- Alpine (apk)
|
||||
- Bitnami packages
|
||||
- C (conan)
|
||||
- C++ (conan)
|
||||
- Dart (pubs)
|
||||
- Debian (dpkg)
|
||||
- Dotnet (deps.json)
|
||||
- Objective-C (cocoapods)
|
||||
- Elixir (mix)
|
||||
- Erlang (rebar3)
|
||||
- Go (go.mod, Go binaries)
|
||||
- Haskell (cabal, stack)
|
||||
- Java (jar, ear, war, par, sar, nar, native-image)
|
||||
- JavaScript (npm, yarn)
|
||||
- Jenkins Plugins (jpi, hpi)
|
||||
- Linux kernel archives (vmlinz)
|
||||
- Linux kernel modules (ko)
|
||||
- Nix (outputs in /nix/store)
|
||||
- PHP (composer, PECL, Pear)
|
||||
- Python (wheel, egg, poetry, requirements.txt, uv)
|
||||
- Red Hat (rpm)
|
||||
- Ruby (gem)
|
||||
- Rust (cargo.lock, auditable binary)
|
||||
- Swift (cocoapods, swift-package-manager)
|
||||
- Wordpress plugins
|
||||
- Terraform providers (.terraform.lock.hcl)
|
||||
|
||||
## Documentation
|
||||
|
||||
Our [wiki](https://github.com/anchore/syft/wiki) contains further details on the following topics:
|
||||
|
||||
* [Supported Sources](https://github.com/anchore/syft/wiki/supported-sources)
|
||||
* [File Selection](https://github.com/anchore/syft/wiki/file-selection)
|
||||
* [Excluding file paths](https://github.com/anchore/syft/wiki/excluding-file-paths)
|
||||
* [Output formats](https://github.com/anchore/syft/wiki/output-formats)
|
||||
* [Package Cataloger Selection](https://github.com/anchore/syft/wiki/package-cataloger-selection)
|
||||
* [Concepts](https://github.com/anchore/syft/wiki/package-cataloger-selection#concepts)
|
||||
* [Examples](https://github.com/anchore/syft/wiki/package-cataloger-selection#examples)
|
||||
* [Using templates](https://github.com/anchore/syft/wiki/using-templates)
|
||||
* [Multiple outputs](https://github.com/anchore/syft/wiki/multiple-outputs)
|
||||
* [Private Registry Authentication](https://github.com/anchore/syft/wiki/private-registry-authentication)
|
||||
* [Local Docker Credentials](https://github.com/anchore/syft/wiki/private-registry-authentication#local-docker)
|
||||
* [Docker Credentials in Kubernetes](https://github.com/anchore/syft/wiki/private-registry-authentication#docker-credentials-in-kubernetes)
|
||||
* [Attestation (experimental)](https://github.com/anchore/syft/wiki/attestation)
|
||||
* [Keyless Support](https://github.com/anchore/syft/wiki/attestation#keyless-support)
|
||||
* [Local private key support](https://github.com/anchore/syft/wiki/attestation#local-private-key-support)
|
||||
* [Adding an SBOM to an image as an attestation using Syft](https://github.com/anchore/syft/wiki/attestation#adding-an-sbom-to-an-image-as-an-attestation-using-syft)
|
||||
* [Configuration](https://github.com/anchore/syft/wiki/configuration)
|
||||
|
||||
## Contributing
|
||||
|
||||
Check out our [contributing](/CONTRIBUTING.md) guide and [developer](/DEVELOPING.md) docs.
|
||||
We encourage users to help make these tools better by [submitting issues](https://github.com/anchore/syft/issues) when you find a bug or want a new feature.
|
||||
Check out our [contributing overview](https://oss.anchore.com/docs/contributing/) and [developer-specific documentation](https://oss.anchore.com/docs/contributing/syft/) if you are interested in providing code contributions.
|
||||
|
||||
## Syft Team Meetings
|
||||
|
||||
The Syft Team hold regular community meetings online. All are welcome to join to bring topics for discussion.
|
||||
|
||||
<p xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/">
|
||||
Syft development is sponsored by <a href="https://anchore.com/">Anchore</a>, and is released under the <a href="https://github.com/anchore/syft?tab=Apache-2.0-1-ov-file">Apache-2.0 License</a>.
|
||||
The <a property="dct:title" rel="cc:attributionURL" href="https://anchore.com/wp-content/uploads/2024/11/syft-logo.svg">Syft logo</a> by <a rel="cc:attributionURL dct:creator" property="cc:attributionName" href="https://anchore.com/">Anchore</a> is licensed under <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="license noopener noreferrer" style="display:inline-block;">CC BY 4.0<img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt=""><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt=""></a>
|
||||
</p>
|
||||
|
||||
For commercial support options with Syft or Grype, please [contact Anchore](https://get.anchore.com/contact/).
|
||||
|
||||
## Come talk to us!
|
||||
|
||||
The Syft Team holds regular community meetings online. All are welcome to join to bring topics for discussion.
|
||||
- Check the [calendar](https://calendar.google.com/calendar/u/0/r?cid=Y182OTM4dGt0MjRtajI0NnNzOThiaGtnM29qNEBncm91cC5jYWxlbmRhci5nb29nbGUuY29t) for the next meeting date.
|
||||
- Add items to the [agenda](https://docs.google.com/document/d/1ZtSAa6fj2a6KRWviTn3WoJm09edvrNUp4Iz_dOjjyY8/edit?usp=sharing) (join [this group](https://groups.google.com/g/anchore-oss-community) for write access to the [agenda](https://docs.google.com/document/d/1ZtSAa6fj2a6KRWviTn3WoJm09edvrNUp4Iz_dOjjyY8/edit?usp=sharing))
|
||||
- See you there!
|
||||
|
||||
## Syft Logo
|
||||
|
||||
<p xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/"><a property="dct:title" rel="cc:attributionURL" href="https://anchore.com/wp-content/uploads/2024/11/syft-logo.svg">Syft Logo</a> by <a rel="cc:attributionURL dct:creator" property="cc:attributionName" href="https://anchore.com/">Anchore</a> is licensed under <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="license noopener noreferrer" style="display:inline-block;">CC BY 4.0<img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt=""><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt=""></a></p>
|
||||
|
||||
+3
-19
@@ -2,31 +2,15 @@
|
||||
|
||||
## Supported Versions
|
||||
|
||||
<!-- Use this section to tell people about which versions of your project are
|
||||
currently being supported with security updates.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 5.1.x | :white_check_mark: |
|
||||
| 5.0.x | :x: |
|
||||
| 4.0.x | :white_check_mark: |
|
||||
| < 4.0 | :x: |
|
||||
|
||||
-->
|
||||
|
||||
Security updates are applied only to the most recent release, try to always be up to date.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
<!-- Use this section to tell people how to report a vulnerability.
|
||||
|
||||
Tell them where to go, how often they can expect to get an update on a
|
||||
reported vulnerability, what to expect if the vulnerability is accepted or
|
||||
declined, etc. -->
|
||||
|
||||
To report a security issue, please email
|
||||
[security@anchore.com](mailto:security@anchore.com)
|
||||
with a description of the issue, the steps you took to create the issue,
|
||||
affected versions, and, if known, mitigations for the issue.
|
||||
|
||||
All support will be made on the best effort base, so please indicate the "urgency level" of the vulnerability as Critical, High, Medium or Low.
|
||||
All support will be made on a best effort basis, so please indicate the "urgency level" of the vulnerability as Critical, High, Medium or Low.
|
||||
|
||||
For more details, see our [security policy documentation](https://oss.anchore.com/docs/contributing/security/).
|
||||
|
||||
+9
-4
@@ -1,5 +1,9 @@
|
||||
|
||||
version: "3"
|
||||
|
||||
includes:
|
||||
generate:cpe-index: ./task.d/generate/cpe-index.yaml
|
||||
|
||||
vars:
|
||||
OWNER: anchore
|
||||
PROJECT: syft
|
||||
@@ -502,7 +506,7 @@ tasks:
|
||||
generate-json-schema:
|
||||
desc: Generate a new JSON schema
|
||||
cmds:
|
||||
- "cd syft/internal && go generate . && cd jsonschema && go run . && go fmt ../..."
|
||||
- "cd ./internal && go generate . && cd ./jsonschema && go run . && go fmt ../..."
|
||||
|
||||
generate-license-list:
|
||||
desc: Generate an updated license processing code off of the latest available SPDX license list
|
||||
@@ -511,10 +515,11 @@ tasks:
|
||||
- "gofmt -s -w ./internal/spdxlicense"
|
||||
|
||||
generate-cpe-dictionary-index:
|
||||
desc: Generate the CPE index based off of the latest available CPE dictionary
|
||||
dir: "syft/pkg/cataloger/internal/cpegenerate/dictionary"
|
||||
desc: Generate the CPE index from local cache
|
||||
cmds:
|
||||
- "go generate"
|
||||
- task: generate:cpe-index:cache:pull
|
||||
- task: generate:cpe-index:cache:update
|
||||
- task: generate:cpe-index:build
|
||||
|
||||
|
||||
## Build-related targets #################################
|
||||
|
||||
@@ -253,7 +253,6 @@ func generateSBOMForAttestation(ctx context.Context, id clio.Identification, opt
|
||||
}
|
||||
|
||||
src, err := getSource(ctx, opts, userInput, stereoscope.RegistryTag)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -87,8 +87,8 @@ func runCatalogerList(opts *catalogerListOptions) error {
|
||||
}
|
||||
|
||||
func catalogerListReport(opts *catalogerListOptions, allTaskGroups [][]task.Task) (string, error) {
|
||||
defaultCatalogers := options.Flatten(opts.DefaultCatalogers)
|
||||
selectCatalogers := options.Flatten(opts.SelectCatalogers)
|
||||
defaultCatalogers := options.FlattenAndSort(opts.DefaultCatalogers)
|
||||
selectCatalogers := options.FlattenAndSort(opts.SelectCatalogers)
|
||||
selectedTaskGroups, selectionEvidence, err := task.SelectInGroups(
|
||||
allTaskGroups,
|
||||
cataloging.NewSelectionRequest().
|
||||
|
||||
@@ -185,7 +185,6 @@ func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, use
|
||||
}
|
||||
|
||||
src, err := getSource(ctx, &opts.Catalog, userInput, sources...)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ import (
|
||||
"github.com/anchore/syft/syft/cataloging"
|
||||
"github.com/anchore/syft/syft/cataloging/filecataloging"
|
||||
"github.com/anchore/syft/syft/cataloging/pkgcataloging"
|
||||
"github.com/anchore/syft/syft/file/cataloger/executable"
|
||||
"github.com/anchore/syft/syft/file/cataloger/filecontent"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/dotnet"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/golang"
|
||||
@@ -142,18 +140,21 @@ func (cfg Catalog) ToFilesConfig() filecataloging.Config {
|
||||
log.WithFields("error", err).Warn("unable to configure file hashers")
|
||||
}
|
||||
|
||||
return filecataloging.Config{
|
||||
Selection: cfg.File.Metadata.Selection,
|
||||
Hashers: hashers,
|
||||
Content: filecontent.Config{
|
||||
Globs: cfg.File.Content.Globs,
|
||||
SkipFilesAboveSize: cfg.File.Content.SkipFilesAboveSize,
|
||||
},
|
||||
Executable: executable.Config{
|
||||
MIMETypes: executable.DefaultConfig().MIMETypes,
|
||||
Globs: cfg.File.Executable.Globs,
|
||||
},
|
||||
}
|
||||
c := filecataloging.DefaultConfig()
|
||||
c.Selection = cfg.File.Metadata.Selection
|
||||
c.Hashers = hashers
|
||||
c.Content.Globs = cfg.File.Content.Globs
|
||||
c.Content.SkipFilesAboveSize = cfg.File.Content.SkipFilesAboveSize
|
||||
c.Executable.Globs = cfg.File.Executable.Globs
|
||||
|
||||
// symbol capture configuration
|
||||
c.Executable.Symbols.CaptureScope = cfg.File.Executable.Symbols.CaptureScope
|
||||
c.Executable.Symbols.Types = cfg.File.Executable.Symbols.Types
|
||||
c.Executable.Symbols.Go.StandardLibrary = cfg.File.Executable.Symbols.Go.StandardLibrary
|
||||
c.Executable.Symbols.Go.ExtendedStandardLibrary = cfg.File.Executable.Symbols.Go.ExtendedStandardLibrary
|
||||
c.Executable.Symbols.Go.ThirdPartyModules = cfg.File.Executable.Symbols.Go.ThirdPartyModules
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (cfg Catalog) ToLicenseConfig() cataloging.LicenseConfig {
|
||||
@@ -188,7 +189,8 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
|
||||
WithFromContents(cfg.Golang.MainModuleVersion.FromContents).
|
||||
WithFromBuildSettings(cfg.Golang.MainModuleVersion.FromBuildSettings).
|
||||
WithFromLDFlags(cfg.Golang.MainModuleVersion.FromLDFlags),
|
||||
),
|
||||
).
|
||||
WithUsePackagesLib(*multiLevelOption(true, enrichmentEnabled(cfg.Enrich, task.Go, task.Golang), cfg.Golang.UsePackagesLib)),
|
||||
JavaScript: javascript.DefaultCatalogerConfig().
|
||||
WithIncludeDevDependencies(*multiLevelOption(false, cfg.JavaScript.IncludeDevDependencies)).
|
||||
WithSearchRemoteLicenses(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.JavaScript, task.Node, task.NPM), cfg.JavaScript.SearchRemoteLicenses)).
|
||||
@@ -198,9 +200,10 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
|
||||
},
|
||||
Nix: nix.DefaultConfig().
|
||||
WithCaptureOwnedFiles(cfg.Nix.CaptureOwnedFiles),
|
||||
Python: python.CatalogerConfig{
|
||||
GuessUnpinnedRequirements: cfg.Python.GuessUnpinnedRequirements,
|
||||
},
|
||||
Python: python.DefaultCatalogerConfig().
|
||||
WithSearchRemoteLicenses(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.Python), cfg.Python.SearchRemoteLicenses)).
|
||||
WithPypiBaseURL(cfg.Python.PypiBaseURL).
|
||||
WithGuessUnpinnedRequirements(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.Python), cfg.Python.GuessUnpinnedRequirements)),
|
||||
JavaArchive: java.DefaultArchiveCatalogerConfig().
|
||||
WithUseMavenLocalRepository(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.Java, task.Maven), cfg.Java.UseMavenLocalRepository)).
|
||||
WithMavenLocalRepositoryDir(cfg.Java.MavenLocalRepositoryDir).
|
||||
@@ -283,10 +286,10 @@ func (cfg *Catalog) PostLoad() error {
|
||||
|
||||
cfg.From = Flatten(cfg.From)
|
||||
|
||||
cfg.Catalogers = Flatten(cfg.Catalogers)
|
||||
cfg.DefaultCatalogers = Flatten(cfg.DefaultCatalogers)
|
||||
cfg.SelectCatalogers = Flatten(cfg.SelectCatalogers)
|
||||
cfg.Enrich = Flatten(cfg.Enrich)
|
||||
cfg.Catalogers = FlattenAndSort(cfg.Catalogers)
|
||||
cfg.DefaultCatalogers = FlattenAndSort(cfg.DefaultCatalogers)
|
||||
cfg.SelectCatalogers = FlattenAndSort(cfg.SelectCatalogers)
|
||||
cfg.Enrich = FlattenAndSort(cfg.Enrich)
|
||||
|
||||
// for backwards compatibility
|
||||
cfg.DefaultCatalogers = append(cfg.DefaultCatalogers, cfg.Catalogers...)
|
||||
@@ -301,6 +304,11 @@ func (cfg *Catalog) PostLoad() error {
|
||||
return fmt.Errorf("cannot enable exclude-binary-overlap-by-ownership without enabling package-file-ownership-overlap")
|
||||
}
|
||||
|
||||
// validate file executable options
|
||||
if err := cfg.ToFilesConfig().Executable.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid file executable configuration: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -311,6 +319,11 @@ func Flatten(commaSeparatedEntries []string) []string {
|
||||
out = append(out, strings.TrimSpace(s))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func FlattenAndSort(commaSeparatedEntries []string) []string {
|
||||
out := Flatten(commaSeparatedEntries)
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
@@ -320,6 +333,7 @@ var publicisedEnrichmentOptions = []string{
|
||||
task.Golang,
|
||||
task.Java,
|
||||
task.JavaScript,
|
||||
task.Python,
|
||||
}
|
||||
|
||||
func enrichmentEnabled(enrichDirectives []string, features ...string) *bool {
|
||||
|
||||
@@ -79,6 +79,98 @@ func TestCatalog_PostLoad(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlatten(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "preserves order of comma-separated values",
|
||||
input: []string{"registry,docker,oci-dir"},
|
||||
expected: []string{"registry", "docker", "oci-dir"},
|
||||
},
|
||||
{
|
||||
name: "preserves order across multiple entries",
|
||||
input: []string{"registry,docker", "oci-dir"},
|
||||
expected: []string{"registry", "docker", "oci-dir"},
|
||||
},
|
||||
{
|
||||
name: "trims whitespace",
|
||||
input: []string{" registry , docker ", " oci-dir "},
|
||||
expected: []string{"registry", "docker", "oci-dir"},
|
||||
},
|
||||
{
|
||||
name: "handles single value",
|
||||
input: []string{"registry"},
|
||||
expected: []string{"registry"},
|
||||
},
|
||||
{
|
||||
name: "handles empty input",
|
||||
input: []string{},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "preserves reverse alphabetical order",
|
||||
input: []string{"zebra,yankee,xray"},
|
||||
expected: []string{"zebra", "yankee", "xray"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Flatten(tt.input)
|
||||
assert.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlattenAndSort(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "sorts comma-separated values",
|
||||
input: []string{"registry,docker,oci-dir"},
|
||||
expected: []string{"docker", "oci-dir", "registry"},
|
||||
},
|
||||
{
|
||||
name: "sorts across multiple entries",
|
||||
input: []string{"registry,docker", "oci-dir"},
|
||||
expected: []string{"docker", "oci-dir", "registry"},
|
||||
},
|
||||
{
|
||||
name: "trims whitespace and sorts",
|
||||
input: []string{" registry , docker ", " oci-dir "},
|
||||
expected: []string{"docker", "oci-dir", "registry"},
|
||||
},
|
||||
{
|
||||
name: "handles single value",
|
||||
input: []string{"registry"},
|
||||
expected: []string{"registry"},
|
||||
},
|
||||
{
|
||||
name: "handles empty input",
|
||||
input: []string{},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "sorts reverse alphabetical order",
|
||||
input: []string{"zebra,yankee,xray"},
|
||||
expected: []string{"xray", "yankee", "zebra"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := FlattenAndSort(tt.input)
|
||||
assert.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_enrichmentEnabled(t *testing.T) {
|
||||
tests := []struct {
|
||||
directives string
|
||||
@@ -139,7 +231,7 @@ func Test_enrichmentEnabled(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.directives, func(t *testing.T) {
|
||||
got := enrichmentEnabled(Flatten([]string{test.directives}), test.test)
|
||||
got := enrichmentEnabled(FlattenAndSort([]string{test.directives}), test.test)
|
||||
assert.Equal(t, test.expected, got)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ type dotnetConfig struct {
|
||||
PropagateDLLClaimsToParents bool `mapstructure:"propagate-dll-claims-to-parents" json:"propagate-dll-claims-to-parents" yaml:"propagate-dll-claims-to-parents"`
|
||||
|
||||
RelaxDLLClaimsWhenBundlingDetected bool `mapstructure:"relax-dll-claims-when-bundling-detected" json:"relax-dll-claims-when-bundling-detected" yaml:"relax-dll-claims-when-bundling-detected"`
|
||||
|
||||
ExcludeProjectReferences bool `mapstructure:"exclude-project-references" json:"exclude-project-references" yaml:"exclude-project-references"`
|
||||
}
|
||||
|
||||
var _ interface {
|
||||
@@ -24,6 +26,7 @@ func (o *dotnetConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
|
||||
descriptions.Add(&o.DepPackagesMustClaimDLL, `only keep dep.json packages which have a runtime/resource DLL claimed in the deps.json targets section (but not necessarily found on disk). The package is also included if any child package claims a DLL, even if the package itself does not claim a DLL.`)
|
||||
descriptions.Add(&o.PropagateDLLClaimsToParents, `treat DLL claims or on-disk evidence for child packages as DLL claims or on-disk evidence for any parent package`)
|
||||
descriptions.Add(&o.RelaxDLLClaimsWhenBundlingDetected, `show all packages from the deps.json if bundling tooling is present as a dependency (e.g. ILRepack)`)
|
||||
descriptions.Add(&o.ExcludeProjectReferences, `exclude packages with type "project" from deps.json output (these are internal project references, not NuGet packages)`)
|
||||
}
|
||||
|
||||
func defaultDotnetConfig() dotnetConfig {
|
||||
@@ -33,5 +36,6 @@ func defaultDotnetConfig() dotnetConfig {
|
||||
DepPackagesMustClaimDLL: def.DepPackagesMustClaimDLL,
|
||||
PropagateDLLClaimsToParents: def.PropagateDLLClaimsToParents,
|
||||
RelaxDLLClaimsWhenBundlingDetected: def.RelaxDLLClaimsWhenBundlingDetected,
|
||||
ExcludeProjectReferences: def.ExcludeProjectReferences,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/anchore/clio"
|
||||
intFile "github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/file/cataloger/executable"
|
||||
)
|
||||
|
||||
type fileConfig struct {
|
||||
@@ -28,11 +29,27 @@ type fileContent struct {
|
||||
}
|
||||
|
||||
type fileExecutable struct {
|
||||
Globs []string `yaml:"globs" json:"globs" mapstructure:"globs"`
|
||||
Globs []string `yaml:"globs" json:"globs" mapstructure:"globs"`
|
||||
Symbols fileSymbolConfig `yaml:"symbols" json:"symbols" mapstructure:"symbols"`
|
||||
}
|
||||
|
||||
type fileSymbolConfig struct {
|
||||
CaptureScope []executable.SymbolCaptureScope `yaml:"capture" json:"capture" mapstructure:"capture"`
|
||||
Types []string `yaml:"types" json:"types" mapstructure:"types"`
|
||||
Go fileGoSymbolConfig `yaml:"go" json:"go" mapstructure:"go"`
|
||||
}
|
||||
|
||||
type fileGoSymbolConfig struct {
|
||||
StandardLibrary bool `yaml:"standard-library" json:"standard-library" mapstructure:"standard-library"`
|
||||
ExtendedStandardLibrary bool `yaml:"extended-standard-library" json:"extended-standard-library" mapstructure:"extended-standard-library"`
|
||||
ThirdPartyModules bool `yaml:"third-party-modules" json:"third-party-modules" mapstructure:"third-party-modules"`
|
||||
}
|
||||
|
||||
func defaultFileConfig() fileConfig {
|
||||
return fileConfig{
|
||||
api := executable.DefaultConfig()
|
||||
|
||||
// start with API defaults and override CLI-specific values
|
||||
cfg := fileConfig{
|
||||
Metadata: fileMetadata{
|
||||
Selection: file.FilesOwnedByPackageSelection,
|
||||
Digests: []string{"sha1", "sha256"},
|
||||
@@ -41,9 +58,19 @@ func defaultFileConfig() fileConfig {
|
||||
SkipFilesAboveSize: 250 * intFile.KB,
|
||||
},
|
||||
Executable: fileExecutable{
|
||||
Globs: nil,
|
||||
Globs: api.Globs,
|
||||
Symbols: fileSymbolConfig{
|
||||
CaptureScope: api.Symbols.CaptureScope,
|
||||
Types: api.Symbols.Types,
|
||||
Go: fileGoSymbolConfig{
|
||||
StandardLibrary: api.Symbols.Go.StandardLibrary,
|
||||
ExtendedStandardLibrary: api.Symbols.Go.ExtendedStandardLibrary,
|
||||
ThirdPartyModules: api.Symbols.Go.ThirdPartyModules,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
var _ interface {
|
||||
@@ -64,7 +91,7 @@ func (c *fileConfig) PostLoad() error {
|
||||
}
|
||||
|
||||
func (c *fileConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
|
||||
descriptions.Add(&c.Metadata.Selection, `select which files should be captured by the file-metadata cataloger and included in the SBOM.
|
||||
descriptions.Add(&c.Metadata.Selection, `select which files should be captured by the file-metadata cataloger and included in the SBOM.
|
||||
Options include:
|
||||
- "all": capture all files from the search space
|
||||
- "owned-by-package": capture only files owned by packages
|
||||
@@ -75,4 +102,13 @@ Options include:
|
||||
descriptions.Add(&c.Content.Globs, `file globs for the cataloger to match on`)
|
||||
|
||||
descriptions.Add(&c.Executable.Globs, `file globs for the cataloger to match on`)
|
||||
|
||||
// symbol capture configuration
|
||||
descriptions.Add(&c.Executable.Symbols.CaptureScope, `the scope of symbols to capture from executables (options: "golang")`)
|
||||
descriptions.Add(&c.Executable.Symbols.Types, `the types of symbols to capture, relative to "go tool nm" output (options: "T", "t", "R", "r", "D", "d", "B", "b", "C", "U")`)
|
||||
|
||||
// go symbol configuration
|
||||
descriptions.Add(&c.Executable.Symbols.Go.StandardLibrary, `capture Go standard library symbols (e.g. "fmt", "net/http")`)
|
||||
descriptions.Add(&c.Executable.Symbols.Go.ExtendedStandardLibrary, `capture extended Go standard library symbols (e.g. "golang.org/x/net")`)
|
||||
descriptions.Add(&c.Executable.Symbols.Go.ThirdPartyModules, `capture third-party module symbols (e.g. "github.com/spf13/cobra")`)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ type golangConfig struct {
|
||||
Proxy string `json:"proxy" yaml:"proxy" mapstructure:"proxy"`
|
||||
NoProxy string `json:"no-proxy" yaml:"no-proxy" mapstructure:"no-proxy"`
|
||||
MainModuleVersion golangMainModuleVersionConfig `json:"main-module-version" yaml:"main-module-version" mapstructure:"main-module-version"`
|
||||
UsePackagesLib *bool `json:"use-packages-lib" yaml:"use-packages-lib" mapstructure:"use-packages-lib"`
|
||||
}
|
||||
|
||||
var _ interface {
|
||||
@@ -37,6 +38,7 @@ if unset this defaults to $GONOPROXY`)
|
||||
descriptions.Add(&o.MainModuleVersion, `the go main module version discovered from binaries built with the go compiler will
|
||||
always show (devel) as the version. Use these options to control heuristics to guess
|
||||
a more accurate version from the binary.`)
|
||||
descriptions.Add(&o.UsePackagesLib, `use the golang.org/x/tools/go/packages library, which executes golang tooling found on the path in addition to potential network access to get the most accurate results`)
|
||||
descriptions.Add(&o.MainModuleVersion.FromLDFlags, `look for LD flags that appear to be setting a version (e.g. -X main.version=1.0.0)`)
|
||||
descriptions.Add(&o.MainModuleVersion.FromBuildSettings, `use the build settings (e.g. vcs.version & vcs.time) to craft a v0 pseudo version
|
||||
(e.g. v0.0.0-20220308212642-53e6d0aaf6fb) when a more accurate version cannot be found otherwise`)
|
||||
@@ -64,5 +66,6 @@ func defaultGolangConfig() golangConfig {
|
||||
FromContents: def.MainModuleVersion.FromContents,
|
||||
FromBuildSettings: def.MainModuleVersion.FromBuildSettings,
|
||||
},
|
||||
UsePackagesLib: nil, // this defaults to true, which is the API default
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ package options
|
||||
import "github.com/anchore/clio"
|
||||
|
||||
type pythonConfig struct {
|
||||
GuessUnpinnedRequirements bool `json:"guess-unpinned-requirements" yaml:"guess-unpinned-requirements" mapstructure:"guess-unpinned-requirements"`
|
||||
SearchRemoteLicenses *bool `json:"search-remote-licenses" yaml:"search-remote-licenses" mapstructure:"search-remote-licenses"`
|
||||
PypiBaseURL string `json:"pypi-base-url" yaml:"pypi-base-url" mapstructure:"pypi-base-url"`
|
||||
GuessUnpinnedRequirements *bool `json:"guess-unpinned-requirements" yaml:"guess-unpinned-requirements" mapstructure:"guess-unpinned-requirements"`
|
||||
}
|
||||
|
||||
var _ interface {
|
||||
@@ -11,6 +13,8 @@ var _ interface {
|
||||
} = (*pythonConfig)(nil)
|
||||
|
||||
func (o *pythonConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
|
||||
descriptions.Add(&o.SearchRemoteLicenses, `enables Syft to use the network to fill in more detailed license information`)
|
||||
descriptions.Add(&o.PypiBaseURL, `base Pypi url to use`)
|
||||
descriptions.Add(&o.GuessUnpinnedRequirements, `when running across entries in requirements.txt that do not specify a specific version
|
||||
(e.g. "sqlalchemy >= 1.0.0, <= 2.0.0, != 3.0.0, <= 3.0.0"), attempt to guess what the version could
|
||||
be based on the version requirements specified (e.g. "1.0.0"). When enabled the lowest expressible version
|
||||
|
||||
@@ -97,7 +97,7 @@ var imageOnlyTestCases = []testCase{
|
||||
"Serilog.Sinks.Console": "4.0.1",
|
||||
//"System.Diagnostics.DiagnosticSource": "6.0.0", // no dll claims in deps.json targets section
|
||||
//"System.Runtime.CompilerServices.Unsafe": "6.0.0", // no dll claims in deps.json targets section
|
||||
"TestCommon": "1.0.0",
|
||||
//"TestCommon": "1.0.0",
|
||||
"TestLibrary": "1.0.0",
|
||||
},
|
||||
},
|
||||
@@ -274,7 +274,7 @@ var dirOnlyTestCases = []testCase{
|
||||
"Serilog.Sinks.Console": "4.0.1",
|
||||
//"System.Diagnostics.DiagnosticSource": "6.0.0", // no dll claims in deps.json targets section
|
||||
//"System.Runtime.CompilerServices.Unsafe": "6.0.0", // no dll claims in deps.json targets section
|
||||
"TestCommon": "1.0.0",
|
||||
//"TestCommon": "1.0.0",
|
||||
"TestLibrary": "1.0.0",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -25,7 +25,6 @@ func BenchmarkImagePackageCatalogers(b *testing.B) {
|
||||
// get the source object for the image
|
||||
theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive"))
|
||||
require.NoError(b, err)
|
||||
|
||||
b.Cleanup(func() {
|
||||
require.NoError(b, theSource.Close())
|
||||
})
|
||||
@@ -88,6 +87,7 @@ func TestPkgCoverageImage(t *testing.T) {
|
||||
definedPkgs.Remove(string(pkg.TerraformPkg))
|
||||
definedPkgs.Remove(string(pkg.PhpPeclPkg)) // we have coverage for pear instead
|
||||
definedPkgs.Remove(string(pkg.CondaPkg))
|
||||
definedPkgs.Remove(string(pkg.ModelPkg))
|
||||
|
||||
var cases []testCase
|
||||
cases = append(cases, commonTestCases...)
|
||||
@@ -162,6 +162,7 @@ func TestPkgCoverageDirectory(t *testing.T) {
|
||||
definedPkgs.Remove(string(pkg.UnknownPkg))
|
||||
definedPkgs.Remove(string(pkg.CondaPkg))
|
||||
definedPkgs.Remove(string(pkg.PhpPeclPkg)) // this is covered as pear packages
|
||||
definedPkgs.Remove(string(pkg.ModelPkg))
|
||||
|
||||
// for directory scans we should not expect to see any of the following package types
|
||||
definedPkgs.Remove(string(pkg.KbPkg))
|
||||
|
||||
@@ -30,10 +30,10 @@ func TestPackageDeduplication(t *testing.T) {
|
||||
locationCount: map[string]int{
|
||||
"basesystem-11-13.el9": 5, // in all layers
|
||||
"curl-minimal-7.76.1-26.el9_3.2.0.1": 2, // base + wget layer
|
||||
"curl-minimal-7.76.1-31.el9": 3, // curl upgrade layer + all above layers
|
||||
"curl-minimal-7.76.1-31.el9_6.1": 3, // curl upgrade layer + all above layers
|
||||
"wget-1.21.1-8.el9_4": 4, // wget + all above layers
|
||||
"vsftpd-3.0.5-6.el9": 2, // vsftpd + all above layers
|
||||
"httpd-2.4.62-4.el9": 1, // last layer
|
||||
"httpd-2.4.62-4.el9_6.4": 1, // last layer
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -47,11 +47,11 @@ func TestPackageDeduplication(t *testing.T) {
|
||||
"httpd": 1, // rpm, binary is now excluded by overlap
|
||||
},
|
||||
locationCount: map[string]int{
|
||||
"basesystem-11-13.el9": 1,
|
||||
"curl-minimal-7.76.1-31.el9": 1, // upgrade
|
||||
"wget-1.21.1-8.el9_4": 1,
|
||||
"vsftpd-3.0.5-6.el9": 1,
|
||||
"httpd-2.4.62-4.el9": 1,
|
||||
"basesystem-11-13.el9": 1,
|
||||
"curl-minimal-7.76.1-31.el9_6.1": 1, // upgrade
|
||||
"wget-1.21.1-8.el9_4": 1,
|
||||
"vsftpd-3.0.5-6.el9": 1,
|
||||
"httpd-2.4.62-4.el9_6.4": 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+4
-4
@@ -7,16 +7,16 @@ FROM --platform=linux/amd64 rockylinux:9.3.20231119@sha256:d644d203142cd5b54ad2a
|
||||
# copying the RPM DB from each stage to a final stage in separate layers. This will result in a much smaller image.
|
||||
|
||||
FROM base AS stage1
|
||||
RUN dnf install -y wget
|
||||
RUN dnf install -y wget-1.21.1-8.el9_4
|
||||
|
||||
FROM stage1 AS stage2
|
||||
RUN dnf update -y curl-minimal
|
||||
RUN dnf update -y curl-minimal-7.76.1-31.el9_6.1
|
||||
|
||||
FROM stage2 AS stage3
|
||||
RUN dnf install -y vsftpd
|
||||
RUN dnf install -y vsftpd-3.0.5-6.el9
|
||||
|
||||
FROM stage3 AS stage4
|
||||
RUN dnf install -y httpd
|
||||
RUN dnf install -y httpd-2.4.62-4.el9_6.4
|
||||
|
||||
FROM scratch
|
||||
|
||||
|
||||
@@ -38,11 +38,11 @@ func catalogFixtureImageWithConfig(t *testing.T, fixtureImageName string, cfg *s
|
||||
// get the source to build an SBOM against
|
||||
theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive"))
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, theSource.Close())
|
||||
})
|
||||
|
||||
// build the SBOM
|
||||
s, err := syft.CreateSBOM(context.Background(), theSource, cfg)
|
||||
|
||||
require.NoError(t, err)
|
||||
@@ -66,7 +66,7 @@ func catalogDirectory(t *testing.T, dir string, catalogerSelection ...string) (s
|
||||
func catalogDirectoryWithConfig(t *testing.T, dir string, cfg *syft.CreateSBOMConfig) (sbom.SBOM, source.Source) {
|
||||
cfg.CatalogerSelection = cfg.CatalogerSelection.WithDefaults(pkgcataloging.DirectoryTag)
|
||||
|
||||
// get the source to build an sbom against
|
||||
// get the source to build an SBOM against
|
||||
theSource, err := syft.GetSource(context.Background(), dir, syft.DefaultGetSourceConfig().WithSources("dir"))
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
|
||||
@@ -23,6 +23,7 @@ const defaultImage = "alpine:3.19"
|
||||
func main() {
|
||||
// automagically get a source.Source for arbitrary string input
|
||||
src := getSource(imageReference())
|
||||
defer src.Close()
|
||||
|
||||
// will catalog the given source and return a SBOM keeping in mind several configurable options
|
||||
sbom := getSBOM(src)
|
||||
@@ -46,7 +47,6 @@ func getSource(input string) source.Source {
|
||||
fmt.Println("detecting source type for input:", input, "...")
|
||||
|
||||
src, err := syft.GetSource(context.Background(), input, nil)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ const defaultImage = "alpine:3.19"
|
||||
func main() {
|
||||
// automagically get a source.Source for arbitrary string input
|
||||
src := getSource(imageReference())
|
||||
defer src.Close()
|
||||
|
||||
// catalog the given source and return a SBOM
|
||||
sbom := getSBOM(src)
|
||||
@@ -40,7 +41,6 @@ func imageReference() string {
|
||||
|
||||
func getSource(input string) source.Source {
|
||||
src, err := syft.GetSource(context.Background(), input, nil)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ const defaultImage = "alpine:3.19"
|
||||
func main() {
|
||||
// automagically get a source.Source for arbitrary string input
|
||||
src := getSource(imageReference())
|
||||
defer src.Close()
|
||||
|
||||
// catalog the given source and return a SBOM
|
||||
// let's explicitly use catalogers that are:
|
||||
@@ -44,7 +45,6 @@ func imageReference() string {
|
||||
|
||||
func getSource(input string) source.Source {
|
||||
src, err := syft.GetSource(context.Background(), input, nil)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ func main() {
|
||||
image := "alpine:3.19"
|
||||
|
||||
src, _ := syft.GetSource(context.Background(), image, syft.DefaultGetSourceConfig().WithSources("registry"))
|
||||
defer src.Close()
|
||||
|
||||
sbom, _ := syft.CreateSBOM(context.Background(), src, syft.DefaultCreateSBOMConfig())
|
||||
|
||||
|
||||
@@ -4,18 +4,17 @@ go 1.24.1
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0
|
||||
github.com/CycloneDX/cyclonedx-go v0.9.2
|
||||
github.com/CycloneDX/cyclonedx-go v0.9.3
|
||||
github.com/Masterminds/semver/v3 v3.4.0
|
||||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
github.com/OneOfOne/xxhash v1.2.8
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
|
||||
github.com/acobaugh/osrelease v0.1.0
|
||||
github.com/adrg/xdg v0.5.3
|
||||
github.com/anchore/archiver/v3 v3.5.3-0.20241210171143-5b1d8d1c7c51
|
||||
github.com/anchore/bubbly v0.0.0-20231115134915-def0aba654a9
|
||||
github.com/anchore/clio v0.0.0-20250319180342-2cfe4b0cb716
|
||||
github.com/anchore/fangs v0.0.0-20250319222917-446a1e748ec2
|
||||
github.com/anchore/go-collections v0.0.0-20240216171411-9321230ce537
|
||||
github.com/anchore/go-collections v0.0.0-20251016125210-a3c352120e8c
|
||||
github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d
|
||||
github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722
|
||||
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb
|
||||
@@ -24,14 +23,14 @@ require (
|
||||
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.20250220190351-d62adb6e1115
|
||||
github.com/anchore/stereoscope v0.1.9-0.20250826202322-ef061ea78385
|
||||
github.com/anchore/stereoscope v0.1.16
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
||||
github.com/aquasecurity/go-pep440-version v0.0.1
|
||||
github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
|
||||
github.com/bmatcuk/doublestar/v4 v4.9.1
|
||||
github.com/charmbracelet/bubbles v0.21.0
|
||||
github.com/charmbracelet/bubbletea v1.3.6
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/dave/jennifer v1.7.1
|
||||
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da
|
||||
@@ -40,35 +39,35 @@ require (
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/elliotchance/phpserialize v1.4.0
|
||||
github.com/facebookincubator/nvdtools v0.1.5
|
||||
github.com/github/go-spdx/v2 v2.3.3
|
||||
github.com/gkampitakis/go-snaps v0.5.14
|
||||
github.com/go-git/go-billy/v5 v5.6.2
|
||||
github.com/go-git/go-git/v5 v5.16.2
|
||||
github.com/github/go-spdx/v2 v2.3.5
|
||||
github.com/gkampitakis/go-snaps v0.5.18
|
||||
github.com/go-git/go-billy/v5 v5.7.0
|
||||
github.com/go-git/go-git/v5 v5.16.3
|
||||
github.com/go-test/deep v1.1.1
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0
|
||||
github.com/gohugoio/hashstructure v0.5.0
|
||||
github.com/gohugoio/hashstructure v0.6.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/go-containerregistry v0.20.6
|
||||
github.com/google/go-containerregistry v0.20.7
|
||||
github.com/google/licensecheck v0.3.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gookit/color v1.5.4
|
||||
github.com/gookit/color v1.6.0
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2
|
||||
github.com/hashicorp/go-getter v1.7.9
|
||||
github.com/hashicorp/go-getter v1.8.3
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/hcl/v2 v2.24.0
|
||||
github.com/iancoleman/strcase v0.3.0
|
||||
github.com/invopop/jsonschema v0.7.0
|
||||
github.com/jedib0t/go-pretty/v6 v6.6.8
|
||||
github.com/jedib0t/go-pretty/v6 v6.7.7
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953
|
||||
github.com/magiconair/properties v1.8.10
|
||||
github.com/mholt/archives v0.1.3
|
||||
github.com/mholt/archives v0.1.5
|
||||
github.com/moby/sys/mountinfo v0.7.2
|
||||
github.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1
|
||||
github.com/olekukonko/tablewriter v1.0.9
|
||||
github.com/olekukonko/tablewriter v1.1.2
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.22
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.23
|
||||
github.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d
|
||||
github.com/sanity-io/litter v1.5.8
|
||||
@@ -78,10 +77,10 @@ require (
|
||||
github.com/sergi/go-diff v1.4.0
|
||||
github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb
|
||||
github.com/spdx/tools-golang v0.5.5
|
||||
github.com/spf13/afero v1.14.0
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.11.0
|
||||
github.com/vbatts/go-mtree v0.5.4
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/vbatts/go-mtree v0.6.0
|
||||
github.com/vifraa/gopom v1.0.0
|
||||
github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651
|
||||
github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0
|
||||
@@ -90,9 +89,9 @@ require (
|
||||
go.uber.org/goleak v1.3.0
|
||||
go.yaml.in/yaml/v3 v3.0.4
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
|
||||
golang.org/x/mod v0.27.0
|
||||
golang.org/x/net v0.43.0
|
||||
modernc.org/sqlite v1.38.2
|
||||
golang.org/x/mod v0.31.0
|
||||
golang.org/x/net v0.48.0
|
||||
modernc.org/sqlite v1.40.1
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -109,30 +108,29 @@ require (
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/Microsoft/hcsshim v0.11.7 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.2.0 // indirect
|
||||
github.com/STARRY-S/zip v0.2.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||
github.com/STARRY-S/zip v0.2.3 // indirect
|
||||
github.com/agext/levenshtein v1.2.1 // indirect; indirectt
|
||||
github.com/anchore/go-lzo v0.1.0 // indirect
|
||||
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect
|
||||
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/aquasecurity/go-version v0.0.1 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aws/aws-sdk-go v1.44.122 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/becheran/wildmatch-go v1.0.0 // indirect
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
|
||||
github.com/bodgit/plumbing v1.3.0 // indirect
|
||||
github.com/bodgit/sevenzip v1.6.0 // indirect
|
||||
github.com/bodgit/sevenzip v1.6.1 // indirect
|
||||
github.com/bodgit/windows v1.0.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.9.3 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/containerd/cgroups v1.1.0 // indirect
|
||||
github.com/containerd/containerd v1.7.28 // indirect
|
||||
github.com/containerd/containerd v1.7.29 // indirect
|
||||
github.com/containerd/containerd/api v1.8.0 // indirect
|
||||
github.com/containerd/continuity v0.4.4 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
@@ -140,64 +138,60 @@ require (
|
||||
github.com/containerd/fifo v1.1.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect
|
||||
github.com/containerd/ttrpc v1.2.7 // indirect
|
||||
github.com/containerd/typeurl/v2 v2.2.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/docker/cli v28.3.2+incompatible // indirect
|
||||
github.com/docker/cli v29.1.2+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker v28.3.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||
github.com/docker/docker v28.5.2+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.4 // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/fatih/color v1.17.0 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/felixge/fgprof v0.9.5 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
|
||||
github.com/gkampitakis/ciinfo v0.3.2 // indirect
|
||||
github.com/gkampitakis/go-diff v1.3.2 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-restruct/restruct v1.2.0-alpha // indirect
|
||||
github.com/goccy/go-yaml v1.18.0
|
||||
github.com/goccy/go-yaml v1.19.1
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
|
||||
github.com/google/s2a-go v0.1.8 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-safetemp v1.0.0 // indirect
|
||||
github.com/hashicorp/go-version v1.6.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect
|
||||
github.com/iancoleman/orderedmap v0.3.0
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/maruel/natural v1.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
||||
github.com/minio/minlz v1.0.0 // indirect
|
||||
github.com/minio/minlz v1.0.1 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
@@ -212,13 +206,9 @@ require (
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/nwaples/rardecode v1.1.3 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.1.0 // indirect
|
||||
github.com/olekukonko/errors v1.1.0 // indirect
|
||||
github.com/olekukonko/ll v0.0.9 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.1.0 // indirect
|
||||
github.com/opencontainers/selinux v1.11.0 // indirect
|
||||
github.com/opencontainers/selinux v1.13.0 // indirect
|
||||
github.com/pborman/indent v1.2.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
@@ -235,22 +225,22 @@ require (
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.5 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
github.com/spf13/viper v1.20.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/sylabs/sif/v2 v2.21.1 // indirect
|
||||
github.com/sylabs/sif/v2 v2.22.0 // indirect
|
||||
github.com/sylabs/squashfs v1.0.6 // indirect
|
||||
github.com/therootcompany/xz v1.0.1 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||
github.com/vbatts/tar-split v0.12.1 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/vbatts/tar-split v0.12.2 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
@@ -267,28 +257,69 @@ require (
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/term v0.34.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.35.0
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/oauth2 v0.33.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/term v0.38.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/time v0.14.0
|
||||
golang.org/x/tools v0.40.0
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
google.golang.org/api v0.203.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241113202542-65e8d215514f // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
|
||||
google.golang.org/grpc v1.67.3 // indirect
|
||||
google.golang.org/protobuf v1.36.4 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.66.3 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/libc v1.66.10 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0
|
||||
github.com/gpustack/gguf-parser-go v0.22.1
|
||||
)
|
||||
|
||||
require (
|
||||
cyphar.com/go-pathrs v0.2.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.80.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect
|
||||
github.com/aws/smithy-go v1.23.2 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.6.0 // indirect
|
||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.65 // indirect
|
||||
github.com/henvic/httpretty v0.1.4 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||
github.com/olekukonko/errors v1.1.0 // indirect
|
||||
github.com/olekukonko/ll v0.1.3 // indirect
|
||||
github.com/smallnest/ringbuffer v0.0.0-20241116012123-461381446e3d // indirect
|
||||
gonum.org/v1/gonum v0.15.1 // indirect
|
||||
)
|
||||
|
||||
retract (
|
||||
v1.25.0 // published with a replace directive (confusing for API users)
|
||||
v0.53.2
|
||||
|
||||
Vendored
+1
-1
@@ -18,7 +18,7 @@ based on the _structure_ of the provided type.
|
||||
If the structure changes in any way it will end up with a new version key and all will re populate this new key,
|
||||
ignoring cached values from older, different versions.
|
||||
The resolver will store items using the `json` package to serialize/deserialize values, so to save space
|
||||
it is encouraged to use `omitempty`. For example:
|
||||
it is encouraged to use `omitempty`. For example:
|
||||
|
||||
```go
|
||||
type myCacheItem struct {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package pkgtestobservation
|
||||
|
||||
import "time"
|
||||
|
||||
// Observations represents capability observations during testing
|
||||
type Observations struct {
|
||||
License bool `json:"license"`
|
||||
Relationships Relationship `json:"relationships"`
|
||||
FileListing Count `json:"file_listing"`
|
||||
FileDigests Count `json:"file_digests"`
|
||||
IntegrityHash Count `json:"integrity_hash"`
|
||||
}
|
||||
|
||||
// Relationship tracks dependency relationship observations
|
||||
type Relationship struct {
|
||||
Found bool `json:"found"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// Count tracks whether a capability was found and how many times
|
||||
type Count struct {
|
||||
Found bool `json:"found"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// Test is the root structure for test-observations.json
|
||||
type Test struct {
|
||||
Package string `json:"package"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Catalogers map[string]*Cataloger `json:"catalogers"`
|
||||
Parsers map[string]*Parser `json:"parsers"`
|
||||
}
|
||||
|
||||
// Parser captures all observations for a parser
|
||||
type Parser struct {
|
||||
MetadataTypes []string `json:"metadata_types"`
|
||||
PackageTypes []string `json:"package_types"`
|
||||
Observations Observations `json:"observations"`
|
||||
}
|
||||
|
||||
// Cataloger captures all observations for a cataloger
|
||||
type Cataloger struct {
|
||||
MetadataTypes []string `json:"metadata_types"`
|
||||
PackageTypes []string `json:"package_types"`
|
||||
Observations Observations `json:"observations"`
|
||||
}
|
||||
@@ -3,5 +3,12 @@ package internal
|
||||
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 = "16.0.39"
|
||||
JSONSchemaVersion = "16.1.3"
|
||||
|
||||
// Changelog
|
||||
// 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field).
|
||||
// 16.1.1 - correct elf package osCpe field according to the document of systemd (also add appCpe field)
|
||||
// 16.1.2 - add DotnetDepsEntry.type package metadata field
|
||||
// 16.1.3 - add file executable toolchain and symbol information
|
||||
|
||||
)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/archives"
|
||||
)
|
||||
|
||||
// compoundExtensionAliases maps shorthand archive extensions to their full forms.
|
||||
// The mholt/archives library doesn't recognize these aliases natively.
|
||||
//
|
||||
// See: https://github.com/anchore/syft/issues/4416
|
||||
// Reference: https://github.com/mholt/archives?tab=readme-ov-file#supported-compression-formats
|
||||
var compoundExtensionAliases = map[string]string{
|
||||
".tgz": ".tar.gz",
|
||||
".tbz2": ".tar.bz2",
|
||||
".txz": ".tar.xz",
|
||||
".tlz": ".tar.lz",
|
||||
".tzst": ".tar.zst",
|
||||
}
|
||||
|
||||
// IdentifyArchive is a wrapper around archives.Identify that handles compound extension
|
||||
// aliases (like .tgz -> .tar.gz) transparently. It first attempts filename-based detection
|
||||
// using the alias map, and falls back to content-based detection if needed.
|
||||
//
|
||||
// This function is a drop-in replacement for archives.Identify that centralizes
|
||||
// the compound alias handling logic in one place.
|
||||
func IdentifyArchive(ctx context.Context, path string, r io.Reader) (archives.Format, io.Reader, error) {
|
||||
// First, try to identify using the alias-mapped path (filename-based detection)
|
||||
normalizedPath := handleCompoundArchiveAliases(path)
|
||||
return archives.Identify(ctx, normalizedPath, r)
|
||||
}
|
||||
|
||||
// handleCompoundArchiveAliases normalizes archive file paths that use compound extension
|
||||
// aliases (like .tgz) to their full forms (like .tar.gz) for correct identification
|
||||
// by the mholt/archives library.
|
||||
func handleCompoundArchiveAliases(path string) string {
|
||||
ext := filepath.Ext(path)
|
||||
if newExt, ok := compoundExtensionAliases[ext]; ok {
|
||||
return strings.TrimSuffix(path, ext) + newExt
|
||||
}
|
||||
return path
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHandleCompoundArchiveAliases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "tgz to tar.gz",
|
||||
input: "/path/to/archive.tgz",
|
||||
expected: "/path/to/archive.tar.gz",
|
||||
},
|
||||
{
|
||||
name: "tbz2 to tar.bz2",
|
||||
input: "/path/to/archive.tbz2",
|
||||
expected: "/path/to/archive.tar.bz2",
|
||||
},
|
||||
{
|
||||
name: "txz to tar.xz",
|
||||
input: "/path/to/archive.txz",
|
||||
expected: "/path/to/archive.tar.xz",
|
||||
},
|
||||
{
|
||||
name: "tlz to tar.lz",
|
||||
input: "/path/to/archive.tlz",
|
||||
expected: "/path/to/archive.tar.lz",
|
||||
},
|
||||
{
|
||||
name: "tzst to tar.zst",
|
||||
input: "/path/to/archive.tzst",
|
||||
expected: "/path/to/archive.tar.zst",
|
||||
},
|
||||
{
|
||||
name: "standard tar.gz unchanged",
|
||||
input: "/path/to/archive.tar.gz",
|
||||
expected: "/path/to/archive.tar.gz",
|
||||
},
|
||||
{
|
||||
name: "zip unchanged",
|
||||
input: "/path/to/archive.zip",
|
||||
expected: "/path/to/archive.zip",
|
||||
},
|
||||
{
|
||||
name: "no extension unchanged",
|
||||
input: "/path/to/archive",
|
||||
expected: "/path/to/archive",
|
||||
},
|
||||
{
|
||||
name: "case sensitive - TGZ not matched",
|
||||
input: "/path/to/archive.TGZ",
|
||||
expected: "/path/to/archive.TGZ",
|
||||
},
|
||||
{
|
||||
name: "just filename with tgz",
|
||||
input: "archive.tgz",
|
||||
expected: "archive.tar.gz",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := handleCompoundArchiveAliases(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package file
|
||||
|
||||
// GlobMatch evaluates the given glob pattern against the given "name" string, indicating if there is a match or not.
|
||||
// Source: https://research.swtch.com/glob.go
|
||||
func GlobMatch(pattern, name string) bool {
|
||||
px := 0
|
||||
nx := 0
|
||||
nextPx := 0
|
||||
nextNx := 0
|
||||
for px < len(pattern) || nx < len(name) {
|
||||
if px < len(pattern) {
|
||||
c := pattern[px]
|
||||
switch c {
|
||||
default: // ordinary character
|
||||
if nx < len(name) && name[nx] == c {
|
||||
px++
|
||||
nx++
|
||||
continue
|
||||
}
|
||||
case '?': // single-character wildcard
|
||||
if nx < len(name) {
|
||||
px++
|
||||
nx++
|
||||
continue
|
||||
}
|
||||
case '*': // zero-or-more-character wildcard
|
||||
// Try to match at nx.
|
||||
// If that doesn't work out,
|
||||
// restart at nx+1 next.
|
||||
nextPx = px
|
||||
nextNx = nx + 1
|
||||
px++
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Mismatch. Maybe restart.
|
||||
if 0 < nextNx && nextNx <= len(name) {
|
||||
px = nextPx
|
||||
nx = nextNx
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
// Matched all of pattern to all of name. Success.
|
||||
return true
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGlobMatch(t *testing.T) {
|
||||
var tests = []struct {
|
||||
pattern string
|
||||
data string
|
||||
ok bool
|
||||
}{
|
||||
{"", "", true},
|
||||
{"x", "", false},
|
||||
{"", "x", false},
|
||||
{"abc", "abc", true},
|
||||
{"*", "abc", true},
|
||||
{"*c", "abc", true},
|
||||
{"*b", "abc", false},
|
||||
{"a*", "abc", true},
|
||||
{"b*", "abc", false},
|
||||
{"a*", "a", true},
|
||||
{"*a", "a", true},
|
||||
{"a*b*c*d*e*", "axbxcxdxe", true},
|
||||
{"a*b*c*d*e*", "axbxcxdxexxx", true},
|
||||
{"a*b?c*x", "abxbbxdbxebxczzx", true},
|
||||
{"a*b?c*x", "abxbbxdbxebxczzy", false},
|
||||
{"a*a*a*a*b", strings.Repeat("a", 100), false},
|
||||
{"*x", "xxx", true},
|
||||
{"/home/place/**", "/home/place/a/thing", true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if GlobMatch(test.pattern, test.data) != test.ok {
|
||||
t.Errorf("failed glob='%s' data='%s'", test.pattern, test.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,40 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
"github.com/mholt/archives"
|
||||
|
||||
"github.com/anchore/archiver/v3"
|
||||
"github.com/anchore/syft/internal"
|
||||
)
|
||||
|
||||
// TraverseFilesInTar enumerates all paths stored within a tar archive using the visitor pattern.
|
||||
func TraverseFilesInTar(ctx context.Context, archivePath string, visitor archives.FileHandler) error {
|
||||
tarReader, err := os.Open(archivePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open tar archive (%s): %w", archivePath, err)
|
||||
}
|
||||
defer internal.CloseAndLogError(tarReader, archivePath)
|
||||
|
||||
format, _, err := IdentifyArchive(ctx, archivePath, tarReader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to identify tar compression format: %w", err)
|
||||
}
|
||||
|
||||
extractor, ok := format.(archives.Extractor)
|
||||
if !ok {
|
||||
return fmt.Errorf("file format does not support extraction: %s", archivePath)
|
||||
}
|
||||
|
||||
return extractor.Extract(ctx, tarReader, visitor)
|
||||
}
|
||||
|
||||
// ExtractGlobsFromTarToUniqueTempFile extracts paths matching the given globs within the given archive to a temporary directory, returning file openers for each file extracted.
|
||||
func ExtractGlobsFromTarToUniqueTempFile(archivePath, dir string, globs ...string) (map[string]Opener, error) {
|
||||
func ExtractGlobsFromTarToUniqueTempFile(ctx context.Context, archivePath, dir string, globs ...string) (map[string]Opener, error) {
|
||||
results := make(map[string]Opener)
|
||||
|
||||
// don't allow for full traversal, only select traversal from given paths
|
||||
@@ -19,9 +42,7 @@ func ExtractGlobsFromTarToUniqueTempFile(archivePath, dir string, globs ...strin
|
||||
return results, nil
|
||||
}
|
||||
|
||||
visitor := func(file archiver.File) error {
|
||||
defer file.Close()
|
||||
|
||||
visitor := func(_ context.Context, file archives.FileInfo) error {
|
||||
// ignore directories
|
||||
if file.IsDir() {
|
||||
return nil
|
||||
@@ -43,7 +64,13 @@ func ExtractGlobsFromTarToUniqueTempFile(archivePath, dir string, globs ...strin
|
||||
// provides a ReadCloser. It is up to the caller to handle closing the file explicitly.
|
||||
defer tempFile.Close()
|
||||
|
||||
if err := safeCopy(tempFile, file.ReadCloser); err != nil {
|
||||
packedFile, err := file.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read file=%q from tar=%q: %w", file.NameInArchive, archivePath, err)
|
||||
}
|
||||
defer internal.CloseAndLogError(packedFile, archivePath)
|
||||
|
||||
if err := safeCopy(tempFile, packedFile); err != nil {
|
||||
return fmt.Errorf("unable to copy source=%q for tar=%q: %w", file.Name(), archivePath, err)
|
||||
}
|
||||
|
||||
@@ -52,7 +79,7 @@ func ExtractGlobsFromTarToUniqueTempFile(archivePath, dir string, globs ...strin
|
||||
return nil
|
||||
}
|
||||
|
||||
return results, archiver.Walk(archivePath, visitor)
|
||||
return results, TraverseFilesInTar(ctx, archivePath, visitor)
|
||||
}
|
||||
|
||||
func matchesAnyGlob(name string, globs ...string) bool {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
this file is in a subdirectory
|
||||
@@ -7,14 +7,14 @@ import (
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var expectedZipArchiveEntries = []string{
|
||||
"some-dir" + string(os.PathSeparator),
|
||||
filepath.Join("some-dir", "a-file.txt"),
|
||||
"some-dir/",
|
||||
"some-dir/a-file.txt",
|
||||
"b-file.txt",
|
||||
"b-file/",
|
||||
"b-file/in-subdir.txt",
|
||||
"nested.zip",
|
||||
}
|
||||
|
||||
@@ -59,12 +59,6 @@ func createZipArchive(t testing.TB, sourceDirPath, destinationArchivePath string
|
||||
|
||||
}
|
||||
|
||||
func assertNoError(t testing.TB, fn func() error) func() {
|
||||
return func() {
|
||||
assert.NoError(t, fn())
|
||||
}
|
||||
}
|
||||
|
||||
// setupZipFileTest encapsulates common test setup work for zip file tests. It returns a cleanup function,
|
||||
// which should be called (typically deferred) by the caller, the path of the created zip archive, and an error,
|
||||
// which should trigger a fatal test failure in the consuming test. The returned cleanup function will never be nil
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
"github.com/mholt/archives"
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
@@ -14,22 +17,25 @@ import (
|
||||
type ZipFileManifest map[string]os.FileInfo
|
||||
|
||||
// NewZipFileManifest creates and returns a new ZipFileManifest populated with path and metadata from the given zip archive path.
|
||||
func NewZipFileManifest(archivePath string) (ZipFileManifest, error) {
|
||||
zipReader, err := OpenZip(archivePath)
|
||||
func NewZipFileManifest(ctx context.Context, archivePath string) (ZipFileManifest, error) {
|
||||
zipReader, err := os.Open(archivePath)
|
||||
manifest := make(ZipFileManifest)
|
||||
if err != nil {
|
||||
log.Debugf("unable to open zip archive (%s): %v", archivePath, err)
|
||||
return manifest, err
|
||||
}
|
||||
defer func() {
|
||||
err = zipReader.Close()
|
||||
if err != nil {
|
||||
if err = zipReader.Close(); err != nil {
|
||||
log.Debugf("unable to close zip archive (%s): %+v", archivePath, err)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, file := range zipReader.File {
|
||||
manifest.Add(file.Name, file.FileInfo())
|
||||
err = archives.Zip{}.Extract(ctx, zipReader, func(_ context.Context, file archives.FileInfo) error {
|
||||
manifest.Add(file.NameInArchive, file.FileInfo)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return manifest, err
|
||||
}
|
||||
return manifest, nil
|
||||
}
|
||||
@@ -39,12 +45,17 @@ func (z ZipFileManifest) Add(entry string, info os.FileInfo) {
|
||||
z[entry] = info
|
||||
}
|
||||
|
||||
// GlobMatch returns the path keys that match the given value(s).
|
||||
// GlobMatch returns the path keys to files (not directories) that match the given value(s).
|
||||
func (z ZipFileManifest) GlobMatch(caseInsensitive bool, patterns ...string) []string {
|
||||
uniqueMatches := strset.New()
|
||||
|
||||
for _, pattern := range patterns {
|
||||
for entry := range z {
|
||||
fileInfo := z[entry]
|
||||
if fileInfo != nil && fileInfo.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
// We want to match globs as if entries begin with a leading slash (akin to an absolute path)
|
||||
// so that glob logic is consistent inside and outside of ZIP archives
|
||||
normalizedEntry := normalizeZipEntryName(caseInsensitive, entry)
|
||||
@@ -52,7 +63,13 @@ func (z ZipFileManifest) GlobMatch(caseInsensitive bool, patterns ...string) []s
|
||||
if caseInsensitive {
|
||||
pattern = strings.ToLower(pattern)
|
||||
}
|
||||
if GlobMatch(pattern, normalizedEntry) {
|
||||
|
||||
matches, err := doublestar.Match(pattern, normalizedEntry)
|
||||
if err != nil {
|
||||
log.Debugf("error with match pattern '%s', including by default: %v", pattern, err)
|
||||
matches = true
|
||||
}
|
||||
if matches {
|
||||
uniqueMatches.Add(entry)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,13 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewZipFileManifest(t *testing.T) {
|
||||
@@ -24,7 +27,7 @@ func TestNewZipFileManifest(t *testing.T) {
|
||||
|
||||
archiveFilePath := setupZipFileTest(t, sourceDirPath, false)
|
||||
|
||||
actual, err := NewZipFileManifest(archiveFilePath)
|
||||
actual, err := NewZipFileManifest(context.Background(), archiveFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to extract from unzip archive: %+v", err)
|
||||
}
|
||||
@@ -59,7 +62,7 @@ func TestNewZip64FileManifest(t *testing.T) {
|
||||
sourceDirPath := path.Join(cwd, "test-fixtures", "zip-source")
|
||||
archiveFilePath := setupZipFileTest(t, sourceDirPath, true)
|
||||
|
||||
actual, err := NewZipFileManifest(archiveFilePath)
|
||||
actual, err := NewZipFileManifest(context.Background(), archiveFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to extract from unzip archive: %+v", err)
|
||||
}
|
||||
@@ -99,30 +102,34 @@ func TestZipFileManifest_GlobMatch(t *testing.T) {
|
||||
|
||||
archiveFilePath := setupZipFileTest(t, sourceDirPath, false)
|
||||
|
||||
z, err := NewZipFileManifest(archiveFilePath)
|
||||
z, err := NewZipFileManifest(context.Background(), archiveFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to extract from unzip archive: %+v", err)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
glob string
|
||||
expected string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
"/b*",
|
||||
"b-file.txt",
|
||||
[]string{"b-file.txt"},
|
||||
},
|
||||
{
|
||||
"*/a-file.txt",
|
||||
"some-dir/a-file.txt",
|
||||
"/b*/**",
|
||||
[]string{"b-file.txt", "b-file/in-subdir.txt"},
|
||||
},
|
||||
{
|
||||
"*/A-file.txt",
|
||||
"some-dir/a-file.txt",
|
||||
"**/a-file.txt",
|
||||
[]string{"some-dir/a-file.txt"},
|
||||
},
|
||||
{
|
||||
"**/A-file.txt",
|
||||
[]string{"some-dir/a-file.txt"},
|
||||
},
|
||||
{
|
||||
"**/*.zip",
|
||||
"nested.zip",
|
||||
[]string{"nested.zip"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -132,11 +139,7 @@ func TestZipFileManifest_GlobMatch(t *testing.T) {
|
||||
|
||||
results := z.GlobMatch(true, glob)
|
||||
|
||||
if len(results) == 1 && results[0] == tc.expected {
|
||||
return
|
||||
}
|
||||
|
||||
t.Errorf("unexpected results for glob '%s': %+v", glob, results)
|
||||
require.ElementsMatch(t, tc.expected, results)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/archives"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
@@ -25,7 +27,7 @@ type errZipSlipDetected struct {
|
||||
}
|
||||
|
||||
func (e *errZipSlipDetected) Error() string {
|
||||
return fmt.Sprintf("paths are not allowed to resolve outside of the root prefix (%q). Destination: %q", e.Prefix, e.JoinArgs)
|
||||
return fmt.Sprintf("path traversal detected: paths are not allowed to resolve outside of the root prefix (%q). Destination: %q", e.Prefix, e.JoinArgs)
|
||||
}
|
||||
|
||||
type zipTraversalRequest map[string]struct{}
|
||||
@@ -39,38 +41,34 @@ func newZipTraverseRequest(paths ...string) zipTraversalRequest {
|
||||
}
|
||||
|
||||
// TraverseFilesInZip enumerates all paths stored within a zip archive using the visitor pattern.
|
||||
func TraverseFilesInZip(archivePath string, visitor func(*zip.File) error, paths ...string) error {
|
||||
func TraverseFilesInZip(ctx context.Context, archivePath string, visitor archives.FileHandler, paths ...string) error {
|
||||
request := newZipTraverseRequest(paths...)
|
||||
|
||||
zipReader, err := OpenZip(archivePath)
|
||||
zipReader, err := os.Open(archivePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open zip archive (%s): %w", archivePath, err)
|
||||
}
|
||||
defer func() {
|
||||
err = zipReader.Close()
|
||||
if err != nil {
|
||||
if err := zipReader.Close(); err != nil {
|
||||
log.Errorf("unable to close zip archive (%s): %+v", archivePath, err)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, file := range zipReader.File {
|
||||
return archives.Zip{}.Extract(ctx, zipReader, func(ctx context.Context, file archives.FileInfo) error {
|
||||
// if no paths are given then assume that all files should be traversed
|
||||
if len(paths) > 0 {
|
||||
if _, ok := request[file.Name]; !ok {
|
||||
if _, ok := request[file.NameInArchive]; !ok {
|
||||
// this file path is not of interest
|
||||
continue
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if err = visitor(file); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return visitor(ctx, file)
|
||||
})
|
||||
}
|
||||
|
||||
// ExtractFromZipToUniqueTempFile extracts select paths for the given archive to a temporary directory, returning file openers for each file extracted.
|
||||
func ExtractFromZipToUniqueTempFile(archivePath, dir string, paths ...string) (map[string]Opener, error) {
|
||||
func ExtractFromZipToUniqueTempFile(ctx context.Context, archivePath, dir string, paths ...string) (map[string]Opener, error) {
|
||||
results := make(map[string]Opener)
|
||||
|
||||
// don't allow for full traversal, only select traversal from given paths
|
||||
@@ -78,9 +76,8 @@ func ExtractFromZipToUniqueTempFile(archivePath, dir string, paths ...string) (m
|
||||
return results, nil
|
||||
}
|
||||
|
||||
visitor := func(file *zip.File) error {
|
||||
tempfilePrefix := filepath.Base(filepath.Clean(file.Name)) + "-"
|
||||
|
||||
visitor := func(_ context.Context, file archives.FileInfo) error {
|
||||
tempfilePrefix := filepath.Base(filepath.Clean(file.NameInArchive)) + "-"
|
||||
tempFile, err := os.CreateTemp(dir, tempfilePrefix)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create temp file: %w", err)
|
||||
@@ -92,33 +89,32 @@ func ExtractFromZipToUniqueTempFile(archivePath, dir string, paths ...string) (m
|
||||
|
||||
zippedFile, err := file.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read file=%q from zip=%q: %w", file.Name, archivePath, err)
|
||||
return fmt.Errorf("unable to read file=%q from zip=%q: %w", file.NameInArchive, archivePath, err)
|
||||
}
|
||||
defer func() {
|
||||
err := zippedFile.Close()
|
||||
if err != nil {
|
||||
log.Errorf("unable to close source file=%q from zip=%q: %+v", file.Name, archivePath, err)
|
||||
if err := zippedFile.Close(); err != nil {
|
||||
log.Errorf("unable to close source file=%q from zip=%q: %+v", file.NameInArchive, archivePath, err)
|
||||
}
|
||||
}()
|
||||
|
||||
if file.FileInfo().IsDir() {
|
||||
return fmt.Errorf("unable to extract directories, only files: %s", file.Name)
|
||||
if file.IsDir() {
|
||||
return fmt.Errorf("unable to extract directories, only files: %s", file.NameInArchive)
|
||||
}
|
||||
|
||||
if err := safeCopy(tempFile, zippedFile); err != nil {
|
||||
return fmt.Errorf("unable to copy source=%q for zip=%q: %w", file.Name, archivePath, err)
|
||||
return fmt.Errorf("unable to copy source=%q for zip=%q: %w", file.NameInArchive, archivePath, err)
|
||||
}
|
||||
|
||||
results[file.Name] = Opener{path: tempFile.Name()}
|
||||
results[file.NameInArchive] = Opener{path: tempFile.Name()}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return results, TraverseFilesInZip(archivePath, visitor, paths...)
|
||||
return results, TraverseFilesInZip(ctx, archivePath, visitor, paths...)
|
||||
}
|
||||
|
||||
// ContentsFromZip extracts select paths for the given archive and returns a set of string contents for each path.
|
||||
func ContentsFromZip(archivePath string, paths ...string) (map[string]string, error) {
|
||||
func ContentsFromZip(ctx context.Context, archivePath string, paths ...string) (map[string]string, error) {
|
||||
results := make(map[string]string)
|
||||
|
||||
// don't allow for full traversal, only select traversal from given paths
|
||||
@@ -126,37 +122,38 @@ func ContentsFromZip(archivePath string, paths ...string) (map[string]string, er
|
||||
return results, nil
|
||||
}
|
||||
|
||||
visitor := func(file *zip.File) error {
|
||||
visitor := func(_ context.Context, file archives.FileInfo) error {
|
||||
zippedFile, err := file.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read file=%q from zip=%q: %w", file.Name, archivePath, err)
|
||||
return fmt.Errorf("unable to read file=%q from zip=%q: %w", file.NameInArchive, archivePath, err)
|
||||
}
|
||||
defer func() {
|
||||
if err := zippedFile.Close(); err != nil {
|
||||
log.Errorf("unable to close source file=%q from zip=%q: %+v", file.NameInArchive, archivePath, err)
|
||||
}
|
||||
}()
|
||||
|
||||
if file.FileInfo().IsDir() {
|
||||
return fmt.Errorf("unable to extract directories, only files: %s", file.Name)
|
||||
if file.IsDir() {
|
||||
return fmt.Errorf("unable to extract directories, only files: %s", file.NameInArchive)
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
if err := safeCopy(&buffer, zippedFile); err != nil {
|
||||
return fmt.Errorf("unable to copy source=%q for zip=%q: %w", file.Name, archivePath, err)
|
||||
return fmt.Errorf("unable to copy source=%q for zip=%q: %w", file.NameInArchive, archivePath, err)
|
||||
}
|
||||
|
||||
results[file.Name] = buffer.String()
|
||||
results[file.NameInArchive] = buffer.String()
|
||||
|
||||
err = zippedFile.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to close source file=%q from zip=%q: %w", file.Name, archivePath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return results, TraverseFilesInZip(archivePath, visitor, paths...)
|
||||
return results, TraverseFilesInZip(ctx, archivePath, visitor, paths...)
|
||||
}
|
||||
|
||||
// UnzipToDir extracts a zip archive to a target directory.
|
||||
func UnzipToDir(archivePath, targetDir string) error {
|
||||
visitor := func(file *zip.File) error {
|
||||
joinedPath, err := safeJoin(targetDir, file.Name)
|
||||
func UnzipToDir(ctx context.Context, archivePath, targetDir string) error {
|
||||
visitor := func(_ context.Context, file archives.FileInfo) error {
|
||||
joinedPath, err := SafeJoin(targetDir, file.NameInArchive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -164,11 +161,11 @@ func UnzipToDir(archivePath, targetDir string) error {
|
||||
return extractSingleFile(file, joinedPath, archivePath)
|
||||
}
|
||||
|
||||
return TraverseFilesInZip(archivePath, visitor)
|
||||
return TraverseFilesInZip(ctx, archivePath, visitor)
|
||||
}
|
||||
|
||||
// safeJoin ensures that any destinations do not resolve to a path above the prefix path.
|
||||
func safeJoin(prefix string, dest ...string) (string, error) {
|
||||
// SafeJoin ensures that any destinations do not resolve to a path above the prefix path.
|
||||
func SafeJoin(prefix string, dest ...string) (string, error) {
|
||||
joinResult := filepath.Join(append([]string{prefix}, dest...)...)
|
||||
cleanJoinResult := filepath.Clean(joinResult)
|
||||
if !strings.HasPrefix(cleanJoinResult, filepath.Clean(prefix)) {
|
||||
@@ -181,13 +178,18 @@ func safeJoin(prefix string, dest ...string) (string, error) {
|
||||
return joinResult, nil
|
||||
}
|
||||
|
||||
func extractSingleFile(file *zip.File, expandedFilePath, archivePath string) error {
|
||||
func extractSingleFile(file archives.FileInfo, expandedFilePath, archivePath string) error {
|
||||
zippedFile, err := file.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read file=%q from zip=%q: %w", file.Name, archivePath, err)
|
||||
return fmt.Errorf("unable to read file=%q from zip=%q: %w", file.NameInArchive, archivePath, err)
|
||||
}
|
||||
defer func() {
|
||||
if err := zippedFile.Close(); err != nil {
|
||||
log.Errorf("unable to close source file=%q from zip=%q: %+v", file.NameInArchive, archivePath, err)
|
||||
}
|
||||
}()
|
||||
|
||||
if file.FileInfo().IsDir() {
|
||||
if file.IsDir() {
|
||||
err = os.MkdirAll(expandedFilePath, file.Mode())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create dir=%q from zip=%q: %w", expandedFilePath, archivePath, err)
|
||||
@@ -202,20 +204,16 @@ func extractSingleFile(file *zip.File, expandedFilePath, archivePath string) err
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create dest file=%q from zip=%q: %w", expandedFilePath, archivePath, err)
|
||||
}
|
||||
defer func() {
|
||||
if err := outputFile.Close(); err != nil {
|
||||
log.Errorf("unable to close dest file=%q from zip=%q: %+v", outputFile.Name(), archivePath, err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := safeCopy(outputFile, zippedFile); err != nil {
|
||||
return fmt.Errorf("unable to copy source=%q to dest=%q for zip=%q: %w", file.Name, outputFile.Name(), archivePath, err)
|
||||
}
|
||||
|
||||
err = outputFile.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to close dest file=%q from zip=%q: %w", outputFile.Name(), archivePath, err)
|
||||
return fmt.Errorf("unable to copy source=%q to dest=%q for zip=%q: %w", file.NameInArchive, outputFile.Name(), archivePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
err = zippedFile.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to close source file=%q from zip=%q: %w", file.Name, archivePath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -17,6 +19,7 @@ import (
|
||||
|
||||
"github.com/go-test/deep"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func equal(r1, r2 io.Reader) (bool, error) {
|
||||
@@ -55,7 +58,7 @@ func TestUnzipToDir(t *testing.T) {
|
||||
expectedPaths := len(expectedZipArchiveEntries)
|
||||
observedPaths := 0
|
||||
|
||||
err = UnzipToDir(archiveFilePath, unzipDestinationDir)
|
||||
err = UnzipToDir(context.Background(), archiveFilePath, unzipDestinationDir)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to unzip archive: %+v", err)
|
||||
}
|
||||
@@ -145,7 +148,7 @@ func TestContentsFromZip(t *testing.T) {
|
||||
paths = append(paths, p)
|
||||
}
|
||||
|
||||
actual, err := ContentsFromZip(archivePath, paths...)
|
||||
actual, err := ContentsFromZip(context.Background(), archivePath, paths...)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to extract from unzip archive: %+v", err)
|
||||
}
|
||||
@@ -307,9 +310,528 @@ func TestSafeJoin(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%+v:%+v", test.prefix, test.args), func(t *testing.T) {
|
||||
actual, err := safeJoin(test.prefix, test.args...)
|
||||
actual, err := SafeJoin(test.prefix, test.args...)
|
||||
test.errAssertion(t, err)
|
||||
assert.Equal(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSymlinkProtection demonstrates that SafeJoin protects against symlink-based
|
||||
// directory traversal attacks by validating that archive entry paths cannot escape
|
||||
// the extraction directory.
|
||||
func TestSafeJoin_SymlinkProtection(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
archivePath string // Path as it would appear in the archive
|
||||
expectError bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "path traversal via ../",
|
||||
archivePath: "../../../outside/file.txt",
|
||||
expectError: true,
|
||||
description: "Archive entry with ../ trying to escape extraction dir",
|
||||
},
|
||||
{
|
||||
name: "absolute path symlink target",
|
||||
archivePath: "../../../sensitive.txt",
|
||||
expectError: true,
|
||||
description: "Simulates symlink pointing outside via relative path",
|
||||
},
|
||||
{
|
||||
name: "safe relative path within extraction dir",
|
||||
archivePath: "subdir/safe.txt",
|
||||
expectError: false,
|
||||
description: "Normal file path that stays within extraction directory",
|
||||
},
|
||||
{
|
||||
name: "safe path with internal ../",
|
||||
archivePath: "dir1/../dir2/file.txt",
|
||||
expectError: false,
|
||||
description: "Path with ../ that still resolves within extraction dir",
|
||||
},
|
||||
{
|
||||
name: "deeply nested traversal",
|
||||
archivePath: "../../../../../../tmp/evil.txt",
|
||||
expectError: true,
|
||||
description: "Multiple levels of ../ trying to escape",
|
||||
},
|
||||
{
|
||||
name: "single parent directory escape",
|
||||
archivePath: "../",
|
||||
expectError: true,
|
||||
description: "Simple one-level escape attempt",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create temp directories to simulate extraction scenario
|
||||
tmpDir := t.TempDir()
|
||||
extractDir := filepath.Join(tmpDir, "extract")
|
||||
outsideDir := filepath.Join(tmpDir, "outside")
|
||||
|
||||
require.NoError(t, os.MkdirAll(extractDir, 0755))
|
||||
require.NoError(t, os.MkdirAll(outsideDir, 0755))
|
||||
|
||||
// Create a file outside extraction dir that an attacker might target
|
||||
outsideFile := filepath.Join(outsideDir, "sensitive.txt")
|
||||
require.NoError(t, os.WriteFile(outsideFile, []byte("sensitive data"), 0644))
|
||||
|
||||
// Test SafeJoin - this is what happens when processing archive entries
|
||||
result, err := SafeJoin(extractDir, tt.archivePath)
|
||||
|
||||
if tt.expectError {
|
||||
// Should block malicious paths
|
||||
require.Error(t, err, "Expected SafeJoin to reject malicious path")
|
||||
var zipSlipErr *errZipSlipDetected
|
||||
assert.ErrorAs(t, err, &zipSlipErr, "Error should be errZipSlipDetected type")
|
||||
assert.Empty(t, result, "Result should be empty for blocked paths")
|
||||
} else {
|
||||
// Should allow safe paths
|
||||
require.NoError(t, err, "Expected SafeJoin to allow safe path")
|
||||
assert.NotEmpty(t, result, "Result should not be empty for safe paths")
|
||||
assert.True(t, strings.HasPrefix(filepath.Clean(result), filepath.Clean(extractDir)),
|
||||
"Safe path should resolve within extraction directory")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnzipToDir_SymlinkAttacks tests UnzipToDir function with malicious ZIP archives
|
||||
// containing symlink entries that attempt path traversal attacks.
|
||||
//
|
||||
// EXPECTED BEHAVIOR: UnzipToDir should either:
|
||||
// 1. Detect and reject symlinks explicitly with a security error, OR
|
||||
// 2. Extract them safely (library converts symlinks to regular files)
|
||||
func TestUnzipToDir_SymlinkAttacks(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
symlinkName string
|
||||
fileName string
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "direct symlink to outside directory",
|
||||
symlinkName: "evil_link",
|
||||
fileName: "evil_link/payload.txt",
|
||||
errContains: "not a directory", // attempt to write through symlink leaf (which is not a directory)
|
||||
},
|
||||
{
|
||||
name: "directory symlink attack",
|
||||
symlinkName: "safe_dir/link",
|
||||
fileName: "safe_dir/link/payload.txt",
|
||||
errContains: "not a directory", // attempt to write through symlink (which is not a directory)
|
||||
},
|
||||
{
|
||||
name: "symlink without payload file",
|
||||
symlinkName: "standalone_link",
|
||||
fileName: "", // no payload file
|
||||
errContains: "", // no error expected, symlink without payload is safe
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// create outside target directory
|
||||
outsideDir := filepath.Join(tempDir, "outside_target")
|
||||
require.NoError(t, os.MkdirAll(outsideDir, 0755))
|
||||
|
||||
// create extraction directory
|
||||
extractDir := filepath.Join(tempDir, "extract")
|
||||
require.NoError(t, os.MkdirAll(extractDir, 0755))
|
||||
|
||||
maliciousZip := createMaliciousZipWithSymlink(t, tempDir, tt.symlinkName, outsideDir, tt.fileName)
|
||||
|
||||
err := UnzipToDir(context.Background(), maliciousZip, extractDir)
|
||||
|
||||
// check error expectations
|
||||
if tt.errContains != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.errContains)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
analyzeExtractionDirectory(t, extractDir)
|
||||
|
||||
// check if payload file escaped extraction directory
|
||||
if tt.fileName != "" {
|
||||
maliciousFile := filepath.Join(outsideDir, filepath.Base(tt.fileName))
|
||||
checkFileOutsideExtraction(t, maliciousFile)
|
||||
}
|
||||
|
||||
// check if symlink was created pointing outside
|
||||
symlinkPath := filepath.Join(extractDir, tt.symlinkName)
|
||||
checkSymlinkCreation(t, symlinkPath, extractDir, outsideDir)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestContentsFromZip_SymlinkAttacks tests the ContentsFromZip function with malicious
|
||||
// ZIP archives containing symlink entries.
|
||||
//
|
||||
// EXPECTED BEHAVIOR: ContentsFromZip should either:
|
||||
// 1. Reject symlinks explicitly, OR
|
||||
// 2. Return empty content for symlinks (library behavior)
|
||||
//
|
||||
// Though ContentsFromZip doesn't write to disk, but if symlinks are followed, it could read sensitive
|
||||
// files from outside the archive.
|
||||
func TestContentsFromZip_SymlinkAttacks(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
symlinkName string
|
||||
symlinkTarget string
|
||||
requestPath string
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "request symlink entry directly",
|
||||
symlinkName: "evil_link",
|
||||
symlinkTarget: "/etc/hosts", // attempt to read sensitive file
|
||||
requestPath: "evil_link",
|
||||
errContains: "", // no error expected - library returns symlink metadata
|
||||
},
|
||||
{
|
||||
name: "symlink in nested directory",
|
||||
symlinkName: "nested/link",
|
||||
symlinkTarget: "/etc/hosts",
|
||||
requestPath: "nested/link",
|
||||
errContains: "", // no error expected - library returns symlink metadata
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// create malicious ZIP with symlink entry (no payload file needed)
|
||||
maliciousZip := createMaliciousZipWithSymlink(t, tempDir, tt.symlinkName, tt.symlinkTarget, "")
|
||||
|
||||
contents, err := ContentsFromZip(context.Background(), maliciousZip, tt.requestPath)
|
||||
|
||||
// check error expectations
|
||||
if tt.errContains != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.errContains)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
// verify symlink handling - library should return symlink target as content (metadata)
|
||||
content, found := contents[tt.requestPath]
|
||||
require.True(t, found, "symlink entry should be found in results")
|
||||
|
||||
// verify symlink was NOT followed (content should be target path or empty)
|
||||
if content != "" && content != tt.symlinkTarget {
|
||||
// content is not empty and not the symlink target - check if actual file was read
|
||||
if _, statErr := os.Stat(tt.symlinkTarget); statErr == nil {
|
||||
targetContent, readErr := os.ReadFile(tt.symlinkTarget)
|
||||
if readErr == nil && string(targetContent) == content {
|
||||
t.Errorf("critical issue!... symlink was FOLLOWED and external file content was read!")
|
||||
t.Logf(" symlink: %s → %s", tt.requestPath, tt.symlinkTarget)
|
||||
t.Logf(" content length: %d bytes", len(content))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestExtractFromZipToUniqueTempFile_SymlinkAttacks tests the ExtractFromZipToUniqueTempFile
|
||||
// function with malicious ZIP archives containing symlink entries.
|
||||
//
|
||||
// EXPECTED BEHAVIOR: ExtractFromZipToUniqueTempFile should either:
|
||||
// 1. Reject symlinks explicitly, OR
|
||||
// 2. Extract them safely (library converts to empty files, filepath.Base sanitizes names)
|
||||
//
|
||||
// This function uses filepath.Base() on the archive entry name for temp file prefix and
|
||||
// os.CreateTemp() which creates files in the specified directory, so it should be protected.
|
||||
func TestExtractFromZipToUniqueTempFile_SymlinkAttacks(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
symlinkName string
|
||||
symlinkTarget string
|
||||
requestPath string
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "extract symlink entry to temp file",
|
||||
symlinkName: "evil_link",
|
||||
symlinkTarget: "/etc/passwd",
|
||||
requestPath: "evil_link",
|
||||
errContains: "", // no error expected - library extracts symlink metadata
|
||||
},
|
||||
{
|
||||
name: "extract nested symlink",
|
||||
symlinkName: "nested/dir/link",
|
||||
symlinkTarget: "/tmp/outside",
|
||||
requestPath: "nested/dir/link",
|
||||
errContains: "", // no error expected
|
||||
},
|
||||
{
|
||||
name: "extract path traversal symlink name",
|
||||
symlinkName: "../../escape",
|
||||
symlinkTarget: "/tmp/outside",
|
||||
requestPath: "../../escape",
|
||||
errContains: "", // no error expected - filepath.Base sanitizes name
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
maliciousZip := createMaliciousZipWithSymlink(t, tempDir, tt.symlinkName, tt.symlinkTarget, "")
|
||||
|
||||
// create temp directory for extraction
|
||||
extractTempDir := filepath.Join(tempDir, "temp_extract")
|
||||
require.NoError(t, os.MkdirAll(extractTempDir, 0755))
|
||||
|
||||
openers, err := ExtractFromZipToUniqueTempFile(context.Background(), maliciousZip, extractTempDir, tt.requestPath)
|
||||
|
||||
// check error expectations
|
||||
if tt.errContains != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.errContains)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
// verify symlink was extracted
|
||||
opener, found := openers[tt.requestPath]
|
||||
require.True(t, found, "symlink entry should be extracted")
|
||||
|
||||
// verify temp file is within temp directory
|
||||
tempFilePath := opener.path
|
||||
cleanTempDir := filepath.Clean(extractTempDir)
|
||||
cleanTempFile := filepath.Clean(tempFilePath)
|
||||
require.True(t, strings.HasPrefix(cleanTempFile, cleanTempDir),
|
||||
"temp file must be within temp directory: %s not in %s", cleanTempFile, cleanTempDir)
|
||||
|
||||
// verify symlink was NOT followed (content should be target path or empty)
|
||||
f, openErr := opener.Open()
|
||||
require.NoError(t, openErr)
|
||||
defer f.Close()
|
||||
|
||||
content, readErr := io.ReadAll(f)
|
||||
require.NoError(t, readErr)
|
||||
|
||||
// check if symlink was followed (content matches actual file)
|
||||
if len(content) > 0 && string(content) != tt.symlinkTarget {
|
||||
if _, statErr := os.Stat(tt.symlinkTarget); statErr == nil {
|
||||
targetContent, readErr := os.ReadFile(tt.symlinkTarget)
|
||||
if readErr == nil && string(targetContent) == string(content) {
|
||||
t.Errorf("critical issue!... symlink was FOLLOWED and external file content was copied!")
|
||||
t.Logf(" symlink: %s → %s", tt.requestPath, tt.symlinkTarget)
|
||||
t.Logf(" content length: %d bytes", len(content))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// forensicFindings contains the results of analyzing an extraction directory
|
||||
type forensicFindings struct {
|
||||
symlinksFound []forensicSymlink
|
||||
regularFiles []string
|
||||
directories []string
|
||||
symlinkVulnerabilities []string
|
||||
}
|
||||
|
||||
type forensicSymlink struct {
|
||||
path string
|
||||
target string
|
||||
escapesExtraction bool
|
||||
resolvedPath string
|
||||
}
|
||||
|
||||
// analyzeExtractionDirectory walks the extraction directory and detects symlinks that point
|
||||
// outside the extraction directory. It is silent unless vulnerabilities are found.
|
||||
func analyzeExtractionDirectory(t *testing.T, extractDir string) forensicFindings {
|
||||
t.Helper()
|
||||
|
||||
findings := forensicFindings{}
|
||||
|
||||
filepath.Walk(extractDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
// only log if there's an error walking the directory
|
||||
t.Logf("Error walking %s: %v", path, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
relPath := strings.TrimPrefix(path, extractDir+"/")
|
||||
if relPath == "" {
|
||||
relPath = "."
|
||||
}
|
||||
|
||||
// use Lstat to detect symlinks without following them
|
||||
linfo, lerr := os.Lstat(path)
|
||||
if lerr == nil && linfo.Mode()&os.ModeSymlink != 0 {
|
||||
target, _ := os.Readlink(path)
|
||||
|
||||
// resolve to see where it actually points
|
||||
var resolvedPath string
|
||||
var escapesExtraction bool
|
||||
|
||||
if filepath.IsAbs(target) {
|
||||
// absolute symlink
|
||||
resolvedPath = target
|
||||
cleanExtractDir := filepath.Clean(extractDir)
|
||||
escapesExtraction = !strings.HasPrefix(filepath.Clean(target), cleanExtractDir)
|
||||
|
||||
if escapesExtraction {
|
||||
t.Errorf("critical issue!... absolute symlink created: %s → %s", relPath, target)
|
||||
t.Logf(" this symlink points outside the extraction directory")
|
||||
findings.symlinkVulnerabilities = append(findings.symlinkVulnerabilities,
|
||||
fmt.Sprintf("absolute symlink: %s → %s", relPath, target))
|
||||
}
|
||||
} else {
|
||||
// relative symlink - resolve it
|
||||
resolvedPath = filepath.Join(filepath.Dir(path), target)
|
||||
cleanResolved := filepath.Clean(resolvedPath)
|
||||
cleanExtractDir := filepath.Clean(extractDir)
|
||||
|
||||
escapesExtraction = !strings.HasPrefix(cleanResolved, cleanExtractDir)
|
||||
|
||||
if escapesExtraction {
|
||||
t.Errorf("critical issue!... symlink escapes extraction dir: %s → %s", relPath, target)
|
||||
t.Logf(" symlink resolves to: %s (outside extraction directory)", cleanResolved)
|
||||
findings.symlinkVulnerabilities = append(findings.symlinkVulnerabilities,
|
||||
fmt.Sprintf("relative symlink escape: %s → %s (resolves to %s)", relPath, target, cleanResolved))
|
||||
}
|
||||
}
|
||||
|
||||
findings.symlinksFound = append(findings.symlinksFound, forensicSymlink{
|
||||
path: relPath,
|
||||
target: target,
|
||||
escapesExtraction: escapesExtraction,
|
||||
resolvedPath: resolvedPath,
|
||||
})
|
||||
} else {
|
||||
// regular file or directory - collect silently
|
||||
if info.IsDir() {
|
||||
findings.directories = append(findings.directories, relPath)
|
||||
} else {
|
||||
findings.regularFiles = append(findings.regularFiles, relPath)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return findings
|
||||
}
|
||||
|
||||
// checkFileOutsideExtraction checks if a file was written outside the extraction directory.
|
||||
// Returns true if the file exists (vulnerability), false otherwise. Silent on success.
|
||||
func checkFileOutsideExtraction(t *testing.T, filePath string) bool {
|
||||
t.Helper()
|
||||
|
||||
if stat, err := os.Stat(filePath); err == nil {
|
||||
content, _ := os.ReadFile(filePath)
|
||||
t.Errorf("critical issue!... file written OUTSIDE extraction directory!")
|
||||
t.Logf(" location: %s", filePath)
|
||||
t.Logf(" size: %d bytes", stat.Size())
|
||||
t.Logf(" content: %s", string(content))
|
||||
t.Logf(" ...this means an attacker can write files to arbitrary locations on the filesystem")
|
||||
return true
|
||||
}
|
||||
// no file found outside extraction directory...
|
||||
return false
|
||||
}
|
||||
|
||||
// checkSymlinkCreation verifies if a symlink was created at the expected path and reports
|
||||
// whether it points outside the extraction directory. Silent unless a symlink is found.
|
||||
func checkSymlinkCreation(t *testing.T, symlinkPath, extractDir, expectedTarget string) bool {
|
||||
t.Helper()
|
||||
|
||||
if linfo, err := os.Lstat(symlinkPath); err == nil {
|
||||
if linfo.Mode()&os.ModeSymlink != 0 {
|
||||
target, _ := os.Readlink(symlinkPath)
|
||||
|
||||
if expectedTarget != "" && target == expectedTarget {
|
||||
t.Errorf("critical issue!... symlink pointing outside extraction dir was created!")
|
||||
t.Logf(" Symlink: %s → %s", symlinkPath, target)
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if it escapes even if target doesn't match expected
|
||||
if filepath.IsAbs(target) {
|
||||
cleanExtractDir := filepath.Clean(extractDir)
|
||||
if !strings.HasPrefix(filepath.Clean(target), cleanExtractDir) {
|
||||
t.Errorf("critical issue!... absolute symlink escapes extraction dir!")
|
||||
t.Logf(" symlink: %s → %s", symlinkPath, target)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
// if it exists but is not a symlink, that's good (attack was thwarted)...
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// createMaliciousZipWithSymlink creates a ZIP archive containing a symlink entry pointing to an arbitrary target,
|
||||
// followed by a file entry that attempts to write through that symlink.
|
||||
// returns the path to the created ZIP archive.
|
||||
func createMaliciousZipWithSymlink(t *testing.T, tempDir, symlinkName, symlinkTarget, fileName string) string {
|
||||
t.Helper()
|
||||
|
||||
maliciousZip := filepath.Join(tempDir, "malicious.zip")
|
||||
zipFile, err := os.Create(maliciousZip)
|
||||
require.NoError(t, err)
|
||||
defer zipFile.Close()
|
||||
|
||||
zw := zip.NewWriter(zipFile)
|
||||
|
||||
// create parent directories if the symlink is nested
|
||||
if dir := filepath.Dir(symlinkName); dir != "." {
|
||||
dirHeader := &zip.FileHeader{
|
||||
Name: dir + "/",
|
||||
Method: zip.Store,
|
||||
}
|
||||
dirHeader.SetMode(os.ModeDir | 0755)
|
||||
_, err = zw.CreateHeader(dirHeader)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// create symlink entry pointing outside extraction directory
|
||||
// note: ZIP format stores symlinks as regular files with the target path as content
|
||||
symlinkHeader := &zip.FileHeader{
|
||||
Name: symlinkName,
|
||||
Method: zip.Store,
|
||||
}
|
||||
symlinkHeader.SetMode(os.ModeSymlink | 0755)
|
||||
|
||||
symlinkWriter, err := zw.CreateHeader(symlinkHeader)
|
||||
require.NoError(t, err)
|
||||
|
||||
// write the symlink target as the file content (this is how ZIP stores symlinks)
|
||||
_, err = symlinkWriter.Write([]byte(symlinkTarget))
|
||||
require.NoError(t, err)
|
||||
|
||||
// create file entry that will be written through the symlink
|
||||
if fileName != "" {
|
||||
payloadContent := []byte("MALICIOUS PAYLOAD - This should NOT be written outside extraction dir!")
|
||||
payloadHeader := &zip.FileHeader{
|
||||
Name: fileName,
|
||||
Method: zip.Deflate,
|
||||
}
|
||||
payloadHeader.SetMode(0644)
|
||||
|
||||
payloadWriter, err := zw.CreateHeader(payloadHeader)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = payloadWriter.Write(payloadContent)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
require.NoError(t, zw.Close())
|
||||
require.NoError(t, zipFile.Close())
|
||||
|
||||
return maliciousZip
|
||||
}
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
// directoryEndLen, readByf, directoryEnd, and findSignatureInBlock were copied from the golang stdlib, specifically:
|
||||
// - https://github.com/golang/go/blob/go1.16.4/src/archive/zip/struct.go
|
||||
// - https://github.com/golang/go/blob/go1.16.4/src/archive/zip/reader.go
|
||||
// findArchiveStartOffset is derived from the same stdlib utils, specifically the readDirectoryEnd function.
|
||||
|
||||
const (
|
||||
directoryEndLen = 22
|
||||
directory64LocLen = 20
|
||||
directory64EndLen = 56
|
||||
directory64LocSignature = 0x07064b50
|
||||
directory64EndSignature = 0x06064b50
|
||||
)
|
||||
|
||||
// ZipReadCloser is a drop-in replacement for zip.ReadCloser (from zip.OpenReader) that additionally considers zips
|
||||
// that have bytes prefixed to the front of the archive (common with self-extracting jars).
|
||||
type ZipReadCloser struct {
|
||||
*zip.Reader
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// OpenZip provides a ZipReadCloser for the given filepath.
|
||||
func OpenZip(filepath string) (*ZipReadCloser, error) {
|
||||
f, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// some archives may have bytes prepended to the front of the archive, such as with self executing JARs. We first
|
||||
// need to find the start of the archive and keep track of this offset.
|
||||
offset, err := findArchiveStartOffset(f, fi.Size())
|
||||
if err != nil {
|
||||
log.Debugf("cannot find beginning of zip archive=%q : %v", filepath, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||||
return nil, fmt.Errorf("unable to seek to beginning of archive: %w", err)
|
||||
}
|
||||
|
||||
if offset > math.MaxInt64 {
|
||||
return nil, fmt.Errorf("archive start offset too large: %v", offset)
|
||||
}
|
||||
offset64 := int64(offset)
|
||||
|
||||
size := fi.Size() - offset64
|
||||
|
||||
r, err := zip.NewReader(io.NewSectionReader(f, offset64, size), size)
|
||||
if err != nil {
|
||||
log.Debugf("unable to open ZipReadCloser @ %q: %v", filepath, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ZipReadCloser{
|
||||
Reader: r,
|
||||
Closer: f,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type readBuf []byte
|
||||
|
||||
func (b *readBuf) uint16() uint16 {
|
||||
v := binary.LittleEndian.Uint16(*b)
|
||||
*b = (*b)[2:]
|
||||
return v
|
||||
}
|
||||
|
||||
func (b *readBuf) uint32() uint32 {
|
||||
v := binary.LittleEndian.Uint32(*b)
|
||||
*b = (*b)[4:]
|
||||
return v
|
||||
}
|
||||
|
||||
func (b *readBuf) uint64() uint64 {
|
||||
v := binary.LittleEndian.Uint64(*b)
|
||||
*b = (*b)[8:]
|
||||
return v
|
||||
}
|
||||
|
||||
type directoryEnd struct {
|
||||
diskNbr uint32 // unused
|
||||
dirDiskNbr uint32 // unused
|
||||
dirRecordsThisDisk uint64 // unused
|
||||
directoryRecords uint64
|
||||
directorySize uint64
|
||||
directoryOffset uint64 // relative to file
|
||||
}
|
||||
|
||||
// note: this is derived from readDirectoryEnd within the archive/zip package
|
||||
func findArchiveStartOffset(r io.ReaderAt, size int64) (startOfArchive uint64, err error) {
|
||||
// look for directoryEndSignature in the last 1k, then in the last 65k
|
||||
var buf []byte
|
||||
var directoryEndOffset int64
|
||||
for i, bLen := range []int64{1024, 65 * 1024} {
|
||||
if bLen > size {
|
||||
bLen = size
|
||||
}
|
||||
buf = make([]byte, int(bLen))
|
||||
if _, err := r.ReadAt(buf, size-bLen); err != nil && !errors.Is(err, io.EOF) {
|
||||
return 0, err
|
||||
}
|
||||
if p := findSignatureInBlock(buf); p >= 0 {
|
||||
buf = buf[p:]
|
||||
directoryEndOffset = size - bLen + int64(p)
|
||||
break
|
||||
}
|
||||
if i == 1 || bLen == size {
|
||||
return 0, zip.ErrFormat
|
||||
}
|
||||
}
|
||||
|
||||
if buf == nil {
|
||||
// we were unable to find the directoryEndSignature block
|
||||
return 0, zip.ErrFormat
|
||||
}
|
||||
|
||||
// read header into struct
|
||||
b := readBuf(buf[4:]) // skip signature
|
||||
d := &directoryEnd{
|
||||
diskNbr: uint32(b.uint16()),
|
||||
dirDiskNbr: uint32(b.uint16()),
|
||||
dirRecordsThisDisk: uint64(b.uint16()),
|
||||
directoryRecords: uint64(b.uint16()),
|
||||
directorySize: uint64(b.uint32()),
|
||||
directoryOffset: uint64(b.uint32()),
|
||||
}
|
||||
// Calculate where the zip data actually begins
|
||||
|
||||
// These values mean that the file can be a zip64 file
|
||||
if d.directoryRecords == 0xffff || d.directorySize == 0xffff || d.directoryOffset == 0xffffffff {
|
||||
p, err := findDirectory64End(r, directoryEndOffset)
|
||||
if err == nil && p >= 0 {
|
||||
directoryEndOffset = p
|
||||
err = readDirectory64End(r, p, d)
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
startOfArchive = uint64(directoryEndOffset) - d.directorySize - d.directoryOffset
|
||||
|
||||
// Make sure directoryOffset points to somewhere in our file.
|
||||
if d.directoryOffset >= uint64(size) {
|
||||
return 0, zip.ErrFormat
|
||||
}
|
||||
return startOfArchive, nil
|
||||
}
|
||||
|
||||
// findDirectory64End tries to read the zip64 locator just before the
|
||||
// directory end and returns the offset of the zip64 directory end if
|
||||
// found.
|
||||
func findDirectory64End(r io.ReaderAt, directoryEndOffset int64) (int64, error) {
|
||||
locOffset := directoryEndOffset - directory64LocLen
|
||||
if locOffset < 0 {
|
||||
return -1, nil // no need to look for a header outside the file
|
||||
}
|
||||
buf := make([]byte, directory64LocLen)
|
||||
if _, err := r.ReadAt(buf, locOffset); err != nil {
|
||||
return -1, err
|
||||
}
|
||||
b := readBuf(buf)
|
||||
if sig := b.uint32(); sig != directory64LocSignature {
|
||||
return -1, nil
|
||||
}
|
||||
if b.uint32() != 0 { // number of the disk with the start of the zip64 end of central directory
|
||||
return -1, nil // the file is not a valid zip64-file
|
||||
}
|
||||
p := b.uint64() // relative offset of the zip64 end of central directory record
|
||||
if b.uint32() != 1 { // total number of disks
|
||||
return -1, nil // the file is not a valid zip64-file
|
||||
}
|
||||
return int64(p), nil
|
||||
}
|
||||
|
||||
// readDirectory64End reads the zip64 directory end and updates the
|
||||
// directory end with the zip64 directory end values.
|
||||
func readDirectory64End(r io.ReaderAt, offset int64, d *directoryEnd) (err error) {
|
||||
buf := make([]byte, directory64EndLen)
|
||||
if _, err := r.ReadAt(buf, offset); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b := readBuf(buf)
|
||||
if sig := b.uint32(); sig != directory64EndSignature {
|
||||
return errors.New("could not read directory64End")
|
||||
}
|
||||
|
||||
b = b[12:] // skip dir size, version and version needed (uint64 + 2x uint16)
|
||||
d.diskNbr = b.uint32() // number of this disk
|
||||
d.dirDiskNbr = b.uint32() // number of the disk with the start of the central directory
|
||||
d.dirRecordsThisDisk = b.uint64() // total number of entries in the central directory on this disk
|
||||
d.directoryRecords = b.uint64() // total number of entries in the central directory
|
||||
d.directorySize = b.uint64() // size of the central directory
|
||||
d.directoryOffset = b.uint64() // offset of start of central directory with respect to the starting disk number
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findSignatureInBlock(b []byte) int {
|
||||
for i := len(b) - directoryEndLen; i >= 0; i-- {
|
||||
// defined from directoryEndSignature
|
||||
if b[i] == 'P' && b[i+1] == 'K' && b[i+2] == 0x05 && b[i+3] == 0x06 {
|
||||
// n is length of comment
|
||||
n := int(b[i+directoryEndLen-2]) | int(b[i+directoryEndLen-1])<<8
|
||||
if n+directoryEndLen+i <= len(b) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFindArchiveStartOffset(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
archivePrep func(tb testing.TB) string
|
||||
expected uint64
|
||||
}{
|
||||
{
|
||||
name: "standard, non-nested zip",
|
||||
archivePrep: prepZipSourceFixture,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "zip with prepended bytes",
|
||||
archivePrep: prependZipSourceFixtureWithString(t, "junk at the beginning of the file..."),
|
||||
expected: 36,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
archivePath := test.archivePrep(t)
|
||||
f, err := os.Open(archivePath)
|
||||
if err != nil {
|
||||
t.Fatalf("could not open archive %q: %+v", archivePath, err)
|
||||
}
|
||||
fi, err := os.Stat(f.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unable to stat archive: %+v", err)
|
||||
}
|
||||
|
||||
actual, err := findArchiveStartOffset(f, fi.Size())
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find offset: %+v", err)
|
||||
}
|
||||
assert.Equal(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Please see [schema/json/README.md](../../schema/json/README.md) for more information on the JSON schema files in this directory.
|
||||
@@ -0,0 +1,159 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/invopop/jsonschema"
|
||||
)
|
||||
|
||||
func copyAliasFieldComments(commentMap map[string]string, repoRoot string) {
|
||||
// find all type aliases by parsing Go source files
|
||||
aliases := findTypeAliases(repoRoot)
|
||||
|
||||
// for each alias, copy field comments from the source type
|
||||
for aliasName, sourceName := range aliases {
|
||||
// find all field comments for the source type
|
||||
for key, comment := range commentMap {
|
||||
// check if this is a field comment for the source type
|
||||
// format: "github.com/anchore/syft/syft/pkg.SourceType.FieldName"
|
||||
if strings.Contains(key, "."+sourceName+".") {
|
||||
// create the corresponding key for the alias
|
||||
aliasKey := strings.Replace(key, "."+sourceName+".", "."+aliasName+".", 1)
|
||||
commentMap[aliasKey] = comment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func findTypeAliases(repoRoot string) map[string]string {
|
||||
aliases := make(map[string]string)
|
||||
fset := token.NewFileSet()
|
||||
|
||||
// walk through all Go files in the repo
|
||||
err := filepath.Walk(repoRoot, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".go") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// parse the file
|
||||
file, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// look for type alias declarations
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
typeSpec, ok := n.(*ast.TypeSpec)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// check if this is a type alias (e.g., type A B where B is an identifier)
|
||||
ident, ok := typeSpec.Type.(*ast.Ident)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// store the alias mapping: aliasName -> sourceName
|
||||
aliases[typeSpec.Name.Name] = ident.Name
|
||||
return true
|
||||
})
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: failed to find type aliases: %v\n", err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return aliases
|
||||
}
|
||||
|
||||
func hasDescriptionInAlternatives(schema *jsonschema.Schema) bool {
|
||||
// check oneOf alternatives
|
||||
for _, alt := range schema.OneOf {
|
||||
if alt.Description != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// check anyOf alternatives
|
||||
for _, alt := range schema.AnyOf {
|
||||
if alt.Description != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func warnMissingDescriptions(schema *jsonschema.Schema, metadataNames []string) { //nolint:gocognit
|
||||
var missingTypeDescriptions []string
|
||||
var missingFieldDescriptions []string
|
||||
|
||||
// check metadata types for missing descriptions
|
||||
for _, name := range metadataNames {
|
||||
def, ok := schema.Definitions[name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// check if type has a description
|
||||
if def.Description == "" {
|
||||
missingTypeDescriptions = append(missingTypeDescriptions, name)
|
||||
}
|
||||
|
||||
// check if fields have descriptions
|
||||
if def.Properties != nil {
|
||||
for _, fieldName := range def.Properties.Keys() {
|
||||
fieldSchemaRaw, _ := def.Properties.Get(fieldName)
|
||||
fieldSchema, ok := fieldSchemaRaw.(*jsonschema.Schema)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// skip if field has a description
|
||||
if fieldSchema.Description != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// skip if field is a reference (descriptions come from the referenced type)
|
||||
if fieldSchema.Ref != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// skip if field is an array/object with items that are references
|
||||
if fieldSchema.Items != nil && fieldSchema.Items.Ref != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// skip if field uses oneOf/anyOf with descriptions in the alternatives
|
||||
if hasDescriptionInAlternatives(fieldSchema) {
|
||||
continue
|
||||
}
|
||||
|
||||
missingFieldDescriptions = append(missingFieldDescriptions, fmt.Sprintf("%s.%s", name, fieldName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// report findings
|
||||
if len(missingTypeDescriptions) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "\nwarning: %d metadata types are missing descriptions:\n", len(missingTypeDescriptions))
|
||||
for _, name := range missingTypeDescriptions {
|
||||
fmt.Fprintf(os.Stderr, " - %s\n", name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(missingFieldDescriptions) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "\nwarning: %d fields are missing descriptions:\n", len(missingFieldDescriptions))
|
||||
for _, field := range missingFieldDescriptions {
|
||||
fmt.Fprintf(os.Stderr, " - %s\n", field)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/iancoleman/orderedmap"
|
||||
"github.com/invopop/jsonschema"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestCopyAliasFieldComments verifies that field comments from source types are correctly copied to alias types.
|
||||
// This is important for type aliases like `type RpmArchive RpmDBEntry` where the alias should inherit all field descriptions.
|
||||
func TestCopyAliasFieldComments(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
commentMap map[string]string
|
||||
aliases map[string]string
|
||||
wantComments map[string]string
|
||||
}{
|
||||
{
|
||||
name: "copies field comments from source type to alias",
|
||||
commentMap: map[string]string{
|
||||
"github.com/anchore/syft/syft/pkg.RpmDBEntry": "RpmDBEntry represents all captured data from a RPM DB package entry.",
|
||||
"github.com/anchore/syft/syft/pkg.RpmDBEntry.Name": "Name is the RPM package name.",
|
||||
"github.com/anchore/syft/syft/pkg.RpmDBEntry.Epoch": "Epoch is the version epoch.",
|
||||
},
|
||||
aliases: map[string]string{
|
||||
"RpmArchive": "RpmDBEntry",
|
||||
},
|
||||
wantComments: map[string]string{
|
||||
"github.com/anchore/syft/syft/pkg.RpmDBEntry": "RpmDBEntry represents all captured data from a RPM DB package entry.",
|
||||
"github.com/anchore/syft/syft/pkg.RpmDBEntry.Name": "Name is the RPM package name.",
|
||||
"github.com/anchore/syft/syft/pkg.RpmDBEntry.Epoch": "Epoch is the version epoch.",
|
||||
"github.com/anchore/syft/syft/pkg.RpmArchive.Name": "Name is the RPM package name.",
|
||||
"github.com/anchore/syft/syft/pkg.RpmArchive.Epoch": "Epoch is the version epoch.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "handles multiple aliases",
|
||||
commentMap: map[string]string{
|
||||
"github.com/anchore/syft/syft/pkg.DpkgDBEntry": "DpkgDBEntry represents data from dpkg.",
|
||||
"github.com/anchore/syft/syft/pkg.DpkgDBEntry.Package": "Package is the package name.",
|
||||
"github.com/anchore/syft/syft/pkg.DpkgDBEntry.Architecture": "Architecture is the target arch.",
|
||||
},
|
||||
aliases: map[string]string{
|
||||
"DpkgArchiveEntry": "DpkgDBEntry",
|
||||
"DpkgSnapshot": "DpkgDBEntry",
|
||||
},
|
||||
wantComments: map[string]string{
|
||||
"github.com/anchore/syft/syft/pkg.DpkgDBEntry": "DpkgDBEntry represents data from dpkg.",
|
||||
"github.com/anchore/syft/syft/pkg.DpkgDBEntry.Package": "Package is the package name.",
|
||||
"github.com/anchore/syft/syft/pkg.DpkgDBEntry.Architecture": "Architecture is the target arch.",
|
||||
"github.com/anchore/syft/syft/pkg.DpkgArchiveEntry.Package": "Package is the package name.",
|
||||
"github.com/anchore/syft/syft/pkg.DpkgArchiveEntry.Architecture": "Architecture is the target arch.",
|
||||
"github.com/anchore/syft/syft/pkg.DpkgSnapshot.Package": "Package is the package name.",
|
||||
"github.com/anchore/syft/syft/pkg.DpkgSnapshot.Architecture": "Architecture is the target arch.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "does not copy non-field comments",
|
||||
commentMap: map[string]string{
|
||||
"github.com/anchore/syft/syft/pkg.SomeType": "SomeType struct comment.",
|
||||
"github.com/anchore/syft/syft/pkg.SomeType.Field": "Field comment.",
|
||||
},
|
||||
aliases: map[string]string{
|
||||
"AliasType": "SomeType",
|
||||
},
|
||||
wantComments: map[string]string{
|
||||
"github.com/anchore/syft/syft/pkg.SomeType": "SomeType struct comment.",
|
||||
"github.com/anchore/syft/syft/pkg.SomeType.Field": "Field comment.",
|
||||
"github.com/anchore/syft/syft/pkg.AliasType.Field": "Field comment.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// create temp dir for testing
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// create a test go file with type aliases
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
content := "package test\n\n"
|
||||
for alias, source := range tt.aliases {
|
||||
content += "type " + alias + " " + source + "\n"
|
||||
}
|
||||
err := os.WriteFile(testFile, []byte(content), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// make a copy of the comment map since the function modifies it
|
||||
commentMap := make(map[string]string)
|
||||
for k, v := range tt.commentMap {
|
||||
commentMap[k] = v
|
||||
}
|
||||
|
||||
// run the function
|
||||
copyAliasFieldComments(commentMap, tmpDir)
|
||||
|
||||
// verify results
|
||||
assert.Equal(t, tt.wantComments, commentMap)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindTypeAliases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fileContent string
|
||||
wantAliases map[string]string
|
||||
}{
|
||||
{
|
||||
name: "finds simple type alias",
|
||||
fileContent: `package test
|
||||
|
||||
type RpmArchive RpmDBEntry
|
||||
type DpkgArchiveEntry DpkgDBEntry
|
||||
`,
|
||||
wantAliases: map[string]string{
|
||||
"RpmArchive": "RpmDBEntry",
|
||||
"DpkgArchiveEntry": "DpkgDBEntry",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ignores struct definitions",
|
||||
fileContent: `package test
|
||||
|
||||
type MyStruct struct {
|
||||
Field string
|
||||
}
|
||||
|
||||
type AliasType BaseType
|
||||
`,
|
||||
wantAliases: map[string]string{
|
||||
"AliasType": "BaseType",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ignores interface definitions",
|
||||
fileContent: `package test
|
||||
|
||||
type MyInterface interface {
|
||||
Method()
|
||||
}
|
||||
|
||||
type AliasType BaseType
|
||||
`,
|
||||
wantAliases: map[string]string{
|
||||
"AliasType": "BaseType",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "handles multiple files",
|
||||
fileContent: `package test
|
||||
|
||||
type Alias1 Base1
|
||||
type Alias2 Base2
|
||||
`,
|
||||
wantAliases: map[string]string{
|
||||
"Alias1": "Base1",
|
||||
"Alias2": "Base2",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// create temp dir
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// write test file
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
err := os.WriteFile(testFile, []byte(tt.fileContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// run function
|
||||
aliases := findTypeAliases(tmpDir)
|
||||
|
||||
// verify
|
||||
assert.Equal(t, tt.wantAliases, aliases)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasDescriptionInAlternatives(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
schema *jsonschema.Schema
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "returns true when oneOf has description",
|
||||
schema: &jsonschema.Schema{
|
||||
OneOf: []*jsonschema.Schema{
|
||||
{Description: "First alternative"},
|
||||
{Type: "null"},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "returns true when anyOf has description",
|
||||
schema: &jsonschema.Schema{
|
||||
AnyOf: []*jsonschema.Schema{
|
||||
{Description: "First alternative"},
|
||||
{Type: "null"},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "returns false when no alternatives have descriptions",
|
||||
schema: &jsonschema.Schema{
|
||||
OneOf: []*jsonschema.Schema{
|
||||
{Type: "integer"},
|
||||
{Type: "null"},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "returns false when no oneOf or anyOf",
|
||||
schema: &jsonschema.Schema{
|
||||
Type: "string",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "returns true when any alternative in oneOf has description",
|
||||
schema: &jsonschema.Schema{
|
||||
OneOf: []*jsonschema.Schema{
|
||||
{Type: "integer"},
|
||||
{Type: "string", Description: "Second alternative"},
|
||||
{Type: "null"},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := hasDescriptionInAlternatives(tt.schema)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWarnMissingDescriptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
schema *jsonschema.Schema
|
||||
metadataNames []string
|
||||
wantTypeWarnings int
|
||||
wantFieldWarnings int
|
||||
}{
|
||||
{
|
||||
name: "no warnings when all types have descriptions",
|
||||
schema: &jsonschema.Schema{
|
||||
Definitions: map[string]*jsonschema.Schema{
|
||||
"TypeA": {
|
||||
Description: "Type A description",
|
||||
Properties: newOrderedMap(map[string]*jsonschema.Schema{
|
||||
"field1": {Type: "string", Description: "Field 1"},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
metadataNames: []string{"TypeA"},
|
||||
wantTypeWarnings: 0,
|
||||
wantFieldWarnings: 0,
|
||||
},
|
||||
{
|
||||
name: "warns about missing type description",
|
||||
schema: &jsonschema.Schema{
|
||||
Definitions: map[string]*jsonschema.Schema{
|
||||
"TypeA": {
|
||||
Properties: newOrderedMap(map[string]*jsonschema.Schema{
|
||||
"field1": {Type: "string", Description: "Field 1"},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
metadataNames: []string{"TypeA"},
|
||||
wantTypeWarnings: 1,
|
||||
wantFieldWarnings: 0,
|
||||
},
|
||||
{
|
||||
name: "warns about missing field description",
|
||||
schema: &jsonschema.Schema{
|
||||
Definitions: map[string]*jsonschema.Schema{
|
||||
"TypeA": {
|
||||
Description: "Type A description",
|
||||
Properties: newOrderedMap(map[string]*jsonschema.Schema{
|
||||
"field1": {Type: "string"},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
metadataNames: []string{"TypeA"},
|
||||
wantTypeWarnings: 0,
|
||||
wantFieldWarnings: 1,
|
||||
},
|
||||
{
|
||||
name: "skips fields with references",
|
||||
schema: &jsonschema.Schema{
|
||||
Definitions: map[string]*jsonschema.Schema{
|
||||
"TypeA": {
|
||||
Description: "Type A description",
|
||||
Properties: newOrderedMap(map[string]*jsonschema.Schema{
|
||||
"field1": {Ref: "#/$defs/OtherType"},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
metadataNames: []string{"TypeA"},
|
||||
wantTypeWarnings: 0,
|
||||
wantFieldWarnings: 0,
|
||||
},
|
||||
{
|
||||
name: "skips fields with items that are references",
|
||||
schema: &jsonschema.Schema{
|
||||
Definitions: map[string]*jsonschema.Schema{
|
||||
"TypeA": {
|
||||
Description: "Type A description",
|
||||
Properties: newOrderedMap(map[string]*jsonschema.Schema{
|
||||
"field1": {
|
||||
Type: "array",
|
||||
Items: &jsonschema.Schema{Ref: "#/$defs/OtherType"},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
metadataNames: []string{"TypeA"},
|
||||
wantTypeWarnings: 0,
|
||||
wantFieldWarnings: 0,
|
||||
},
|
||||
{
|
||||
name: "skips fields with oneOf containing descriptions",
|
||||
schema: &jsonschema.Schema{
|
||||
Definitions: map[string]*jsonschema.Schema{
|
||||
"TypeA": {
|
||||
Description: "Type A description",
|
||||
Properties: newOrderedMap(map[string]*jsonschema.Schema{
|
||||
"field1": {
|
||||
OneOf: []*jsonschema.Schema{
|
||||
{Type: "integer", Description: "Integer value"},
|
||||
{Type: "null"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
metadataNames: []string{"TypeA"},
|
||||
wantTypeWarnings: 0,
|
||||
wantFieldWarnings: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// capture stderr output would require more complex testing
|
||||
// for now, just verify the function runs without panicking
|
||||
require.NotPanics(t, func() {
|
||||
warnMissingDescriptions(tt.schema, tt.metadataNames)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// helper to create an ordered map from a regular map
|
||||
func newOrderedMap(m map[string]*jsonschema.Schema) *orderedmap.OrderedMap {
|
||||
om := orderedmap.New()
|
||||
for k, v := range m {
|
||||
om.Set(k, v)
|
||||
}
|
||||
return om
|
||||
}
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
"github.com/invopop/jsonschema"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/packagemetadata"
|
||||
syftJsonModel "github.com/anchore/syft/syft/format/syftjson/model"
|
||||
"github.com/anchore/syft/syft/internal/packagemetadata"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -26,6 +26,17 @@ are not captured (empty interfaces). This means that pkg.Package.Metadata is not
|
||||
can be extended to include specific package metadata struct shapes in the future.
|
||||
*/
|
||||
|
||||
var repoRoot string
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
repoRoot, err = packagemetadata.RepoRoot()
|
||||
if err != nil {
|
||||
fmt.Println("unable to determine repo root")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
write(encode(build()))
|
||||
}
|
||||
@@ -60,7 +71,7 @@ func assembleTypeContainer(items []any) (any, map[string]string) {
|
||||
}
|
||||
|
||||
if len(typesMissingNames) > 0 {
|
||||
fmt.Println("the following types are missing JSON names (manually curated in ./syft/internal/packagemetadata/names.go):")
|
||||
fmt.Println("the following types are missing JSON names (manually curated in ./internal/packagemetadata/names.go):")
|
||||
for _, t := range typesMissingNames {
|
||||
fmt.Println(" - ", t.Name())
|
||||
}
|
||||
@@ -78,6 +89,38 @@ func build() *jsonschema.Schema {
|
||||
Namer: func(r reflect.Type) string {
|
||||
return strings.TrimPrefix(r.Name(), "JSON")
|
||||
},
|
||||
CommentMap: make(map[string]string),
|
||||
}
|
||||
|
||||
// extract comments from Go source files to enrich schema descriptions
|
||||
//
|
||||
// note: AddGoComments parses from the module root and creates keys like "syft/pkg.TypeName",
|
||||
// but the reflector expects fully qualified paths like "github.com/anchore/syft/syft/pkg.TypeName".
|
||||
// We fix up the keys after extraction to match the expected format.
|
||||
if err := reflector.AddGoComments("github.com/anchore/syft", repoRoot); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warning: failed to extract Go comments: %v\n", err)
|
||||
} else {
|
||||
// fix up comment map keys to use fully qualified import paths
|
||||
// note: AddGoComments includes the absolute repo path WITHOUT the leading slash
|
||||
repoRootNoSlash := strings.TrimPrefix(repoRoot, "/")
|
||||
fixedMap := make(map[string]string)
|
||||
for k, v := range reflector.CommentMap {
|
||||
newKey := k
|
||||
if !strings.HasPrefix(k, "github.com/") {
|
||||
// key doesn't have module prefix, add it
|
||||
newKey = "github.com/anchore/syft/" + k
|
||||
} else if strings.Contains(k, repoRootNoSlash) {
|
||||
// key has the absolute repo path embedded, strip it
|
||||
// format: github.com/anchore/syft/Users/wagoodman/code/syft-manual/syft/pkg.Type
|
||||
// should be: github.com/anchore/syft/syft/pkg.Type
|
||||
newKey = strings.Replace(k, repoRootNoSlash+"/", "", 1)
|
||||
}
|
||||
fixedMap[newKey] = v
|
||||
}
|
||||
reflector.CommentMap = fixedMap
|
||||
|
||||
// copy field comments for type aliases (e.g., type RpmArchive RpmDBEntry)
|
||||
copyAliasFieldComments(reflector.CommentMap, repoRoot)
|
||||
}
|
||||
|
||||
pkgMetadataContainer, pkgMetadataMapping := assembleTypeContainer(packagemetadata.AllTypes())
|
||||
@@ -130,6 +173,9 @@ func build() *jsonschema.Schema {
|
||||
"anyOf": metadataTypes,
|
||||
})
|
||||
|
||||
// warn about missing descriptions
|
||||
warnMissingDescriptions(documentSchema, metadataNames)
|
||||
|
||||
return documentSchema
|
||||
}
|
||||
|
||||
@@ -148,11 +194,6 @@ func encode(schema *jsonschema.Schema) []byte {
|
||||
}
|
||||
|
||||
func write(schema []byte) {
|
||||
repoRoot, err := packagemetadata.RepoRoot()
|
||||
if err != nil {
|
||||
fmt.Println("unable to determine repo root")
|
||||
os.Exit(1)
|
||||
}
|
||||
schemaPath := filepath.Join(repoRoot, "schema", "json", fmt.Sprintf("schema-%s.json", internal.JSONSchemaVersion))
|
||||
latestSchemaPath := filepath.Join(repoRoot, "schema", "json", "schema-latest.json")
|
||||
|
||||
@@ -81,6 +81,10 @@ func Test_EnvironmentTask(t *testing.T) {
|
||||
// get the source
|
||||
theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive"))
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, theSource.Close())
|
||||
})
|
||||
|
||||
resolver, err := theSource.FileResolver(source.SquashedScope)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
+2
-2
@@ -24,7 +24,7 @@ var knownNonMetadataTypeNames = strset.New(
|
||||
|
||||
// these are names that would be removed due to common convention (e.g. used within another metadata type) but are
|
||||
// known to be metadata types themselves. Adding to this list will prevent the removal of the type from the schema.
|
||||
var knownMetadaTypeNames = strset.New(
|
||||
var knownMetadataTypeNames = strset.New(
|
||||
"DotnetPortableExecutableEntry",
|
||||
)
|
||||
|
||||
@@ -72,7 +72,7 @@ func findMetadataDefinitionNames(paths ...string) ([]string, error) {
|
||||
}
|
||||
|
||||
// any definition that is used within another struct should not be considered a top-level metadata definition
|
||||
removeNames := strset.Difference(usedNames, knownMetadaTypeNames)
|
||||
removeNames := strset.Difference(usedNames, knownMetadataTypeNames)
|
||||
names.Remove(removeNames.List()...)
|
||||
|
||||
// remove known exceptions, that is, types exported in the pkg Package that are not used
|
||||
+2
-2
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/dave/jennifer/jen"
|
||||
|
||||
"github.com/anchore/syft/syft/internal/packagemetadata"
|
||||
"github.com/anchore/syft/internal/packagemetadata"
|
||||
)
|
||||
|
||||
// This program is invoked from syft/internal and generates packagemetadata/generated.go
|
||||
@@ -31,7 +31,7 @@ func main() {
|
||||
fmt.Printf("updating package metadata type list with %+v types\n", len(typeNames))
|
||||
|
||||
f := jen.NewFile("packagemetadata")
|
||||
f.HeaderComment("DO NOT EDIT: generated by syft/internal/packagemetadata/generate/main.go")
|
||||
f.HeaderComment("DO NOT EDIT: generated by internal/packagemetadata/generate/main.go")
|
||||
f.ImportName(pkgImport, "pkg")
|
||||
f.Comment("AllTypes returns a list of all pkg metadata types that syft supports (that are represented in the pkg.Package.Metadata field).")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// DO NOT EDIT: generated by syft/internal/packagemetadata/generate/main.go
|
||||
// DO NOT EDIT: generated by internal/packagemetadata/generate/main.go
|
||||
|
||||
package packagemetadata
|
||||
|
||||
@@ -27,6 +27,7 @@ func AllTypes() []any {
|
||||
pkg.ELFBinaryPackageNoteJSONPayload{},
|
||||
pkg.ElixirMixLockEntry{},
|
||||
pkg.ErlangRebarLockEntry{},
|
||||
pkg.GGUFFileHeader{},
|
||||
pkg.GitHubActionsUseStatement{},
|
||||
pkg.GolangBinaryBuildinfoEntry{},
|
||||
pkg.GolangModuleEntry{},
|
||||
@@ -49,8 +50,10 @@ func AllTypes() []any {
|
||||
pkg.PhpComposerLockEntry{},
|
||||
pkg.PhpPearEntry{},
|
||||
pkg.PhpPeclEntry{},
|
||||
pkg.PnpmLockEntry{},
|
||||
pkg.PortageEntry{},
|
||||
pkg.PythonPackage{},
|
||||
pkg.PythonPdmLockEntry{},
|
||||
pkg.PythonPipfileLockEntry{},
|
||||
pkg.PythonPoetryLockEntry{},
|
||||
pkg.PythonRequirementsEntry{},
|
||||
@@ -61,6 +64,7 @@ func AllTypes() []any {
|
||||
pkg.RubyGemspec{},
|
||||
pkg.RustBinaryAuditEntry{},
|
||||
pkg.RustCargoLockEntry{},
|
||||
pkg.SnapEntry{},
|
||||
pkg.SwiftPackageManagerResolvedEntry{},
|
||||
pkg.SwiplPackEntry{},
|
||||
pkg.TerraformLockProviderEntry{},
|
||||
@@ -95,13 +95,16 @@ var jsonTypes = makeJSONTypes(
|
||||
jsonNames(pkg.NpmPackage{}, "javascript-npm-package", "NpmPackageJsonMetadata"),
|
||||
jsonNames(pkg.NpmPackageLockEntry{}, "javascript-npm-package-lock-entry", "NpmPackageLockJsonMetadata"),
|
||||
jsonNames(pkg.YarnLockEntry{}, "javascript-yarn-lock-entry", "YarnLockJsonMetadata"),
|
||||
jsonNames(pkg.PnpmLockEntry{}, "javascript-pnpm-lock-entry"),
|
||||
jsonNames(pkg.PEBinary{}, "pe-binary"),
|
||||
jsonNames(pkg.PhpComposerLockEntry{}, "php-composer-lock-entry", "PhpComposerJsonMetadata"),
|
||||
jsonNamesWithoutLookup(pkg.PhpComposerInstalledEntry{}, "php-composer-installed-entry", "PhpComposerJsonMetadata"), // the legacy value is split into two types, where the other is preferred
|
||||
//nolint:staticcheck
|
||||
jsonNames(pkg.PhpPeclEntry{}, "php-pecl-entry", "PhpPeclMetadata"),
|
||||
jsonNames(pkg.PhpPearEntry{}, "php-pear-entry"),
|
||||
jsonNames(pkg.PortageEntry{}, "portage-db-entry", "PortageMetadata"),
|
||||
jsonNames(pkg.PythonPackage{}, "python-package", "PythonPackageMetadata"),
|
||||
jsonNames(pkg.PythonPdmLockEntry{}, "python-pdm-lock-entry"),
|
||||
jsonNames(pkg.PythonPipfileLockEntry{}, "python-pipfile-lock-entry", "PythonPipfileLockMetadata"),
|
||||
jsonNames(pkg.PythonPoetryLockEntry{}, "python-poetry-lock-entry", "PythonPoetryLockMetadata"),
|
||||
jsonNames(pkg.PythonRequirementsEntry{}, "python-pip-requirements-entry", "PythonRequirementsMetadata"),
|
||||
@@ -115,12 +118,14 @@ var jsonTypes = makeJSONTypes(
|
||||
jsonNames(pkg.OpamPackage{}, "opam-package"),
|
||||
jsonNames(pkg.RustCargoLockEntry{}, "rust-cargo-lock-entry", "RustCargoPackageMetadata"),
|
||||
jsonNamesWithoutLookup(pkg.RustBinaryAuditEntry{}, "rust-cargo-audit-entry", "RustCargoPackageMetadata"), // the legacy value is split into two types, where the other is preferred
|
||||
jsonNames(pkg.SnapEntry{}, "snap-entry"),
|
||||
jsonNames(pkg.WordpressPluginEntry{}, "wordpress-plugin-entry", "WordpressMetadata"),
|
||||
jsonNames(pkg.HomebrewFormula{}, "homebrew-formula"),
|
||||
jsonNames(pkg.LuaRocksPackage{}, "luarocks-package"),
|
||||
jsonNames(pkg.TerraformLockProviderEntry{}, "terraform-lock-provider-entry"),
|
||||
jsonNames(pkg.DotnetPackagesLockEntry{}, "dotnet-packages-lock-entry"),
|
||||
jsonNames(pkg.CondaMetaPackage{}, "conda-metadata-entry", "CondaPackageMetadata"),
|
||||
jsonNames(pkg.GGUFFileHeader{}, "gguf-file-header"),
|
||||
)
|
||||
|
||||
func expandLegacyNameVariants(names ...string) []string {
|
||||
@@ -79,6 +79,10 @@ func (i *Index) Remove(id artifact.ID) {
|
||||
|
||||
func (i *Index) Replace(ogID artifact.ID, replacement artifact.Identifiable) {
|
||||
for _, mapped := range fromMappedByID(i.fromID, ogID) {
|
||||
// the stale relationship(i.e. if there's an elder ID in either side) should be discarded
|
||||
if len(fromMappedByID(i.toID, mapped.relationship.To.ID())) == 0 {
|
||||
continue
|
||||
}
|
||||
i.Add(artifact.Relationship{
|
||||
From: replacement,
|
||||
To: mapped.relationship.To,
|
||||
@@ -87,6 +91,10 @@ func (i *Index) Replace(ogID artifact.ID, replacement artifact.Identifiable) {
|
||||
}
|
||||
|
||||
for _, mapped := range fromMappedByID(i.toID, ogID) {
|
||||
// same as the above
|
||||
if len(fromMappedByID(i.fromID, mapped.relationship.To.ID())) == 0 {
|
||||
continue
|
||||
}
|
||||
i.Add(artifact.Relationship{
|
||||
From: mapped.relationship.From,
|
||||
To: replacement,
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/dave/jennifer/jen"
|
||||
|
||||
"github.com/anchore/syft/syft/internal/sourcemetadata"
|
||||
"github.com/anchore/syft/internal/sourcemetadata"
|
||||
)
|
||||
|
||||
// This program is invoked from syft/internal and generates sourcemetadata/generated.go
|
||||
@@ -25,7 +25,7 @@ func main() {
|
||||
fmt.Printf("updating source metadata type list with %+v types\n", len(typeNames))
|
||||
|
||||
f := jen.NewFile("sourcemetadata")
|
||||
f.HeaderComment("DO NOT EDIT: generated by syft/internal/sourcemetadata/generate/main.go")
|
||||
f.HeaderComment("DO NOT EDIT: generated by internal/sourcemetadata/generate/main.go")
|
||||
f.ImportName(srcImport, "source")
|
||||
f.Comment("AllTypes returns a list of all source metadata types that syft supports (that are represented in the source.Description.Metadata field).")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// DO NOT EDIT: generated by syft/internal/sourcemetadata/generate/main.go
|
||||
// DO NOT EDIT: generated by internal/sourcemetadata/generate/main.go
|
||||
|
||||
package sourcemetadata
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
// This program generates license_list.go.
|
||||
@@ -20,8 +19,7 @@ const (
|
||||
)
|
||||
|
||||
var tmp = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT.
|
||||
// This file was generated by robots at {{ .Timestamp }}
|
||||
// using data from {{ .URL }}
|
||||
// This file was generated using data from {{ .URL }}
|
||||
package spdxlicense
|
||||
|
||||
const Version = {{ printf "%q" .Version }}
|
||||
@@ -78,13 +76,11 @@ func run() error {
|
||||
urlToLicense := buildURLToLicenseMap(result)
|
||||
|
||||
err = tmp.Execute(f, struct {
|
||||
Timestamp time.Time
|
||||
URL string
|
||||
Version string
|
||||
LicenseIDs map[string]string
|
||||
URLToLicense map[string]string
|
||||
}{
|
||||
Timestamp: time.Now(),
|
||||
URL: url,
|
||||
Version: result.Version,
|
||||
LicenseIDs: licenseIDs,
|
||||
|
||||
@@ -3,6 +3,7 @@ package task
|
||||
import (
|
||||
"github.com/anchore/syft/syft/cataloging/pkgcataloging"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/ai"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/alpine"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/arch"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary"
|
||||
@@ -32,6 +33,7 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/ruby"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/rust"
|
||||
sbomCataloger "github.com/anchore/syft/syft/pkg/cataloger/sbom"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/snap"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/swift"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/swipl"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/terraform"
|
||||
@@ -51,6 +53,9 @@ const (
|
||||
JavaScript = "javascript"
|
||||
Node = "node"
|
||||
NPM = "npm"
|
||||
|
||||
// Python ecosystem labels
|
||||
Python = "python"
|
||||
)
|
||||
|
||||
//nolint:funlen
|
||||
@@ -108,7 +113,7 @@ func DefaultPackageTaskFactories() Factories {
|
||||
func(cfg CatalogingFactoryConfig) pkg.Cataloger {
|
||||
return python.NewPackageCataloger(cfg.PackagesConfig.Python)
|
||||
},
|
||||
pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "python",
|
||||
pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, Python,
|
||||
),
|
||||
newSimplePackageTaskFactory(ruby.NewGemFileLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "ruby", "gem"),
|
||||
newSimplePackageTaskFactory(ruby.NewGemSpecCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "ruby", "gem", "gemspec"),
|
||||
@@ -126,7 +131,7 @@ func DefaultPackageTaskFactories() Factories {
|
||||
pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dotnet", "c#",
|
||||
),
|
||||
newSimplePackageTaskFactory(dotnet.NewDotnetPackagesLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.ImageTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dotnet", "c#"),
|
||||
newSimplePackageTaskFactory(python.NewInstalledPackageCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.LanguageTag, "python"),
|
||||
newSimplePackageTaskFactory(python.NewInstalledPackageCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.LanguageTag, Python),
|
||||
newPackageTaskFactory(
|
||||
func(cfg CatalogingFactoryConfig) pkg.Cataloger {
|
||||
return golang.NewGoModuleBinaryCataloger(cfg.PackagesConfig.Golang)
|
||||
@@ -173,12 +178,14 @@ func DefaultPackageTaskFactories() Factories {
|
||||
newSimplePackageTaskFactory(terraform.NewLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "terraform"),
|
||||
newSimplePackageTaskFactory(homebrew.NewCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "homebrew"),
|
||||
newSimplePackageTaskFactory(conda.NewCondaMetaCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.PackageTag, "conda"),
|
||||
newSimplePackageTaskFactory(snap.NewCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "snap"),
|
||||
newSimplePackageTaskFactory(ai.NewGGUFCataloger, pkgcataloging.DirectoryTag, pkgcataloging.ImageTag, "ai", "model", "gguf", "ml"),
|
||||
|
||||
// deprecated catalogers ////////////////////////////////////////
|
||||
// these are catalogers that should not be selectable other than specific inclusion via name or "deprecated" tag (to remain backwards compatible)
|
||||
newSimplePackageTaskFactory(dotnet.NewDotnetDepsCataloger, pkgcataloging.DeprecatedTag), // TODO: remove in syft v2.0
|
||||
newSimplePackageTaskFactory(dotnet.NewDotnetPortableExecutableCataloger, pkgcataloging.DeprecatedTag), // TODO: remove in syft v2.0
|
||||
newSimplePackageTaskFactory(php.NewPeclCataloger, pkgcataloging.DeprecatedTag), // TODO: remove in syft v2.0
|
||||
newSimplePackageTaskFactory(nix.NewStoreCataloger, pkgcataloging.DeprecatedTag), // TODO: remove in syft v2.0
|
||||
newSimplePackageTaskFactory(dotnet.NewDotnetDepsCataloger, pkgcataloging.DeprecatedTag), //nolint:staticcheck // TODO: remove in syft v2.0
|
||||
newSimplePackageTaskFactory(dotnet.NewDotnetPortableExecutableCataloger, pkgcataloging.DeprecatedTag), //nolint:staticcheck // TODO: remove in syft v2.0
|
||||
newSimplePackageTaskFactory(php.NewPeclCataloger, pkgcataloging.DeprecatedTag), //nolint:staticcheck // TODO: remove in syft v2.0
|
||||
newSimplePackageTaskFactory(nix.NewStoreCataloger, pkgcataloging.DeprecatedTag), //nolint:staticcheck // TODO: remove in syft v2.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/archiver/v3"
|
||||
intFile "github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/sbomsync"
|
||||
"github.com/anchore/syft/syft/cataloging"
|
||||
@@ -57,9 +57,10 @@ func (c unknownsLabelerTask) finalize(resolver file.Resolver, s *sbom.SBOM) {
|
||||
}
|
||||
|
||||
if c.IncludeUnexpandedArchives {
|
||||
ctx := context.Background()
|
||||
for coords := range s.Artifacts.FileMetadata {
|
||||
unarchiver, notArchiveErr := archiver.ByExtension(coords.RealPath)
|
||||
if unarchiver != nil && notArchiveErr == nil && !hasPackageReference(coords) {
|
||||
format, _, notArchiveErr := intFile.IdentifyArchive(ctx, coords.RealPath, nil)
|
||||
if format != nil && notArchiveErr == nil && !hasPackageReference(coords) {
|
||||
s.Artifacts.Unknowns[coords] = append(s.Artifacts.Unknowns[coords], "archive not cataloged")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,6 @@ prior spec version (e.g. if updating to 1.7, compare the files in this directory
|
||||
equivalents).
|
||||
|
||||
One can also update the schemas and observe the errors in order to make the necessary updates.
|
||||
At the time of writing, the cyclonedx.xsd needed modifications to link to the local spdx.xsd,
|
||||
and also to changes the minOccurs for a license tag to 0. (The json schema does not require
|
||||
At the time of writing, the cyclonedx.xsd needed modifications to link to the local spdx.xsd,
|
||||
and also to change the minOccurs for a license tag to 0. (The json schema does not require
|
||||
modification for the generated file to lint properly, but can simply be copy/pasted).
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1630
-674
File diff suppressed because it is too large
Load Diff
@@ -24,9 +24,119 @@ import (
|
||||
"github.com/anchore/syft/syft/internal/unionreader"
|
||||
)
|
||||
|
||||
// SymbolCaptureScope defines the scope of symbols to capture from executables. For the meantime only golang binaries are supported,
|
||||
// however, in the future this can be expanded to include rust audit binaries, libraries only, applications only, or all binaries.
|
||||
type SymbolCaptureScope string
|
||||
|
||||
const SymbolScopeGolang SymbolCaptureScope = "golang" // only binaries built with the golang toolchain
|
||||
|
||||
type Config struct {
|
||||
// MIMETypes are the MIME types that will be considered for executable cataloging.
|
||||
MIMETypes []string `json:"mime-types" yaml:"mime-types" mapstructure:"mime-types"`
|
||||
Globs []string `json:"globs" yaml:"globs" mapstructure:"globs"`
|
||||
|
||||
// Globs are the glob patterns that will be used to filter which files are cataloged.
|
||||
Globs []string `json:"globs" yaml:"globs" mapstructure:"globs"`
|
||||
|
||||
// Symbols configures symbol extraction settings.
|
||||
Symbols SymbolConfig `json:"symbols" yaml:"symbols" mapstructure:"symbols"`
|
||||
}
|
||||
|
||||
// SymbolConfig holds settings related to symbol capturing from executables.
|
||||
type SymbolConfig struct {
|
||||
// CaptureScope defines the scope of symbols to capture from executables (all binaries, libraries only, applications only, golang binaries only, or none).
|
||||
CaptureScope []SymbolCaptureScope `json:"capture" yaml:"capture" mapstructure:"capture"`
|
||||
|
||||
// Types are the types of Go symbols to capture, relative to `go tool nm` output (e.g. T, t, R, r, D, d, B, b, C, U, etc).
|
||||
// If empty, all symbol types are captured.
|
||||
Types []string `json:"types" yaml:"types" mapstructure:"types"`
|
||||
|
||||
// Go configures Go-specific symbol capturing settings.
|
||||
Go GoSymbolConfig `json:"go" yaml:"go" mapstructure:"go"`
|
||||
}
|
||||
|
||||
// GoSymbolConfig holds settings specific to capturing symbols from binaries built with the golang toolchain.
|
||||
type GoSymbolConfig struct {
|
||||
// StandardLibrary indicates whether to capture Go standard library symbols (e.g. "fmt", "net/http", etc).
|
||||
StandardLibrary bool `json:"standard-library" yaml:"standard-library" mapstructure:"standard-library"`
|
||||
|
||||
// ExtendedStandardLibrary indicates whether to capture extended Go standard library symbols (e.g. "golang.org/x/net", etc).
|
||||
ExtendedStandardLibrary bool `json:"extended-standard-library" yaml:"extended-standard-library" mapstructure:"extended-standard-library"`
|
||||
|
||||
// ThirdPartyModules indicates whether to capture third-party module symbols (e.g. github.com/spf13/cobra, etc).
|
||||
ThirdPartyModules bool `json:"third-party-modules" yaml:"third-party-modules" mapstructure:"third-party-modules"`
|
||||
|
||||
// NormalizeVendoredModules indicates whether to normalize vendored module paths by removing the "vendor/" prefix when capturing third-party module symbols.
|
||||
NormalizeVendoredModules bool `json:"normalize-vendored-modules" yaml:"normalize-vendored-modules" mapstructure:"normalize-vendored-modules"`
|
||||
|
||||
// TypeEqualityFunctions indicates whether to capture type equality functions (e.g. "type..eq..T1..T2") when capturing Go symbols. These are automatically generated by the Go compiler for generic types.
|
||||
TypeEqualityFunctions bool `json:"type-equality-functions" yaml:"type-equality-functions" mapstructure:"type-equality-functions"`
|
||||
|
||||
// GCShapeStencils indicates whether to capture GC shape stencil functions (e.g. "go.shape.*") when capturing Go symbols. These are related to how generics are implemented and are not user defined or directly callable.
|
||||
GCShapeStencils bool `json:"gc-shape-stencils" yaml:"gc-shape-stencils" mapstructure:"gc-shape-stencils"`
|
||||
|
||||
// ExportedSymbols indicates whether to capture only exported (public/global) symbols from Go binaries.
|
||||
ExportedSymbols bool `json:"exported-symbols" yaml:"exported-symbols" mapstructure:"exported-symbols"`
|
||||
|
||||
// UnexportedSymbols indicates whether to capture unexported (private/local) symbols from Go binaries.
|
||||
UnexportedSymbols bool `json:"unexported-symbols" yaml:"unexported-symbols" mapstructure:"unexported-symbols"`
|
||||
}
|
||||
|
||||
// Validate checks for logical configuration inconsistencies and returns an error if any are found.
|
||||
func (c Config) Validate() error {
|
||||
return c.Symbols.Validate()
|
||||
}
|
||||
|
||||
// Validate checks for logical configuration inconsistencies in symbol capture settings.
|
||||
func (s SymbolConfig) Validate() error {
|
||||
// validate that all CaptureScope values are valid
|
||||
for _, scope := range s.CaptureScope {
|
||||
if !isValidCaptureScope(scope) {
|
||||
return fmt.Errorf("invalid symbol capture scope %q: valid values are %q", scope, SymbolScopeGolang)
|
||||
}
|
||||
}
|
||||
|
||||
// validate NM types if specified
|
||||
if len(s.Types) > 0 {
|
||||
for _, t := range s.Types {
|
||||
if !isValidNMType(t) {
|
||||
return fmt.Errorf("invalid NM type %q: valid values are %v", t, validNMTypes())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remaining validations only apply when Go symbol capture is enabled
|
||||
if !s.hasGolangScope() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// if Go symbol capture is enabled, at least one of exported/unexported must be true
|
||||
if !s.Go.ExportedSymbols && !s.Go.UnexportedSymbols {
|
||||
return fmt.Errorf("both exported-symbols and unexported-symbols are disabled; no Go symbols would be captured")
|
||||
}
|
||||
|
||||
// if Go symbol capture is enabled, at least one module source must be enabled
|
||||
if !s.Go.StandardLibrary && !s.Go.ExtendedStandardLibrary && !s.Go.ThirdPartyModules {
|
||||
return fmt.Errorf("all module sources (standard-library, extended-standard-library, third-party-modules) are disabled; no meaningful Go symbols would be captured")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s SymbolConfig) hasGolangScope() bool {
|
||||
for _, scope := range s.CaptureScope {
|
||||
if scope == SymbolScopeGolang {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isValidCaptureScope(scope SymbolCaptureScope) bool {
|
||||
switch scope { //nolint:gocritic // lets elect a pattern as if we'll have multiple options in the future...
|
||||
case SymbolScopeGolang:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type Cataloger struct {
|
||||
@@ -39,6 +149,20 @@ func DefaultConfig() Config {
|
||||
return Config{
|
||||
MIMETypes: m,
|
||||
Globs: nil,
|
||||
Symbols: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{}, // important! by default we do not capture any symbols unless explicitly configured
|
||||
Types: []string{"T", "t"}, // by default only capture "T" (text/code) symbols, since vulnerability data tracks accessible function symbols
|
||||
Go: GoSymbolConfig{
|
||||
StandardLibrary: true,
|
||||
ExtendedStandardLibrary: true,
|
||||
ThirdPartyModules: true,
|
||||
NormalizeVendoredModules: true,
|
||||
ExportedSymbols: true,
|
||||
TypeEqualityFunctions: false, // capturing this adds a lot of noise and have arguably little value
|
||||
GCShapeStencils: false, // capturing this adds a lot of noise and have arguably little value
|
||||
UnexportedSymbols: false, // vulnerabilities tend to track only exported symbols
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +193,7 @@ func (i *Cataloger) CatalogCtx(ctx context.Context, resolver file.Resolver) (map
|
||||
errs := sync.Collect(&ctx, cataloging.ExecutorFile, sync.ToSeq(locs), func(loc file.Location) (*file.Executable, error) {
|
||||
prog.AtomicStage.Set(loc.Path())
|
||||
|
||||
exec, err := processExecutableLocation(loc, resolver)
|
||||
exec, err := i.processExecutableLocation(loc, resolver)
|
||||
if err != nil {
|
||||
err = unknown.New(loc, err)
|
||||
}
|
||||
@@ -89,7 +213,7 @@ func (i *Cataloger) CatalogCtx(ctx context.Context, resolver file.Resolver) (map
|
||||
return results, errs
|
||||
}
|
||||
|
||||
func processExecutableLocation(loc file.Location, resolver file.Resolver) (*file.Executable, error) {
|
||||
func (i *Cataloger) processExecutableLocation(loc file.Location, resolver file.Resolver) (*file.Executable, error) {
|
||||
reader, err := resolver.FileContentsByLocation(loc)
|
||||
if err != nil {
|
||||
log.WithFields("error", err, "path", loc.RealPath).Debug("unable to get file contents")
|
||||
@@ -103,7 +227,52 @@ func processExecutableLocation(loc file.Location, resolver file.Resolver) (*file
|
||||
return nil, fmt.Errorf("unable to get union reader: %w", err)
|
||||
}
|
||||
|
||||
return processExecutable(loc, uReader)
|
||||
return i.processExecutable(loc, uReader)
|
||||
}
|
||||
|
||||
func (i *Cataloger) processExecutable(loc file.Location, reader unionreader.UnionReader) (*file.Executable, error) {
|
||||
data := file.Executable{}
|
||||
|
||||
// determine the executable format
|
||||
|
||||
format, err := findExecutableFormat(reader)
|
||||
if err != nil {
|
||||
log.Debugf("unable to determine executable kind for %v: %v", loc.RealPath, err)
|
||||
return nil, fmt.Errorf("unable to determine executable kind: %w", err)
|
||||
}
|
||||
|
||||
if format == "" {
|
||||
// this is not an "unknown", so just log -- this binary does not have parseable data in it
|
||||
log.Debugf("unable to determine executable format for %q", loc.RealPath)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
data.Format = format
|
||||
|
||||
switch format {
|
||||
case file.ELF:
|
||||
if err = findELFFeatures(&data, reader, i.config.Symbols); err != nil {
|
||||
log.WithFields("error", err, "path", loc.RealPath).Trace("unable to determine ELF features")
|
||||
err = fmt.Errorf("unable to determine ELF features: %w", err)
|
||||
}
|
||||
case file.PE:
|
||||
if err = findPEFeatures(&data, reader, i.config.Symbols); err != nil {
|
||||
log.WithFields("error", err, "path", loc.RealPath).Trace("unable to determine PE features")
|
||||
err = fmt.Errorf("unable to determine PE features: %w", err)
|
||||
}
|
||||
case file.MachO:
|
||||
if err = findMachoFeatures(&data, reader, i.config.Symbols); err != nil {
|
||||
log.WithFields("error", err, "path", loc.RealPath).Trace("unable to determine Macho features")
|
||||
err = fmt.Errorf("unable to determine Macho features: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// always allocate collections for presentation
|
||||
if data.ImportedLibraries == nil {
|
||||
data.ImportedLibraries = []string{}
|
||||
}
|
||||
|
||||
return &data, err
|
||||
}
|
||||
|
||||
func catalogingProgress(locations int64) *monitor.TaskProgress {
|
||||
@@ -152,51 +321,6 @@ func locationMatchesGlob(loc file.Location, globs []string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func processExecutable(loc file.Location, reader unionreader.UnionReader) (*file.Executable, error) {
|
||||
data := file.Executable{}
|
||||
|
||||
// determine the executable format
|
||||
|
||||
format, err := findExecutableFormat(reader)
|
||||
if err != nil {
|
||||
log.Debugf("unable to determine executable kind for %v: %v", loc.RealPath, err)
|
||||
return nil, fmt.Errorf("unable to determine executable kind: %w", err)
|
||||
}
|
||||
|
||||
if format == "" {
|
||||
// this is not an "unknown", so just log -- this binary does not have parseable data in it
|
||||
log.Debugf("unable to determine executable format for %q", loc.RealPath)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
data.Format = format
|
||||
|
||||
switch format {
|
||||
case file.ELF:
|
||||
if err = findELFFeatures(&data, reader); err != nil {
|
||||
log.WithFields("error", err, "path", loc.RealPath).Trace("unable to determine ELF features")
|
||||
err = fmt.Errorf("unable to determine ELF features: %w", err)
|
||||
}
|
||||
case file.PE:
|
||||
if err = findPEFeatures(&data, reader); err != nil {
|
||||
log.WithFields("error", err, "path", loc.RealPath).Trace("unable to determine PE features")
|
||||
err = fmt.Errorf("unable to determine PE features: %w", err)
|
||||
}
|
||||
case file.MachO:
|
||||
if err = findMachoFeatures(&data, reader); err != nil {
|
||||
log.WithFields("error", err, "path", loc.RealPath).Trace("unable to determine Macho features")
|
||||
err = fmt.Errorf("unable to determine Macho features: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// always allocate collections for presentation
|
||||
if data.ImportedLibraries == nil {
|
||||
data.ImportedLibraries = []string{}
|
||||
}
|
||||
|
||||
return &data, err
|
||||
}
|
||||
|
||||
func findExecutableFormat(reader unionreader.UnionReader) (file.ExecutableFormat, error) {
|
||||
// read the first sector of the file
|
||||
buf := make([]byte, 512)
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
package executable
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
func TestDefaultConfig_SymbolCaptureIsDisabled(t *testing.T) {
|
||||
// symbol capture should be disabled by default -- this is an expensive operation space-wise in the SBOM
|
||||
// and should only be enabled when explicitly configured by the user.
|
||||
cfg := DefaultConfig()
|
||||
|
||||
require.Empty(t, cfg.Symbols.CaptureScope, "symbol capture should be disabled by default (empty capture scope)")
|
||||
|
||||
assert.Equal(t, cfg.Symbols.Types, []string{"T", "t"})
|
||||
|
||||
// verify that shouldCaptureSymbols returns false for any executable when using default config
|
||||
assert.False(t, shouldCaptureSymbols(nil, cfg.Symbols), "should not capture symbols for nil executable")
|
||||
assert.False(t, shouldCaptureSymbols(&file.Executable{}, cfg.Symbols), "should not capture symbols for empty executable")
|
||||
assert.False(t, shouldCaptureSymbols(&file.Executable{
|
||||
Toolchains: []file.Toolchain{
|
||||
{Name: "go", Version: "1.21.0", Kind: file.ToolchainKindCompiler},
|
||||
},
|
||||
}, cfg.Symbols), "should not capture symbols even for go binaries when using default config")
|
||||
}
|
||||
|
||||
func TestConfig_Validate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg Config
|
||||
wantErr require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "default config is valid",
|
||||
cfg: DefaultConfig(),
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "valid config with golang scope enabled",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{SymbolScopeGolang},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
ThirdPartyModules: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "empty capture scope with Go settings is valid",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
ThirdPartyModules: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "invalid capture scope",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{"invalid-scope"},
|
||||
},
|
||||
},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "invalid NM type",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
Types: []string{"X", "Y"},
|
||||
},
|
||||
},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "valid NM types",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
Types: []string{"T", "t", "R"},
|
||||
},
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "both exported and unexported disabled with golang scope",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{SymbolScopeGolang},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: false,
|
||||
UnexportedSymbols: false,
|
||||
ThirdPartyModules: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "both exported and unexported disabled without golang scope is valid",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: false,
|
||||
UnexportedSymbols: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "all module sources disabled with golang scope",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{SymbolScopeGolang},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
StandardLibrary: false,
|
||||
ExtendedStandardLibrary: false,
|
||||
ThirdPartyModules: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "all module sources disabled without golang scope is valid",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
StandardLibrary: false,
|
||||
ExtendedStandardLibrary: false,
|
||||
ThirdPartyModules: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "only standard library enabled is valid",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{SymbolScopeGolang},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
StandardLibrary: true,
|
||||
ExtendedStandardLibrary: false,
|
||||
ThirdPartyModules: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "only extended stdlib enabled is valid",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{SymbolScopeGolang},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
StandardLibrary: false,
|
||||
ExtendedStandardLibrary: true,
|
||||
ThirdPartyModules: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "only third party modules enabled is valid",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{SymbolScopeGolang},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
StandardLibrary: false,
|
||||
ExtendedStandardLibrary: false,
|
||||
ThirdPartyModules: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "only unexported symbols enabled is valid",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{SymbolScopeGolang},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: false,
|
||||
UnexportedSymbols: true,
|
||||
ThirdPartyModules: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.cfg.Validate()
|
||||
tt.wantErr(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSymbolConfig_Validate_ErrorMessages(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg SymbolConfig
|
||||
wantErrContain string
|
||||
}{
|
||||
{
|
||||
name: "invalid capture scope error message",
|
||||
cfg: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{"rust"},
|
||||
},
|
||||
wantErrContain: "invalid symbol capture scope",
|
||||
},
|
||||
{
|
||||
name: "invalid NM type error message",
|
||||
cfg: SymbolConfig{
|
||||
Types: []string{"Z"},
|
||||
},
|
||||
wantErrContain: "invalid NM type",
|
||||
},
|
||||
{
|
||||
name: "both export options disabled error message",
|
||||
cfg: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{SymbolScopeGolang},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: false,
|
||||
UnexportedSymbols: false,
|
||||
ThirdPartyModules: true,
|
||||
},
|
||||
},
|
||||
wantErrContain: "both exported-symbols and unexported-symbols are disabled",
|
||||
},
|
||||
{
|
||||
name: "all module sources disabled error message",
|
||||
cfg: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{SymbolScopeGolang},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
StandardLibrary: false,
|
||||
ExtendedStandardLibrary: false,
|
||||
ThirdPartyModules: false,
|
||||
},
|
||||
},
|
||||
wantErrContain: "all module sources",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.cfg.Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.wantErrContain)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/anchore/syft/syft/internal/unionreader"
|
||||
)
|
||||
|
||||
func findELFFeatures(data *file.Executable, reader unionreader.UnionReader) error {
|
||||
func findELFFeatures(data *file.Executable, reader unionreader.UnionReader, cfg SymbolConfig) error {
|
||||
f, err := elf.NewFile(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -34,10 +34,168 @@ func findELFFeatures(data *file.Executable, reader unionreader.UnionReader) erro
|
||||
data.ELFSecurityFeatures = findELFSecurityFeatures(f)
|
||||
data.HasEntrypoint = elfHasEntrypoint(f)
|
||||
data.HasExports = elfHasExports(f)
|
||||
data.Toolchains = elfToolchains(reader, f)
|
||||
if shouldCaptureSymbols(data, cfg) {
|
||||
data.SymbolNames = elfNMSymbols(f, cfg, data.Toolchains)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func elfToolchains(reader unionreader.UnionReader, f *elf.File) []file.Toolchain {
|
||||
return includeNoneNil(
|
||||
golangToolchainEvidence(reader),
|
||||
cToolchainEvidence(f),
|
||||
)
|
||||
}
|
||||
|
||||
func includeNoneNil(evidence ...*file.Toolchain) []file.Toolchain {
|
||||
var toolchains []file.Toolchain
|
||||
for _, e := range evidence {
|
||||
if e != nil {
|
||||
toolchains = append(toolchains, *e)
|
||||
}
|
||||
}
|
||||
return toolchains
|
||||
}
|
||||
|
||||
func elfNMSymbols(f *elf.File, cfg SymbolConfig, toolchains []file.Toolchain) []string {
|
||||
if isGoToolchainPresent(toolchains) {
|
||||
return captureElfGoSymbols(f, cfg)
|
||||
}
|
||||
|
||||
// include all symbols
|
||||
syms, err := f.Symbols()
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Trace("unable to read symbols from elf file")
|
||||
return nil
|
||||
}
|
||||
|
||||
var symbols []string
|
||||
for _, sym := range syms {
|
||||
symbols = append(symbols, sym.Name)
|
||||
}
|
||||
return symbols
|
||||
}
|
||||
|
||||
func captureElfGoSymbols(f *elf.File, cfg SymbolConfig) []string {
|
||||
syms, err := f.Symbols()
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Trace("unable to read symbols from elf file")
|
||||
return nil
|
||||
}
|
||||
|
||||
var symbols []string
|
||||
filter := createGoSymbolFilter(cfg)
|
||||
for _, sym := range syms {
|
||||
name, include := filter(sym.Name, elfSymbolType(sym, f.Sections))
|
||||
if include {
|
||||
symbols = append(symbols, name)
|
||||
}
|
||||
}
|
||||
return symbols
|
||||
}
|
||||
|
||||
// elfSymbolType returns the nm-style single character representing the symbol type.
|
||||
// This mimics the output of `nm` for ELF binaries.
|
||||
func elfSymbolType(sym elf.Symbol, sections []*elf.Section) string {
|
||||
binding := elf.ST_BIND(sym.Info)
|
||||
symType := elf.ST_TYPE(sym.Info)
|
||||
|
||||
// handle special section indices first
|
||||
switch sym.Section {
|
||||
case elf.SHN_UNDEF:
|
||||
// undefined symbols
|
||||
if binding == elf.STB_WEAK {
|
||||
if symType == elf.STT_OBJECT {
|
||||
return "v" // weak object
|
||||
}
|
||||
return "w" // weak symbol
|
||||
}
|
||||
return "U" // undefined (always uppercase)
|
||||
|
||||
case elf.SHN_ABS:
|
||||
// absolute symbols
|
||||
if binding == elf.STB_LOCAL {
|
||||
return "a"
|
||||
}
|
||||
return "A"
|
||||
|
||||
case elf.SHN_COMMON:
|
||||
// common symbols (uninitialized data)
|
||||
return "C" // always uppercase per nm convention
|
||||
}
|
||||
|
||||
// for defined symbols, determine type based on section characteristics
|
||||
typeChar := elfSectionTypeChar(sym.Section, sections)
|
||||
|
||||
// handle weak symbols
|
||||
if binding == elf.STB_WEAK {
|
||||
if typeChar == 'U' || typeChar == 'u' {
|
||||
if symType == elf.STT_OBJECT {
|
||||
return "v"
|
||||
}
|
||||
return "w"
|
||||
}
|
||||
// weak defined symbol
|
||||
if binding == elf.STB_LOCAL {
|
||||
return strings.ToLower(string(typeChar))
|
||||
}
|
||||
// use 'W' for weak defined, or 'V' for weak object
|
||||
if symType == elf.STT_OBJECT {
|
||||
return "V"
|
||||
}
|
||||
return "W"
|
||||
}
|
||||
|
||||
// local symbols are lowercase, global symbols are uppercase
|
||||
if binding == elf.STB_LOCAL {
|
||||
return strings.ToLower(string(typeChar))
|
||||
}
|
||||
return string(typeChar)
|
||||
}
|
||||
|
||||
// elfSectionTypeChar returns the nm-style character based on section flags and type.
|
||||
func elfSectionTypeChar(sectIdx elf.SectionIndex, sections []*elf.Section) byte {
|
||||
idx := int(sectIdx)
|
||||
// the sections slice from debug/elf includes the NULL section at index 0, so we use idx directly
|
||||
if idx < 0 || idx >= len(sections) {
|
||||
return '?'
|
||||
}
|
||||
|
||||
section := sections[idx]
|
||||
flags := section.Flags
|
||||
stype := section.Type
|
||||
|
||||
// check section characteristics to determine symbol type
|
||||
switch {
|
||||
case flags&elf.SHF_EXECINSTR != 0:
|
||||
// executable section -> text
|
||||
return 'T'
|
||||
|
||||
case stype == elf.SHT_NOBITS:
|
||||
// uninitialized data section -> BSS
|
||||
return 'B'
|
||||
|
||||
case flags&elf.SHF_WRITE == 0 && flags&elf.SHF_ALLOC != 0:
|
||||
// read-only allocated section -> rodata
|
||||
return 'R'
|
||||
|
||||
case flags&elf.SHF_WRITE != 0 && flags&elf.SHF_ALLOC != 0:
|
||||
// writable allocated section -> data
|
||||
return 'D'
|
||||
|
||||
case flags&elf.SHF_ALLOC != 0:
|
||||
// other allocated section
|
||||
return 'D'
|
||||
|
||||
default:
|
||||
// non-allocated sections (debug info, etc.)
|
||||
// nm typically shows 'n' for debug, but we'll use 'N' for consistency
|
||||
return 'N'
|
||||
}
|
||||
}
|
||||
|
||||
func findELFSecurityFeatures(f *elf.File) *file.ELFSecurityFeatures {
|
||||
return &file.ELFSecurityFeatures{
|
||||
SymbolTableStripped: isElfSymbolTableStripped(f),
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -226,3 +227,350 @@ func Test_elfHasExports(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_elfGoToolchainDetection(t *testing.T) {
|
||||
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
|
||||
t.Helper()
|
||||
f, err := os.Open(filepath.Join("test-fixtures/golang", fixture))
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fixture string
|
||||
wantPresent bool
|
||||
}{
|
||||
{
|
||||
name: "go binary has toolchain",
|
||||
fixture: "bin/hello_linux",
|
||||
wantPresent: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reader := readerForFixture(t, tt.fixture)
|
||||
f, err := elf.NewFile(reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
toolchains := elfToolchains(reader, f)
|
||||
assert.Equal(t, tt.wantPresent, isGoToolchainPresent(toolchains))
|
||||
|
||||
if tt.wantPresent {
|
||||
require.NotEmpty(t, toolchains)
|
||||
assert.Equal(t, "go", toolchains[0].Name)
|
||||
assert.NotEmpty(t, toolchains[0].Version)
|
||||
assert.Equal(t, file.ToolchainKindCompiler, toolchains[0].Kind)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_elfCgoToolchainDetection(t *testing.T) {
|
||||
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
|
||||
t.Helper()
|
||||
f, err := os.Open(filepath.Join("test-fixtures/golang", fixture))
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}
|
||||
|
||||
t.Run("cgo binary has both go and c toolchains", func(t *testing.T) {
|
||||
reader := readerForFixture(t, "bin/hello_linux_cgo")
|
||||
f, err := elf.NewFile(reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
toolchains := elfToolchains(reader, f)
|
||||
|
||||
// versions are dynamic based on Docker image, so we ignore them in comparison
|
||||
want := []file.Toolchain{
|
||||
{Name: "go", Kind: file.ToolchainKindCompiler},
|
||||
{Name: "gcc", Kind: file.ToolchainKindCompiler},
|
||||
}
|
||||
|
||||
if d := cmp.Diff(want, toolchains, cmpopts.IgnoreFields(file.Toolchain{}, "Version")); d != "" {
|
||||
t.Errorf("elfToolchains() mismatch (-want +got):\n%s", d)
|
||||
}
|
||||
|
||||
// verify versions are populated
|
||||
for _, tc := range toolchains {
|
||||
assert.NotEmpty(t, tc.Version, "expected version to be set for %s toolchain", tc.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_elfGoSymbolCapture(t *testing.T) {
|
||||
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
|
||||
t.Helper()
|
||||
f, err := os.Open(filepath.Join("test-fixtures/golang", fixture))
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fixture string
|
||||
cfg SymbolConfig
|
||||
wantSymbols []string // exact symbol names that must be present
|
||||
wantMinSymbolCount int
|
||||
}{
|
||||
{
|
||||
name: "capture all symbol types",
|
||||
fixture: "bin/hello_linux",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
StandardLibrary: true,
|
||||
ExtendedStandardLibrary: true,
|
||||
ThirdPartyModules: true,
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
},
|
||||
},
|
||||
wantSymbols: []string{
|
||||
// stdlib - fmt package (used via fmt.Println)
|
||||
"fmt.(*fmt).fmtInteger",
|
||||
"fmt.(*pp).doPrintf",
|
||||
// stdlib - strings package (used via strings.ToUpper)
|
||||
"strings.ToUpper",
|
||||
"strings.Map",
|
||||
// stdlib - encoding/json package (used via json.Marshal)
|
||||
"encoding/json.Marshal",
|
||||
// extended stdlib - golang.org/x/text (used via language.English)
|
||||
"golang.org/x/text/internal/language.Tag.String",
|
||||
"golang.org/x/text/internal/language.Language.String",
|
||||
// third-party - go-spew (used via spew.Dump)
|
||||
"github.com/davecgh/go-spew/spew.(*dumpState).dump",
|
||||
"github.com/davecgh/go-spew/spew.fdump",
|
||||
},
|
||||
wantMinSymbolCount: 50,
|
||||
},
|
||||
{
|
||||
name: "capture only third-party symbols",
|
||||
fixture: "bin/hello_linux",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ThirdPartyModules: true,
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
},
|
||||
},
|
||||
wantSymbols: []string{
|
||||
"github.com/davecgh/go-spew/spew.(*dumpState).dump",
|
||||
"github.com/davecgh/go-spew/spew.(*formatState).Format",
|
||||
"github.com/davecgh/go-spew/spew.fdump",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "capture only extended stdlib symbols",
|
||||
fixture: "bin/hello_linux",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExtendedStandardLibrary: true,
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
},
|
||||
},
|
||||
wantSymbols: []string{
|
||||
"golang.org/x/text/internal/language.Tag.String",
|
||||
"golang.org/x/text/internal/language.Parse",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "capture with text section types only",
|
||||
fixture: "bin/hello_linux",
|
||||
cfg: SymbolConfig{
|
||||
Types: []string{"T", "t"}, // text section (code) symbols
|
||||
Go: GoSymbolConfig{
|
||||
StandardLibrary: true,
|
||||
ExtendedStandardLibrary: true,
|
||||
ThirdPartyModules: true,
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
},
|
||||
},
|
||||
wantSymbols: []string{
|
||||
"encoding/json.Marshal",
|
||||
"strings.ToUpper",
|
||||
},
|
||||
wantMinSymbolCount: 10,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reader := readerForFixture(t, tt.fixture)
|
||||
f, err := elf.NewFile(reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
symbols := captureElfGoSymbols(f, tt.cfg)
|
||||
symbolSet := make(map[string]struct{}, len(symbols))
|
||||
for _, sym := range symbols {
|
||||
symbolSet[sym] = struct{}{}
|
||||
}
|
||||
|
||||
if tt.wantMinSymbolCount > 0 {
|
||||
assert.GreaterOrEqual(t, len(symbols), tt.wantMinSymbolCount,
|
||||
"expected at least %d symbols, got %d", tt.wantMinSymbolCount, len(symbols))
|
||||
}
|
||||
|
||||
for _, want := range tt.wantSymbols {
|
||||
_, found := symbolSet[want]
|
||||
assert.True(t, found, "expected symbol %q to be present", want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_elfNMSymbols_goReturnsSymbols(t *testing.T) {
|
||||
// for Go binaries, elfNMSymbols should return symbols when Go toolchain is present
|
||||
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
|
||||
t.Helper()
|
||||
f, err := os.Open(filepath.Join("test-fixtures/golang", fixture))
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}
|
||||
|
||||
reader := readerForFixture(t, "bin/hello_linux")
|
||||
f, err := elf.NewFile(reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
toolchains := []file.Toolchain{
|
||||
{Name: "go", Version: "1.24", Kind: file.ToolchainKindCompiler},
|
||||
}
|
||||
cfg := SymbolConfig{
|
||||
Types: []string{"T", "t"},
|
||||
Go: GoSymbolConfig{
|
||||
StandardLibrary: true,
|
||||
ExtendedStandardLibrary: true,
|
||||
ThirdPartyModules: true,
|
||||
ExportedSymbols: true,
|
||||
},
|
||||
}
|
||||
|
||||
symbols := elfNMSymbols(f, cfg, toolchains)
|
||||
assert.NotNil(t, symbols, "expected symbols for Go binary")
|
||||
assert.NotEmpty(t, symbols, "expected non-empty symbols for Go binary")
|
||||
}
|
||||
|
||||
func Test_elfSymbolType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
sym elf.Symbol
|
||||
sections []*elf.Section
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "undefined symbol",
|
||||
sym: elf.Symbol{
|
||||
Info: byte(elf.STB_GLOBAL)<<4 | byte(elf.STT_NOTYPE),
|
||||
Section: elf.SHN_UNDEF,
|
||||
},
|
||||
want: "U",
|
||||
},
|
||||
{
|
||||
name: "absolute symbol global",
|
||||
sym: elf.Symbol{
|
||||
Info: byte(elf.STB_GLOBAL)<<4 | byte(elf.STT_NOTYPE),
|
||||
Section: elf.SHN_ABS,
|
||||
},
|
||||
want: "A",
|
||||
},
|
||||
{
|
||||
name: "absolute symbol local",
|
||||
sym: elf.Symbol{
|
||||
Info: byte(elf.STB_LOCAL)<<4 | byte(elf.STT_NOTYPE),
|
||||
Section: elf.SHN_ABS,
|
||||
},
|
||||
want: "a",
|
||||
},
|
||||
{
|
||||
name: "common symbol",
|
||||
sym: elf.Symbol{
|
||||
Info: byte(elf.STB_GLOBAL)<<4 | byte(elf.STT_OBJECT),
|
||||
Section: elf.SHN_COMMON,
|
||||
},
|
||||
want: "C",
|
||||
},
|
||||
{
|
||||
name: "weak undefined symbol",
|
||||
sym: elf.Symbol{
|
||||
Info: byte(elf.STB_WEAK)<<4 | byte(elf.STT_NOTYPE),
|
||||
Section: elf.SHN_UNDEF,
|
||||
},
|
||||
want: "w",
|
||||
},
|
||||
{
|
||||
name: "weak undefined object",
|
||||
sym: elf.Symbol{
|
||||
Info: byte(elf.STB_WEAK)<<4 | byte(elf.STT_OBJECT),
|
||||
Section: elf.SHN_UNDEF,
|
||||
},
|
||||
want: "v",
|
||||
},
|
||||
{
|
||||
name: "text section global",
|
||||
sym: elf.Symbol{
|
||||
Info: byte(elf.STB_GLOBAL)<<4 | byte(elf.STT_FUNC),
|
||||
Section: 1,
|
||||
},
|
||||
sections: []*elf.Section{
|
||||
{SectionHeader: elf.SectionHeader{Type: elf.SHT_NULL}}, // index 0: NULL section
|
||||
{SectionHeader: elf.SectionHeader{Flags: elf.SHF_ALLOC | elf.SHF_EXECINSTR}}, // index 1: .text
|
||||
},
|
||||
want: "T",
|
||||
},
|
||||
{
|
||||
name: "text section local",
|
||||
sym: elf.Symbol{
|
||||
Info: byte(elf.STB_LOCAL)<<4 | byte(elf.STT_FUNC),
|
||||
Section: 1,
|
||||
},
|
||||
sections: []*elf.Section{
|
||||
{SectionHeader: elf.SectionHeader{Type: elf.SHT_NULL}}, // index 0: NULL section
|
||||
{SectionHeader: elf.SectionHeader{Flags: elf.SHF_ALLOC | elf.SHF_EXECINSTR}}, // index 1: .text
|
||||
},
|
||||
want: "t",
|
||||
},
|
||||
{
|
||||
name: "data section global",
|
||||
sym: elf.Symbol{
|
||||
Info: byte(elf.STB_GLOBAL)<<4 | byte(elf.STT_OBJECT),
|
||||
Section: 1,
|
||||
},
|
||||
sections: []*elf.Section{
|
||||
{SectionHeader: elf.SectionHeader{Type: elf.SHT_NULL}}, // index 0: NULL section
|
||||
{SectionHeader: elf.SectionHeader{Flags: elf.SHF_ALLOC | elf.SHF_WRITE}}, // index 1: .data
|
||||
},
|
||||
want: "D",
|
||||
},
|
||||
{
|
||||
name: "bss section global",
|
||||
sym: elf.Symbol{
|
||||
Info: byte(elf.STB_GLOBAL)<<4 | byte(elf.STT_OBJECT),
|
||||
Section: 1,
|
||||
},
|
||||
sections: []*elf.Section{
|
||||
{SectionHeader: elf.SectionHeader{Type: elf.SHT_NULL}}, // index 0: NULL section
|
||||
{SectionHeader: elf.SectionHeader{Type: elf.SHT_NOBITS, Flags: elf.SHF_ALLOC | elf.SHF_WRITE}}, // index 1: .bss
|
||||
},
|
||||
want: "B",
|
||||
},
|
||||
{
|
||||
name: "rodata section global",
|
||||
sym: elf.Symbol{
|
||||
Info: byte(elf.STB_GLOBAL)<<4 | byte(elf.STT_OBJECT),
|
||||
Section: 1,
|
||||
},
|
||||
sections: []*elf.Section{
|
||||
{SectionHeader: elf.SectionHeader{Type: elf.SHT_NULL}}, // index 0: NULL section
|
||||
{SectionHeader: elf.SectionHeader{Flags: elf.SHF_ALLOC}}, // index 1: .rodata (no write flag = read-only)
|
||||
},
|
||||
want: "R",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := elfSymbolType(tt.sym, tt.sections)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
package executable
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
)
|
||||
|
||||
var goNMTypes = []string{
|
||||
"T", // text (code) segment symbol
|
||||
"t", // static text segment symbol
|
||||
"R", // read-only data segment symbol
|
||||
"r", // static read-only data segment symbol
|
||||
"D", // data segment symbol
|
||||
"d", // static data segment symbol
|
||||
"B", // bss segment symbol
|
||||
"b", // static bss segment symbol
|
||||
"C", // constant address
|
||||
"U", // referenced but undefined symbol
|
||||
}
|
||||
|
||||
// validNMTypes returns the list of valid NM types for Go symbols.
|
||||
func validNMTypes() []string {
|
||||
return goNMTypes
|
||||
}
|
||||
|
||||
// isValidNMType checks if the given type is a valid NM type.
|
||||
func isValidNMType(t string) bool {
|
||||
for _, valid := range goNMTypes {
|
||||
if t == valid {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const (
|
||||
vendorPrefix = "vendor/"
|
||||
extendedStdlibPrefix = "golang.org/x/"
|
||||
typeEqualityPrefix = "type:.eq."
|
||||
gcShapeStencilPrefix = "go.shape."
|
||||
)
|
||||
|
||||
// createGoSymbolFilter creates a filter function for Go symbols based on the provided configuration. This filter function
|
||||
// returns true if a symbol should be included based on its name and type. This also allows for modification of the symbol name
|
||||
// if necessary (e.g., normalization of vendored module paths). The returned name is only valid if the boolean is true.
|
||||
func createGoSymbolFilter(cfg SymbolConfig) func(string, string) (string, bool) {
|
||||
validNmTypes := buildNmTypes(cfg.Types)
|
||||
|
||||
return func(symName, symType string) (string, bool) {
|
||||
// check if this is a valid type
|
||||
if !validNmTypes.Has(symType) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// filter out floating point literals and other compiler literals (e.g., $f64.3fceb851eb851eb8)
|
||||
if isCompilerLiteral(symName) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// filter based on exported/unexported symbol configuration
|
||||
exported := isExportedSymbol(symName)
|
||||
if !shouldIncludeByExportStatus(exported, cfg.Go.ExportedSymbols, cfg.Go.UnexportedSymbols) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// handle type equality functions (e.g., type:.eq.myStruct)
|
||||
if isTypeEqualityFunction(symName) {
|
||||
if !cfg.Go.TypeEqualityFunctions {
|
||||
return "", false
|
||||
}
|
||||
return symName, true
|
||||
}
|
||||
|
||||
// handle GC shape stencil functions (e.g., go.shape.func())
|
||||
if isGCShapeStencil(symName) {
|
||||
if !cfg.Go.GCShapeStencils {
|
||||
return "", false
|
||||
}
|
||||
return symName, true
|
||||
}
|
||||
|
||||
// normalize vendored module paths if configured
|
||||
symName = normalizeVendoredPath(symName, cfg.Go.NormalizeVendoredModules)
|
||||
|
||||
// determine the package path for classification
|
||||
pkgPath := extractPackagePath(symName)
|
||||
|
||||
// handle extended stdlib (golang.org/x/*)
|
||||
if isExtendedStdlib(pkgPath) {
|
||||
if !cfg.Go.ExtendedStandardLibrary {
|
||||
return "", false
|
||||
}
|
||||
return symName, true
|
||||
}
|
||||
|
||||
// handle stdlib packages
|
||||
if isStdlibPackage(pkgPath) {
|
||||
if !cfg.Go.StandardLibrary {
|
||||
return "", false
|
||||
}
|
||||
return symName, true
|
||||
}
|
||||
|
||||
// this is a third-party package
|
||||
if !cfg.Go.ThirdPartyModules {
|
||||
return "", false
|
||||
}
|
||||
return symName, true
|
||||
}
|
||||
}
|
||||
|
||||
// buildNmTypes creates a set of valid NM types from the configuration.
|
||||
// If no types are specified, all default types are used.
|
||||
func buildNmTypes(types []string) *strset.Set {
|
||||
tys := strset.New(types...)
|
||||
if tys.Size() == 0 {
|
||||
return strset.New(goNMTypes...)
|
||||
}
|
||||
|
||||
// only allow valid nm types to continue...
|
||||
return strset.Intersection(strset.New(goNMTypes...), tys)
|
||||
}
|
||||
|
||||
// isCompilerLiteral checks if a symbol is literal symbol inserted by the compiler.
|
||||
// This includes floating point literals, int constants, and others.
|
||||
// These have the format: $f64.3fceb851eb851eb8 or $f32.3f800000
|
||||
// where the hex represents the IEEE 754 representation of the value.
|
||||
func isCompilerLiteral(symName string) bool {
|
||||
return strings.HasPrefix(symName, "$")
|
||||
}
|
||||
|
||||
// shouldIncludeByExportStatus determines if a symbol should be included based on its
|
||||
// export status and the configuration settings for exported/unexported symbols.
|
||||
func shouldIncludeByExportStatus(exported, includeExported, includeUnexported bool) bool {
|
||||
if exported && !includeExported {
|
||||
return false
|
||||
}
|
||||
if !exported && !includeUnexported {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// isTypeEqualityFunction checks if a symbol is a compiler-generated type equality function.
|
||||
// These are automatically generated by the Go compiler for generic types
|
||||
// and have the format: type:.eq.TypeName
|
||||
func isTypeEqualityFunction(symName string) bool {
|
||||
return strings.HasPrefix(symName, typeEqualityPrefix)
|
||||
}
|
||||
|
||||
// isGCShapeStencil checks if a symbol is a GC shape stencil function.
|
||||
// These are related to how generics are implemented and are not user defined or directly callable.
|
||||
// They can appear as a prefix (e.g., "go.shape.func()") or within generic type parameters
|
||||
// (e.g., "slices.partitionCmpFunc[go.shape.struct { ... }]").
|
||||
func isGCShapeStencil(symName string) bool {
|
||||
// check for prefix: go.shape.*
|
||||
if strings.HasPrefix(symName, gcShapeStencilPrefix) {
|
||||
return true
|
||||
}
|
||||
// check for embedded in generic type parameter: [go.shape.*
|
||||
return strings.Contains(symName, "["+gcShapeStencilPrefix)
|
||||
}
|
||||
|
||||
// normalizeVendoredPath removes the "vendor/" prefix from vendored module paths if normalization is enabled.
|
||||
func normalizeVendoredPath(symName string, normalize bool) string {
|
||||
if normalize && isVendoredPath(symName) {
|
||||
return strings.TrimPrefix(symName, vendorPrefix)
|
||||
}
|
||||
return symName
|
||||
}
|
||||
|
||||
// isVendoredPath checks if a symbol name represents a vendored module path.
|
||||
func isVendoredPath(symName string) bool {
|
||||
return strings.HasPrefix(symName, vendorPrefix)
|
||||
}
|
||||
|
||||
// isExtendedStdlib checks if a package path is part of the Go extended standard library (golang.org/x/*).
|
||||
func isExtendedStdlib(pkgPath string) bool {
|
||||
return strings.HasPrefix(pkgPath, extendedStdlibPrefix)
|
||||
}
|
||||
|
||||
// extractPackagePath extracts the package import path from a symbol name.
|
||||
// For example, "github.com/foo/bar.Baz" returns "github.com/foo/bar".
|
||||
func extractPackagePath(symName string) string {
|
||||
lastDot := strings.LastIndex(symName, ".")
|
||||
if lastDot == -1 {
|
||||
return symName
|
||||
}
|
||||
return symName[:lastDot]
|
||||
}
|
||||
|
||||
// isExportedSymbol checks if a symbol is exported (public) by examining if the first
|
||||
// character of the symbol name (after the last '.') is uppercase.
|
||||
func isExportedSymbol(symName string) bool {
|
||||
lastDot := strings.LastIndex(symName, ".")
|
||||
if lastDot == -1 || lastDot >= len(symName)-1 {
|
||||
return false
|
||||
}
|
||||
firstRune, _ := utf8.DecodeRuneInString(symName[lastDot+1:])
|
||||
return unicode.IsUpper(firstRune)
|
||||
}
|
||||
|
||||
// isStdlibPackage determines if a package path represents a Go standard library package.
|
||||
// Stdlib packages don't contain a '.' in their path (they use simple names like "fmt", "net/http").
|
||||
// Third-party packages start with a domain containing a '.' (e.g., "github.com", "golang.org").
|
||||
func isStdlibPackage(pkgPath string) bool {
|
||||
// the "main" package is treated as stdlib for our purposes
|
||||
if pkgPath == "main" {
|
||||
return true
|
||||
}
|
||||
// stdlib packages don't contain dots in their import path
|
||||
return !strings.Contains(pkgPath, ".")
|
||||
}
|
||||
@@ -0,0 +1,989 @@
|
||||
package executable
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_buildNmTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
types []string
|
||||
wantSize int
|
||||
contains []string
|
||||
}{
|
||||
{
|
||||
name: "empty types uses defaults",
|
||||
types: nil,
|
||||
wantSize: len(goNMTypes),
|
||||
contains: []string{"T", "t", "R", "r", "D", "d", "B", "b", "C", "U"},
|
||||
},
|
||||
{
|
||||
name: "custom types",
|
||||
types: []string{"T", "t"},
|
||||
wantSize: 2,
|
||||
contains: []string{"T", "t"},
|
||||
},
|
||||
{
|
||||
name: "invalid types",
|
||||
types: []string{"T", "t", "m", ",", "thing!"},
|
||||
wantSize: 2,
|
||||
contains: []string{"T", "t"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := buildNmTypes(tt.types)
|
||||
assert.Equal(t, tt.wantSize, got.Size())
|
||||
for _, c := range tt.contains {
|
||||
assert.True(t, got.Has(c), "expected set to contain %q", c)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isCompilerLiteral(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
symName string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "64-bit float literal",
|
||||
symName: "$f64.3fceb851eb851eb8",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "32-bit float literal",
|
||||
symName: "$f32.3f800000",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "other dollar prefix",
|
||||
symName: "$something",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "regular symbol",
|
||||
symName: "main.main",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
symName: "",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := isCompilerLiteral(tt.symName)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_shouldIncludeByExportStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
exported bool
|
||||
includeExported bool
|
||||
includeUnexported bool
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "exported symbol with both enabled",
|
||||
exported: true,
|
||||
includeExported: true,
|
||||
includeUnexported: true,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "unexported symbol with both enabled",
|
||||
exported: false,
|
||||
includeExported: true,
|
||||
includeUnexported: true,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "exported symbol with only exported enabled",
|
||||
exported: true,
|
||||
includeExported: true,
|
||||
includeUnexported: false,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "unexported symbol with only exported enabled",
|
||||
exported: false,
|
||||
includeExported: true,
|
||||
includeUnexported: false,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "exported symbol with only unexported enabled",
|
||||
exported: true,
|
||||
includeExported: false,
|
||||
includeUnexported: true,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "unexported symbol with only unexported enabled",
|
||||
exported: false,
|
||||
includeExported: false,
|
||||
includeUnexported: true,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "exported symbol with both disabled",
|
||||
exported: true,
|
||||
includeExported: false,
|
||||
includeUnexported: false,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "unexported symbol with both disabled",
|
||||
exported: false,
|
||||
includeExported: false,
|
||||
includeUnexported: false,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := shouldIncludeByExportStatus(tt.exported, tt.includeExported, tt.includeUnexported)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isTypeEqualityFunction(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
symName string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "type equality function",
|
||||
symName: "type:.eq.myStruct",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "type equality with package",
|
||||
symName: "type:.eq.main.MyType",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "regular function",
|
||||
symName: "main.main",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "similar but not type equality",
|
||||
symName: "mytype:.eq.something",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
symName: "",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := isTypeEqualityFunction(tt.symName)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isGCShapeStencil(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
symName string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "gc shape stencil function prefix",
|
||||
symName: "go.shape.func()",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "gc shape with type prefix",
|
||||
symName: "go.shape.int",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "gc shape in generic type parameter - struct",
|
||||
symName: `slices.partitionCmpFunc[go.shape.struct { Key string "json:\"key,omitempty\""; Value go.opentelemetry.io/otel/trace/internal/telemetry.Value "json:\"value,omitempty\"" }]`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "gc shape in generic type parameter - interface",
|
||||
symName: "slices.pdqsortCmpFunc[go.shape.interface { Info() (io/fs.FileInfo, error); IsDir() bool; Name() string; Type() io/fs.FileMode }]",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "gc shape in generic - syft location",
|
||||
symName: `slices.partitionCmpFunc[go.shape.struct { github.com/anchore/syft/syft/file.LocationData "cyclonedx:\"\""; github.com/anchore/syft/syft/file.LocationMetadata "cyclonedx:\"\"" }]`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "gc shape in generic - rotate",
|
||||
symName: "slices.rotateCmpFunc[go.shape.struct { Key go.opentelemetry.io/otel/attribute.Key; Value go.opentelemetry.io/otel/attribute.Value }]",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "regular function",
|
||||
symName: "main.main",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "go package but not shape",
|
||||
symName: "go.string.something",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "generic without go.shape",
|
||||
symName: "slices.Sort[int]",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "go.shape in comment or string would not match",
|
||||
symName: "mypackage.FuncWithComment",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
symName: "",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := isGCShapeStencil(tt.symName)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_normalizeVendoredPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
symName string
|
||||
normalize bool
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "vendored path with normalization enabled",
|
||||
symName: "vendor/github.com/foo/bar.Baz",
|
||||
normalize: true,
|
||||
want: "github.com/foo/bar.Baz",
|
||||
},
|
||||
{
|
||||
name: "vendored path with normalization disabled",
|
||||
symName: "vendor/github.com/foo/bar.Baz",
|
||||
normalize: false,
|
||||
want: "vendor/github.com/foo/bar.Baz",
|
||||
},
|
||||
{
|
||||
name: "non-vendored path with normalization enabled",
|
||||
symName: "github.com/foo/bar.Baz",
|
||||
normalize: true,
|
||||
want: "github.com/foo/bar.Baz",
|
||||
},
|
||||
{
|
||||
name: "non-vendored path with normalization disabled",
|
||||
symName: "github.com/foo/bar.Baz",
|
||||
normalize: false,
|
||||
want: "github.com/foo/bar.Baz",
|
||||
},
|
||||
{
|
||||
name: "stdlib path with normalization enabled",
|
||||
symName: "fmt.Println",
|
||||
normalize: true,
|
||||
want: "fmt.Println",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := normalizeVendoredPath(tt.symName, tt.normalize)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isVendoredPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
symName string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "vendored third-party",
|
||||
symName: "vendor/github.com/foo/bar.Baz",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "non-vendored third-party",
|
||||
symName: "github.com/foo/bar.Baz",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "stdlib",
|
||||
symName: "fmt.Println",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "main package",
|
||||
symName: "main.main",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
symName: "",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := isVendoredPath(tt.symName)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isExtendedStdlib(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pkgPath string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "golang.org/x/net",
|
||||
pkgPath: "golang.org/x/net",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "golang.org/x/text/encoding",
|
||||
pkgPath: "golang.org/x/text/encoding",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "golang.org/x/sys/unix",
|
||||
pkgPath: "golang.org/x/sys/unix",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "regular golang.org package",
|
||||
pkgPath: "golang.org/protobuf",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "github package",
|
||||
pkgPath: "github.com/foo/bar",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "stdlib",
|
||||
pkgPath: "fmt",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
pkgPath: "",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := isExtendedStdlib(tt.pkgPath)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_extractPackagePath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
symName string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "simple package",
|
||||
symName: "fmt.Println",
|
||||
want: "fmt",
|
||||
},
|
||||
{
|
||||
name: "nested stdlib package",
|
||||
symName: "net/http.ListenAndServe",
|
||||
want: "net/http",
|
||||
},
|
||||
{
|
||||
name: "third-party package",
|
||||
symName: "github.com/foo/bar.Baz",
|
||||
want: "github.com/foo/bar",
|
||||
},
|
||||
{
|
||||
name: "deep third-party package",
|
||||
symName: "github.com/foo/bar/pkg/util.Helper",
|
||||
want: "github.com/foo/bar/pkg/util",
|
||||
},
|
||||
{
|
||||
name: "main package",
|
||||
symName: "main.main",
|
||||
want: "main",
|
||||
},
|
||||
{
|
||||
name: "no dot (just package name)",
|
||||
symName: "fmt",
|
||||
want: "fmt",
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
symName: "",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "method with receiver",
|
||||
symName: "github.com/foo/bar.(*MyType).Method",
|
||||
want: "github.com/foo/bar.(*MyType)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := extractPackagePath(tt.symName)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isExportedSymbol(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
symName string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "exported function",
|
||||
symName: "fmt.Println",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "unexported function",
|
||||
symName: "fmt.println",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "exported in main",
|
||||
symName: "main.Main",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "unexported main",
|
||||
symName: "main.main",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "exported third-party",
|
||||
symName: "github.com/foo/bar.Export",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "unexported third-party",
|
||||
symName: "github.com/foo/bar.private",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "unicode uppercase",
|
||||
symName: "main.Über",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "unicode lowercase",
|
||||
symName: "main.über",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "no dot",
|
||||
symName: "nodot",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
symName: "",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "dot at end",
|
||||
symName: "main.",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "underscore start (unexported)",
|
||||
symName: "main._private",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := isExportedSymbol(tt.symName)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isStdlibPackage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pkgPath string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "fmt",
|
||||
pkgPath: "fmt",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "net/http",
|
||||
pkgPath: "net/http",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "crypto/sha256",
|
||||
pkgPath: "crypto/sha256",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "main",
|
||||
pkgPath: "main",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "runtime",
|
||||
pkgPath: "runtime",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "github.com third-party",
|
||||
pkgPath: "github.com/foo/bar",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "golang.org/x extended stdlib",
|
||||
pkgPath: "golang.org/x/net",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "gopkg.in third-party",
|
||||
pkgPath: "gopkg.in/yaml.v3",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
pkgPath: "",
|
||||
want: true, // no dots means stdlib by our heuristic
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := isStdlibPackage(tt.pkgPath)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_createGoSymbolFilter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg SymbolConfig
|
||||
symName string
|
||||
symType string
|
||||
wantName string
|
||||
keep bool
|
||||
}{
|
||||
// NM type filtering
|
||||
{
|
||||
name: "valid NM type with defaults",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
StandardLibrary: true,
|
||||
},
|
||||
},
|
||||
symName: "fmt.Println",
|
||||
symType: "T",
|
||||
wantName: "fmt.Println",
|
||||
keep: true,
|
||||
},
|
||||
{
|
||||
name: "invalid NM type with defaults",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
StandardLibrary: true,
|
||||
},
|
||||
},
|
||||
symName: "fmt.Println",
|
||||
symType: "X", // important!
|
||||
wantName: "",
|
||||
keep: false,
|
||||
},
|
||||
{
|
||||
name: "custom NM types - included",
|
||||
cfg: SymbolConfig{
|
||||
Types: []string{"T"},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
StandardLibrary: true,
|
||||
},
|
||||
},
|
||||
symName: "fmt.Println",
|
||||
symType: "T",
|
||||
wantName: "fmt.Println",
|
||||
keep: true,
|
||||
},
|
||||
{
|
||||
name: "custom NM types - excluded",
|
||||
cfg: SymbolConfig{
|
||||
Types: []string{"T"},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
StandardLibrary: true,
|
||||
},
|
||||
},
|
||||
symName: "fmt.Println",
|
||||
symType: "t",
|
||||
wantName: "",
|
||||
keep: false,
|
||||
},
|
||||
|
||||
// floating point literal filtering
|
||||
{
|
||||
name: "floating point literal filtered",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
StandardLibrary: true,
|
||||
},
|
||||
},
|
||||
symName: "$f64.3fceb851eb851eb8",
|
||||
symType: "R",
|
||||
wantName: "",
|
||||
keep: false,
|
||||
},
|
||||
|
||||
// export status filtering
|
||||
{
|
||||
name: "exported symbol with only exported enabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: false,
|
||||
StandardLibrary: true,
|
||||
},
|
||||
},
|
||||
symName: "fmt.Println",
|
||||
symType: "T",
|
||||
wantName: "fmt.Println",
|
||||
keep: true,
|
||||
},
|
||||
{
|
||||
name: "unexported symbol with only exported enabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: false,
|
||||
StandardLibrary: true,
|
||||
},
|
||||
},
|
||||
symName: "fmt.println",
|
||||
symType: "T",
|
||||
wantName: "",
|
||||
keep: false,
|
||||
},
|
||||
|
||||
// type equality functions
|
||||
{
|
||||
name: "type equality function - enabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
TypeEqualityFunctions: true,
|
||||
},
|
||||
},
|
||||
symName: "type:.eq.myStruct",
|
||||
symType: "T",
|
||||
wantName: "type:.eq.myStruct",
|
||||
keep: true,
|
||||
},
|
||||
{
|
||||
name: "type equality function - disabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
TypeEqualityFunctions: false,
|
||||
},
|
||||
},
|
||||
symName: "type:.eq.myStruct",
|
||||
symType: "T",
|
||||
wantName: "",
|
||||
keep: false,
|
||||
},
|
||||
|
||||
// GC shape stencils
|
||||
{
|
||||
name: "gc shape stencil - enabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
GCShapeStencils: true,
|
||||
},
|
||||
},
|
||||
symName: "go.shape.func()",
|
||||
symType: "T",
|
||||
wantName: "go.shape.func()",
|
||||
keep: true,
|
||||
},
|
||||
{
|
||||
name: "gc shape stencil - disabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
GCShapeStencils: false,
|
||||
},
|
||||
},
|
||||
symName: "go.shape.func()",
|
||||
symType: "T",
|
||||
wantName: "",
|
||||
keep: false,
|
||||
},
|
||||
{
|
||||
name: "gc shape stencil embedded in generic - enabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
GCShapeStencils: true,
|
||||
},
|
||||
},
|
||||
symName: "slices.partitionCmpFunc[go.shape.struct { Key string; Value int }]",
|
||||
symType: "T",
|
||||
wantName: "slices.partitionCmpFunc[go.shape.struct { Key string; Value int }]",
|
||||
keep: true,
|
||||
},
|
||||
{
|
||||
name: "gc shape stencil embedded in generic - disabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
GCShapeStencils: false,
|
||||
},
|
||||
},
|
||||
symName: "slices.partitionCmpFunc[go.shape.struct { Key string; Value int }]",
|
||||
symType: "T",
|
||||
wantName: "",
|
||||
keep: false,
|
||||
},
|
||||
|
||||
// vendored module normalization
|
||||
{
|
||||
name: "vendored path - normalization enabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
ThirdPartyModules: true,
|
||||
NormalizeVendoredModules: true,
|
||||
},
|
||||
},
|
||||
symName: "vendor/github.com/foo/bar.Baz",
|
||||
symType: "T",
|
||||
wantName: "github.com/foo/bar.Baz",
|
||||
keep: true,
|
||||
},
|
||||
{
|
||||
name: "vendored path - normalization disabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
ThirdPartyModules: true,
|
||||
NormalizeVendoredModules: false,
|
||||
},
|
||||
},
|
||||
symName: "vendor/github.com/foo/bar.Baz",
|
||||
symType: "T",
|
||||
wantName: "vendor/github.com/foo/bar.Baz",
|
||||
keep: true,
|
||||
},
|
||||
|
||||
// extended stdlib
|
||||
{
|
||||
name: "extended stdlib - enabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
ExtendedStandardLibrary: true,
|
||||
},
|
||||
},
|
||||
symName: "golang.org/x/net/html.Parse",
|
||||
symType: "T",
|
||||
wantName: "golang.org/x/net/html.Parse",
|
||||
keep: true,
|
||||
},
|
||||
{
|
||||
name: "extended stdlib - disabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
ExtendedStandardLibrary: false,
|
||||
},
|
||||
},
|
||||
symName: "golang.org/x/net/html.Parse",
|
||||
symType: "T",
|
||||
wantName: "",
|
||||
keep: false,
|
||||
},
|
||||
|
||||
// stdlib
|
||||
{
|
||||
name: "stdlib - enabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
StandardLibrary: true,
|
||||
},
|
||||
},
|
||||
symName: "fmt.Println",
|
||||
symType: "T",
|
||||
wantName: "fmt.Println",
|
||||
keep: true,
|
||||
},
|
||||
{
|
||||
name: "stdlib - disabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
StandardLibrary: false,
|
||||
},
|
||||
},
|
||||
symName: "fmt.Println",
|
||||
symType: "T",
|
||||
wantName: "",
|
||||
keep: false,
|
||||
},
|
||||
{
|
||||
name: "nested stdlib - enabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
StandardLibrary: true,
|
||||
},
|
||||
},
|
||||
symName: "net/http.ListenAndServe",
|
||||
symType: "T",
|
||||
wantName: "net/http.ListenAndServe",
|
||||
keep: true,
|
||||
},
|
||||
|
||||
// third party
|
||||
{
|
||||
name: "third party - enabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
ThirdPartyModules: true,
|
||||
},
|
||||
},
|
||||
symName: "github.com/spf13/cobra.Command",
|
||||
symType: "T",
|
||||
wantName: "github.com/spf13/cobra.Command",
|
||||
keep: true,
|
||||
},
|
||||
{
|
||||
name: "third party - disabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
ThirdPartyModules: false,
|
||||
},
|
||||
},
|
||||
symName: "github.com/spf13/cobra.Command",
|
||||
symType: "T",
|
||||
wantName: "",
|
||||
keep: false,
|
||||
},
|
||||
|
||||
// main package (treated as stdlib)
|
||||
{
|
||||
name: "main package - stdlib enabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
StandardLibrary: true,
|
||||
},
|
||||
},
|
||||
symName: "main.main",
|
||||
symType: "T",
|
||||
wantName: "main.main",
|
||||
keep: true,
|
||||
},
|
||||
{
|
||||
name: "main package - stdlib disabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
StandardLibrary: false,
|
||||
},
|
||||
},
|
||||
symName: "main.main",
|
||||
symType: "T",
|
||||
wantName: "",
|
||||
keep: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
filter := createGoSymbolFilter(tt.cfg)
|
||||
require.NotNil(t, filter)
|
||||
|
||||
gotName, gotKeep := filter(tt.symName, tt.symType)
|
||||
assert.Equal(t, tt.keep, gotKeep)
|
||||
if gotKeep {
|
||||
assert.Equal(t, tt.wantName, gotName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,40 +3,163 @@ package executable
|
||||
import (
|
||||
"debug/macho"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/internal/unionreader"
|
||||
)
|
||||
|
||||
// source http://www.cilinder.be/docs/next/NeXTStep/3.3/nd/DevTools/14_MachO/MachO.htmld/index.html
|
||||
const (
|
||||
machoNPExt uint8 = 0x10 /* N_PEXT: private external symbol bit */
|
||||
machoNExt uint8 = 0x01 /* N_EXT: external symbol bit, set for external symbols */
|
||||
machoNStab uint8 = 0xe0 // N_STAB mask for debugging symbols
|
||||
machoNPExt uint8 = 0x10 // N_PEXT: private external symbol bit
|
||||
machoNType uint8 = 0x0e // N_TYPE mask for symbol type
|
||||
machoNExt uint8 = 0x01 // N_EXT: external symbol bit
|
||||
|
||||
// N_TYPE values (after masking with 0x0e)
|
||||
machoNUndf uint8 = 0x00 // undefined symbol
|
||||
machoNAbs uint8 = 0x02 // absolute symbol
|
||||
machoNSect uint8 = 0x0e // defined in section
|
||||
machoNPbud uint8 = 0x0c // prebound undefined
|
||||
machoNIndr uint8 = 0x0a // indirect symbol
|
||||
|
||||
// > #define LC_REQ_DYLD 0x80000000
|
||||
// > #define LC_MAIN (0x28|LC_REQ_DYLD) /* replacement for LC_UNIXTHREAD */
|
||||
lcMain = 0x28 | 0x80000000
|
||||
)
|
||||
|
||||
func findMachoFeatures(data *file.Executable, reader unionreader.UnionReader) error {
|
||||
func findMachoFeatures(data *file.Executable, reader unionreader.UnionReader, cfg SymbolConfig) error {
|
||||
// TODO: support security features
|
||||
|
||||
// TODO: support multi-architecture binaries
|
||||
f, err := macho.NewFile(reader)
|
||||
// a universal binary may have multiple architectures, so we need to check each one
|
||||
readers, err := unionreader.GetReaders(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
libs, err := f.ImportedLibraries()
|
||||
if err != nil {
|
||||
return err
|
||||
var libs, symbols []string
|
||||
for _, r := range readers {
|
||||
f, err := macho.NewFile(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rLibs, err := f.ImportedLibraries()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
libs = append(libs, rLibs...)
|
||||
|
||||
// TODO handle only some having entrypoints/exports? If that is even practical
|
||||
// only check for entrypoint if we don't already have one
|
||||
if !data.HasEntrypoint {
|
||||
data.HasEntrypoint = machoHasEntrypoint(f)
|
||||
}
|
||||
// only check for exports if we don't already have them
|
||||
if !data.HasExports {
|
||||
data.HasExports = machoHasExports(f)
|
||||
}
|
||||
|
||||
data.Toolchains = machoToolchains(reader)
|
||||
if shouldCaptureSymbols(data, cfg) {
|
||||
symbols = machoNMSymbols(f, cfg, data.Toolchains)
|
||||
}
|
||||
}
|
||||
|
||||
data.ImportedLibraries = libs
|
||||
data.HasEntrypoint = machoHasEntrypoint(f)
|
||||
data.HasExports = machoHasExports(f)
|
||||
// de-duplicate libraries andn symbols
|
||||
data.ImportedLibraries = internal.NewSet(libs...).ToSlice()
|
||||
data.SymbolNames = internal.NewSet(symbols...).ToSlice()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func machoToolchains(reader unionreader.UnionReader) []file.Toolchain {
|
||||
return includeNoneNil(
|
||||
golangToolchainEvidence(reader),
|
||||
)
|
||||
}
|
||||
|
||||
func machoNMSymbols(f *macho.File, cfg SymbolConfig, toolchains []file.Toolchain) []string {
|
||||
if isGoToolchainPresent(toolchains) {
|
||||
return captureMachoGoSymbols(f, cfg)
|
||||
}
|
||||
|
||||
// include all symbols
|
||||
var symbols []string
|
||||
for _, sym := range f.Symtab.Syms {
|
||||
symbols = append(symbols, sym.Name)
|
||||
}
|
||||
return symbols
|
||||
}
|
||||
|
||||
func captureMachoGoSymbols(f *macho.File, cfg SymbolConfig) []string {
|
||||
var symbols []string
|
||||
filter := createGoSymbolFilter(cfg)
|
||||
for _, sym := range f.Symtab.Syms {
|
||||
name, include := filter(sym.Name, machoSymbolType(sym, f.Sections))
|
||||
if include {
|
||||
symbols = append(symbols, name)
|
||||
}
|
||||
}
|
||||
return symbols
|
||||
}
|
||||
|
||||
func machoSymbolType(s macho.Symbol, sections []*macho.Section) string {
|
||||
// stab (debugging) symbols get '-'
|
||||
if s.Type&machoNStab != 0 {
|
||||
return "-"
|
||||
}
|
||||
|
||||
isExternal := s.Type&machoNExt != 0
|
||||
symType := s.Type & machoNType
|
||||
|
||||
var typeChar byte
|
||||
switch symType {
|
||||
case machoNUndf, machoNPbud:
|
||||
typeChar = 'U'
|
||||
case machoNAbs:
|
||||
typeChar = 'A'
|
||||
case machoNSect:
|
||||
typeChar = machoSectionTypeChar(s.Sect, sections)
|
||||
case machoNIndr:
|
||||
typeChar = 'I'
|
||||
default:
|
||||
typeChar = '?'
|
||||
}
|
||||
|
||||
// lowercase for local symbols, uppercase for external
|
||||
if !isExternal && typeChar != '-' && typeChar != '?' {
|
||||
typeChar += 32 // convert to lowercase
|
||||
}
|
||||
|
||||
return string(typeChar)
|
||||
}
|
||||
|
||||
// machoSectionTypeChar returns the nm-style character for a section-defined symbol.
|
||||
// Section numbers are 1-based; 0 means NO_SECT.
|
||||
func machoSectionTypeChar(sect uint8, sections []*macho.Section) byte {
|
||||
if sect == 0 || int(sect) > len(sections) {
|
||||
return 'S'
|
||||
}
|
||||
|
||||
section := sections[sect-1]
|
||||
seg := section.Seg
|
||||
|
||||
// match nm behavior based on segment and section names
|
||||
switch seg {
|
||||
case "__TEXT":
|
||||
return 'T'
|
||||
case "__DATA", "__DATA_CONST":
|
||||
switch section.Name {
|
||||
case "__bss", "__common":
|
||||
return 'B'
|
||||
default:
|
||||
return 'D'
|
||||
}
|
||||
default:
|
||||
return 'S'
|
||||
}
|
||||
}
|
||||
|
||||
func machoHasEntrypoint(f *macho.File) bool {
|
||||
// derived from struct entry_point_command found from which explicitly calls out LC_MAIN:
|
||||
// https://opensource.apple.com/source/xnu/xnu-2050.18.24/EXTERNAL_HEADERS/mach-o/loader.h
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user