-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
deps: remove fanal framework, call Trivy parsers directly #2476
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
42 commits
Select commit
Hold shift + click to select a range
8e9ce75
test: add golden tests for AnalyzeLibrary
kotakanbe f7d7408
refactor: extract file-to-parser dispatch function
kotakanbe 1509894
deps: remove fanal framework, call Trivy parsers directly
kotakanbe a536e40
style: fix goimports formatting and go mod tidy
kotakanbe 0962eb9
test: address Copilot review on golden tests
kotakanbe 4ebd1f2
test: copy text fixtures to testdata for CI compatibility
kotakanbe c7260d0
test: force-add requirements.txt ignored by *.txt gitignore
kotakanbe afed2c9
fix: address Copilot review on PR #2476
kotakanbe f9cc28c
fix: address Copilot review round 2 on PR #2476
kotakanbe b1c4c88
fix: address Copilot review round 3
kotakanbe 3ac622b
test: add diff-lockfile for A/B regression testing
kotakanbe 13329ff
chore: gitignore compare-lockfile binary
kotakanbe 4a56ec1
docs: add scripts/README.md for diff-lockfile usage
kotakanbe c954ef7
fix: address Copilot review round 4 on PR #2476
kotakanbe 8885a19
fix: update fixtures to non-zero libs and fix docs
kotakanbe 2a7187f
fix: surface all filesystem errors in compare-lockfile
kotakanbe 86550f6
fix: add complete sort tie-breakers and harden error handling
kotakanbe 2ebd3e3
fix: sort imports per gofmt and sanitize worktree path
kotakanbe 60207aa
fix: use MkdirTemp for worktree and handle result-counting errors
kotakanbe 7bedc67
fix: check os.Remove and worktree cleanup errors
kotakanbe 2e1001e
fix: use abspath instead of trivypath in error messages
kotakanbe 7ada7fc
docs: use workdir-relative path in README instead of hardcoded /tmp
kotakanbe cf9810c
fix: address remaining code quality issues
kotakanbe 4d70b7d
fix: fix result map key and add diff fallback
kotakanbe 574ec26
fix: comprehensive code quality sweep
kotakanbe 82d7e60
fix: add ComposerVendor dispatch and complete test coverage
kotakanbe 504fa70
fix: add ComposerVendor dispatch, fix lint, use request context
kotakanbe 77b85d8
fix: make diff-lockfile fetch optional via FETCH variable
kotakanbe 364d87f
docs: fix README wording to match actual script behavior
kotakanbe 27fa2c7
fix: detect .exe and exclude non-regular files in isExecutable
kotakanbe be81b3e
fix: address review feedback (rename, typo, comments, unused arg)
kotakanbe 8ba7439
docs: add security rationale for not using submodules in CI
kotakanbe 3f3890e
test: expand diff-lockfile to 129 fixtures, remove unused jar method
kotakanbe 81d07f1
chore: add /compare-lockfile binary to .gitignore
kotakanbe 8f58ddf
style: fix goimports alignment in dispatch_test.go
kotakanbe 8661bd3
fix: check IsRegular before .exe in isExecutable
kotakanbe 03dfcb6
docs: update fixture count in scripts/README.md (17 → 129)
kotakanbe 204fb48
test: compare lib count instead of byte length in pom online test
kotakanbe 3c8a234
fix: unmarshal golden JSON as slice in PomOnline test
kotakanbe ce2bcdf
Merge branch 'master' into diet-trivy-dispatch
kotakanbe 52c78de
fix: address MaineK00n review feedback
claude a426e58
Merge branch 'master' into diet-trivy-dispatch
kotakanbe File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,3 +23,5 @@ vuls | |
| /trivy-to-vuls | ||
| snmp2cpe | ||
| !snmp2cpe/ | ||
| /scripts/compare-lockfile | ||
| /compare-lockfile | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,317 @@ | ||
| package scanner | ||
|
|
||
| import ( | ||
| "cmp" | ||
| "context" | ||
| "encoding/json" | ||
| "flag" | ||
| "os" | ||
| "path/filepath" | ||
| "slices" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/future-architect/vuls/models" | ||
| ) | ||
|
|
||
| var update = flag.Bool("update", false, "update golden files") | ||
|
|
||
| // lockfileEntry defines a test fixture for AnalyzeLibrary golden testing. | ||
| type lockfileEntry struct { | ||
| // path is the relative path from the fixtures directory. | ||
| path string | ||
| // filemode to pass to AnalyzeLibrary (0755 for executables, 0644 otherwise). | ||
| filemode os.FileMode | ||
| // binary indicates the fixture is a binary file only available in the | ||
| // integration submodule (not copied to testdata/fixtures/). | ||
| binary bool | ||
| // expectParseError indicates this fixture is known to produce a parse error | ||
| // (e.g. unsupported lockfile version). The test treats errors as empty result. | ||
| expectParseError bool | ||
| } | ||
|
|
||
| var lockfiles = []lockfileEntry{ | ||
| // Node.js | ||
| {"npm-v1/package-lock.json", 0644, false, false}, | ||
| {"npm-v2/package-lock.json", 0644, false, false}, | ||
| {"npm-v3/package-lock.json", 0644, false, false}, | ||
| {"yarn.lock", 0644, false, false}, | ||
| {"pnpm/pnpm-lock.yaml", 0644, false, true}, // pnpm v8: known parse error | ||
| {"pnpm-v9/pnpm-lock.yaml", 0644, false, false}, | ||
| {"bun.lock", 0644, false, false}, | ||
|
|
||
| // Python | ||
| {"requirements.txt", 0644, false, false}, | ||
| {"Pipfile.lock", 0644, false, false}, | ||
| {"poetry-v1/poetry.lock", 0644, false, false}, | ||
| {"poetry-v2/poetry.lock", 0644, false, false}, | ||
| {"uv.lock", 0644, false, false}, | ||
|
|
||
| // Ruby | ||
| {"Gemfile.lock", 0644, false, false}, | ||
|
|
||
| // Rust | ||
| {"Cargo.lock", 0644, false, false}, | ||
| {"hello-rust", 0755, true, false}, | ||
|
|
||
| // PHP | ||
| {"composer.lock", 0644, false, false}, | ||
| {"installed.json", 0644, false, false}, | ||
|
|
||
| // Go | ||
| {"go.mod", 0644, false, false}, | ||
| {"go.sum", 0644, false, false}, | ||
| {"gobinary", 0755, true, false}, | ||
|
|
||
| // Java | ||
| {"pom.xml", 0644, false, false}, | ||
| {"gradle.lockfile", 0644, false, false}, | ||
| {"log4j-core-2.13.0.jar", 0644, true, false}, | ||
| {"wrong-name-log4j-core.jar", 0644, true, false}, | ||
| {"juddiv3-war-3.3.5.war", 0644, true, false}, | ||
|
|
||
| // .NET | ||
| {"packages.lock.json", 0644, false, false}, | ||
| {"packages.config", 0644, false, false}, | ||
| {"datacollector.deps.json", 0644, false, false}, | ||
| {"Directory.Packages.props", 0644, false, false}, | ||
|
|
||
| // C/C++ | ||
| {"conan-v1/conan.lock", 0644, false, false}, | ||
| {"conan-v2/conan.lock", 0644, false, false}, | ||
|
|
||
| // Dart | ||
| {"pubspec.lock", 0644, false, false}, | ||
|
|
||
| // Elixir | ||
| {"mix.lock", 0644, false, false}, | ||
|
|
||
| // Swift | ||
| {"Podfile.lock", 0644, false, false}, | ||
| {"Package.resolved", 0644, false, false}, | ||
| } | ||
|
|
||
| // goldenFileName converts a lockfile path to a golden file name. | ||
| // e.g. "npm-v3/package-lock.json" -> "npm-v3_package-lock.json" | ||
| // Uses filepath.ToSlash to normalize path separators across platforms. | ||
| func goldenFileName(lockfilePath string) string { | ||
| return strings.ReplaceAll(filepath.ToSlash(lockfilePath), "/", "_") + ".json" | ||
| } | ||
|
|
||
| func TestAnalyzeLibrary_Golden(t *testing.T) { | ||
| fixturesDir := filepath.Join("testdata", "fixtures") | ||
| integrationDir := filepath.Join("..", "integration", "data", "lockfile") | ||
| goldenDir := filepath.Join("testdata", "golden") | ||
|
|
||
| for _, lf := range lockfiles { | ||
| t.Run(lf.path, func(t *testing.T) { | ||
| // Test fixtures are in testdata/fixtures/ (committed to repo). | ||
| // Binary fixtures (JAR, WAR, Go/Rust binaries) are only in the | ||
| // integration submodule — skip if not available. | ||
| // NOTE: We intentionally do NOT add submodules: true to CI checkout. | ||
| // Attack scenario: an attacker forks this repo, edits .gitmodules to | ||
| // replace the integration submodule URL with their own repo containing | ||
| // a malicious go.mod or _test.go, then opens a PR. If CI checks out | ||
| // submodules, `go test` executes attacker-controlled code with access | ||
| // to the CI environment (secrets, GITHUB_TOKEN, network). | ||
| // Binary fixture tests therefore run locally only. | ||
| srcPath := filepath.Join(fixturesDir, lf.path) | ||
| if lf.binary { | ||
| srcPath = filepath.Join(integrationDir, lf.path) | ||
| } | ||
| contents, err := os.ReadFile(srcPath) | ||
| if err != nil { | ||
| if lf.binary { | ||
| t.Skipf("Binary fixture not found: %s (requires: git submodule update --init)", srcPath) | ||
| } | ||
| t.Fatalf("Failed to read %s: %v", srcPath, err) | ||
kotakanbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
kotakanbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| got, err := AnalyzeLibrary(context.Background(), lf.path, contents, lf.filemode, true) | ||
| if err != nil { | ||
| if lf.expectParseError { | ||
| // Verify the error is actually a parse error (contains "parse error" or the parser type) | ||
| errMsg := err.Error() | ||
| if !strings.Contains(errMsg, "parse error") && !strings.Contains(errMsg, "Failed to parse") { | ||
| t.Fatalf("AnalyzeLibrary(%s) expected parse error but got: %v", lf.path, err) | ||
| } | ||
| t.Logf("AnalyzeLibrary(%s) returned expected parse error: %v", lf.path, err) | ||
| got = nil | ||
| } else { | ||
| t.Fatalf("AnalyzeLibrary(%s) unexpected error: %v", lf.path, err) | ||
| } | ||
| } | ||
|
|
||
| gotJSON, err := json.MarshalIndent(normalizeResult(got), "", " ") | ||
| if err != nil { | ||
| t.Fatalf("Failed to marshal result: %v", err) | ||
| } | ||
|
|
||
| goldenPath := filepath.Join(goldenDir, goldenFileName(lf.path)) | ||
|
|
||
| if *update { | ||
| if err := os.MkdirAll(goldenDir, 0755); err != nil { | ||
| t.Fatalf("Failed to create golden dir: %v", err) | ||
| } | ||
| if err := os.WriteFile(goldenPath, gotJSON, 0644); err != nil { | ||
| t.Fatalf("Failed to write golden file: %v", err) | ||
| } | ||
| t.Logf("Updated golden file: %s", goldenPath) | ||
| return | ||
| } | ||
|
|
||
| wantJSON, err := os.ReadFile(goldenPath) | ||
| if err != nil { | ||
| t.Fatalf("Golden file not found: %s (run with -update to generate)", goldenPath) | ||
| } | ||
|
|
||
| if string(gotJSON) != string(wantJSON) { | ||
| t.Errorf("AnalyzeLibrary(%s) output differs from golden file.\nGot:\n%s\nWant:\n%s", | ||
| lf.path, string(gotJSON), string(wantJSON)) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // TestAnalyzeLibrary_PomOnline verifies that pom.xml parsing in online mode | ||
| // (resolving transitive dependencies from Maven Central) works correctly. | ||
| // Skipped with -short since it requires network access. | ||
| func TestAnalyzeLibrary_PomOnline(t *testing.T) { | ||
| if testing.Short() { | ||
| t.Skip("skipping online pom.xml test (requires network access)") | ||
| } | ||
|
|
||
| fixturesDir := filepath.Join("testdata", "fixtures") | ||
| goldenDir := filepath.Join("testdata", "golden") | ||
|
|
||
| contents, err := os.ReadFile(filepath.Join(fixturesDir, "pom.xml")) | ||
| if err != nil { | ||
| t.Fatalf("Failed to read pom.xml: %v", err) | ||
| } | ||
|
|
||
| got, err := AnalyzeLibrary(context.Background(), "pom.xml", contents, 0644, false) | ||
| if err != nil { | ||
| t.Fatalf("AnalyzeLibrary(pom.xml, online) unexpected error: %v", err) | ||
| } | ||
|
|
||
| gotJSON, err := json.MarshalIndent(normalizeResult(got), "", " ") | ||
| if err != nil { | ||
| t.Fatalf("Failed to marshal result: %v", err) | ||
| } | ||
|
|
||
| goldenPath := filepath.Join(goldenDir, "pom.xml.online.json") | ||
|
|
||
| if *update { | ||
| if err := os.MkdirAll(goldenDir, 0755); err != nil { | ||
| t.Fatalf("Failed to create golden dir: %v", err) | ||
| } | ||
| if err := os.WriteFile(goldenPath, gotJSON, 0644); err != nil { | ||
| t.Fatalf("Failed to write golden file: %v", err) | ||
| } | ||
| t.Logf("Updated golden file: %s", goldenPath) | ||
| return | ||
| } | ||
|
|
||
| wantJSON, err := os.ReadFile(goldenPath) | ||
| if err != nil { | ||
| t.Fatalf("Golden file not found: %s (run with -update to generate)", goldenPath) | ||
| } | ||
|
|
||
| if string(gotJSON) != string(wantJSON) { | ||
| t.Errorf("AnalyzeLibrary(pom.xml, online) output differs from golden file.\nGot:\n%s\nWant:\n%s", | ||
| string(gotJSON), string(wantJSON)) | ||
| } | ||
|
|
||
| // Online mode should resolve transitive dependencies, producing more results than offline. | ||
| offlineGoldenPath := filepath.Join(goldenDir, "pom.xml.json") | ||
| offlineJSON, err := os.ReadFile(offlineGoldenPath) | ||
| if err != nil { | ||
| t.Logf("Offline golden file not found, skipping comparison: %s", offlineGoldenPath) | ||
| return | ||
| } | ||
|
|
||
| var onlineRes []goldenLibraryScanner | ||
| if err := json.Unmarshal(gotJSON, &onlineRes); err != nil { | ||
| t.Fatalf("Failed to unmarshal online JSON result: %v", err) | ||
| } | ||
| var offlineRes []goldenLibraryScanner | ||
| if err := json.Unmarshal(offlineJSON, &offlineRes); err != nil { | ||
| t.Fatalf("Failed to unmarshal offline golden JSON: %v", err) | ||
| } | ||
| var onlineLibs, offlineLibs int | ||
| for _, s := range onlineRes { | ||
| onlineLibs += len(s.Libs) | ||
| } | ||
| for _, s := range offlineRes { | ||
| offlineLibs += len(s.Libs) | ||
| } | ||
| if onlineLibs <= offlineLibs { | ||
| t.Errorf("Online mode should resolve more dependencies than offline mode.\nOnline libs: %d\nOffline libs: %d", | ||
| onlineLibs, offlineLibs) | ||
| } | ||
| } | ||
|
|
||
| // normalizeResult produces a stable, comparable representation of the scan result. | ||
| // It sorts libraries by name+version to avoid ordering-dependent diffs. | ||
| type goldenLibraryScanner struct { | ||
| Type string `json:"type"` | ||
| LockfilePath string `json:"lockfilePath"` | ||
| Libs []goldenLibrary `json:"libs"` | ||
| } | ||
|
|
||
| type goldenLibrary struct { | ||
| Name string `json:"name"` | ||
| Version string `json:"version"` | ||
| PURL string `json:"purl,omitempty"` | ||
| FilePath string `json:"filePath,omitempty"` | ||
| Digest string `json:"digest,omitempty"` | ||
| Dev bool `json:"dev,omitempty"` | ||
| } | ||
|
|
||
| func normalizeResult(scanners []models.LibraryScanner) []goldenLibraryScanner { | ||
| result := make([]goldenLibraryScanner, 0, len(scanners)) | ||
| for _, s := range scanners { | ||
| gs := goldenLibraryScanner{ | ||
| Type: string(s.Type), | ||
| LockfilePath: s.LockfilePath, | ||
| Libs: make([]goldenLibrary, 0, len(s.Libs)), | ||
| } | ||
| for _, lib := range s.Libs { | ||
| gs.Libs = append(gs.Libs, goldenLibrary{ | ||
| Name: lib.Name, | ||
| Version: lib.Version, | ||
| PURL: lib.PURL, | ||
| FilePath: lib.FilePath, | ||
| Digest: lib.Digest, | ||
| Dev: lib.Dev, | ||
| }) | ||
| } | ||
| slices.SortFunc(gs.Libs, func(a, b goldenLibrary) int { | ||
| return cmp.Or( | ||
| cmp.Compare(a.Name, b.Name), | ||
| cmp.Compare(a.Version, b.Version), | ||
| cmp.Compare(a.PURL, b.PURL), | ||
| cmp.Compare(a.FilePath, b.FilePath), | ||
| cmp.Compare(a.Digest, b.Digest), | ||
| func() int { | ||
| switch { | ||
| case !a.Dev && b.Dev: | ||
| return -1 | ||
| case a.Dev && !b.Dev: | ||
| return +1 | ||
| default: | ||
| return 0 | ||
| } | ||
| }(), | ||
| ) | ||
| }) | ||
| result = append(result, gs) | ||
| } | ||
| slices.SortFunc(result, func(a, b goldenLibraryScanner) int { | ||
| return cmp.Or( | ||
| cmp.Compare(a.Type, b.Type), | ||
| cmp.Compare(a.LockfilePath, b.LockfilePath), | ||
| ) | ||
| }) | ||
| return result | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.