Skip to content

Commit f4b2cf1

Browse files
feat(repo): add git repository metadata to reports (#9252)
Co-authored-by: knqyf263 <[email protected]> Co-authored-by: DmitriyLewen <[email protected]>
1 parent b4193d0 commit f4b2cf1

File tree

8 files changed

+158
-29
lines changed

8 files changed

+158
-29
lines changed

docs/docs/target/repository.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,26 @@ $ trivy repo --scanners license (REPO_PATH | REPO_URL)
109109
Trivy can generate SBOM for code repositories.
110110
See [here](../supply-chain/sbom.md) for the detail.
111111

112+
## Git Metadata
113+
When scanning git repositories (both local and remote), Trivy automatically extracts and includes git metadata in the scan results.
114+
This metadata provides context about the scanned repository.
115+
116+
The metadata includes information such as:
117+
118+
- Repository URL
119+
- Branch name
120+
- Tags
121+
- Commit details (hash, message, commiter)
122+
- Author information
123+
124+
This feature works automatically for any git repository.
125+
When using JSON format output, the git metadata will be included in the `Metadata` field.
126+
For detailed information about the available fields, please refer to the JSON output of your scan results.
127+
128+
```bash
129+
$ trivy repo --format json <repo-name>
130+
```
131+
112132
## Scan Cache
113133
When scanning git repositories, it stores analysis results in the cache, using the latest commit hash as the key.
114134
Note that the cache is not used when the repository is dirty, otherwise Trivy will miss the files that are not committed.

integration/repo_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,10 @@ func TestRepository(t *testing.T) {
313313
input: "testdata/fixtures/repo/trivy-ci-test",
314314
},
315315
golden: "testdata/test-repo.json.golden",
316+
override: func(_ *testing.T, want, _ *types.Report) {
317+
// Clear all metadata as this is a local directory scan without git info
318+
want.Metadata = types.Metadata{}
319+
},
316320
},
317321
{
318322
name: "installed.json",

integration/testdata/test-repo.json.golden

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@
1313
"diff_ids": null
1414
},
1515
"config": {}
16-
}
16+
},
17+
"RepoURL": "https://github.com/knqyf263/trivy-ci-test",
18+
"Branch": "master",
19+
"Commit": "5ae342eb2802672402d9b2c26f09e2051bbd91b8",
20+
"CommitMsg": "Use COPY instead of ADD in Dockerfile (#4)",
21+
"Author": "gy741 <[email protected]>",
22+
"Committer": "knqyf263 <[email protected]>"
1723
},
1824
"Results": [
1925
{

pkg/fanal/artifact/artifact.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ type Reference struct {
9292
ID string
9393
BlobIDs []string
9494
ImageMetadata ImageMetadata
95+
RepoMetadata RepoMetadata
9596

9697
// SBOM
9798
BOM *core.BOM
@@ -104,3 +105,13 @@ type ImageMetadata struct {
104105
RepoDigests []string
105106
ConfigFile v1.ConfigFile
106107
}
108+
109+
type RepoMetadata struct {
110+
RepoURL string // repository URL (from upstream/origin)
111+
Branch string // current branch name
112+
Tags []string // tag names pointing to HEAD
113+
Commit string // commit hash
114+
CommitMsg string // commit message
115+
Author string // commit author
116+
Committer string // commit committer
117+
}

pkg/fanal/artifact/local/fs.go

Lines changed: 71 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"sync"
1414

1515
"github.com/go-git/go-git/v5"
16+
"github.com/go-git/go-git/v5/plumbing"
1617
"github.com/google/wire"
1718
"github.com/samber/lo"
1819
"golang.org/x/xerrors"
@@ -56,7 +57,8 @@ type Artifact struct {
5657

5758
artifactOption artifact.Option
5859

59-
commitHash string // only set when the git repository is clean
60+
isClean bool // whether git repository is clean (for caching)
61+
repoMetadata artifact.RepoMetadata // git repository metadata
6062
}
6163

6264
func NewArtifact(rootPath string, c cache.ArtifactCache, w Walker, opt artifact.Option) (artifact.Artifact, error) {
@@ -86,10 +88,14 @@ func NewArtifact(rootPath string, c cache.ArtifactCache, w Walker, opt artifact.
8688
art.logger.Debug("Analyzing...", log.String("root", art.rootPath),
8789
lo.Ternary(opt.Original != "", log.String("original", opt.Original), log.Nil))
8890

89-
// Check if the directory is a git repository and clean
90-
if hash, err := gitCommitHash(art.rootPath); err == nil {
91-
art.logger.Debug("Using the latest commit hash for calculating cache key", log.String("commit_hash", hash))
92-
art.commitHash = hash
91+
// Check if the directory is a git repository and extract metadata
92+
if art.isClean, art.repoMetadata, err = extractGitInfo(art.rootPath); err == nil {
93+
if art.isClean {
94+
art.logger.Debug("Using the latest commit hash for calculating cache key",
95+
log.String("commit_hash", art.repoMetadata.Commit))
96+
} else {
97+
art.logger.Debug("Repository is dirty, random cache key will be used")
98+
}
9399
} else if !errors.Is(err, git.ErrRepositoryNotExists) {
94100
// Only log if the file path is a git repository
95101
art.logger.Debug("Random cache key will be used", log.Err(err))
@@ -98,36 +104,72 @@ func NewArtifact(rootPath string, c cache.ArtifactCache, w Walker, opt artifact.
98104
return art, nil
99105
}
100106

101-
// gitCommitHash returns the latest commit hash if the git repository is clean, otherwise returns an error
102-
func gitCommitHash(dir string) (string, error) {
107+
// extractGitInfo extracts git repository information including clean status and metadata
108+
// Returns clean status (for caching), metadata, and error
109+
func extractGitInfo(dir string) (bool, artifact.RepoMetadata, error) {
110+
var metadata artifact.RepoMetadata
111+
103112
repo, err := git.PlainOpen(dir)
104113
if err != nil {
105-
return "", xerrors.Errorf("failed to open git repository: %w", err)
114+
return false, metadata, xerrors.Errorf("failed to open git repository: %w", err)
106115
}
107116

108-
// Get the working tree
109-
worktree, err := repo.Worktree()
117+
// Get HEAD commit
118+
head, err := repo.Head()
110119
if err != nil {
111-
return "", xerrors.Errorf("failed to get worktree: %w", err)
120+
return false, metadata, xerrors.Errorf("failed to get HEAD: %w", err)
112121
}
113122

114-
// Get the current status
115-
status, err := worktree.Status()
123+
commit, err := repo.CommitObject(head.Hash())
116124
if err != nil {
117-
return "", xerrors.Errorf("failed to get status: %w", err)
125+
return false, metadata, xerrors.Errorf("failed to get commit object: %w", err)
118126
}
119127

120-
if !status.IsClean() {
121-
return "", xerrors.New("repository is dirty")
128+
// Extract basic commit metadata
129+
metadata.Commit = head.Hash().String()
130+
metadata.CommitMsg = strings.TrimSpace(commit.Message)
131+
metadata.Author = commit.Author.String()
132+
metadata.Committer = commit.Committer.String()
133+
134+
// Get branch name
135+
if head.Name().IsBranch() {
136+
metadata.Branch = head.Name().Short()
122137
}
123138

124-
// Get the HEAD commit hash
125-
head, err := repo.Head()
139+
// Get all tag names that point to HEAD
140+
if tags, err := repo.Tags(); err == nil {
141+
var headTags []string
142+
_ = tags.ForEach(func(tag *plumbing.Reference) error {
143+
if tag.Hash() == head.Hash() {
144+
headTags = append(headTags, tag.Name().Short())
145+
}
146+
return nil
147+
})
148+
metadata.Tags = headTags
149+
}
150+
151+
// Get repository URL - prefer upstream, fallback to origin
152+
remoteConfig, err := repo.Remote("upstream")
153+
if err != nil {
154+
remoteConfig, err = repo.Remote("origin")
155+
}
156+
if err == nil && len(remoteConfig.Config().URLs) > 0 {
157+
metadata.RepoURL = remoteConfig.Config().URLs[0]
158+
}
159+
160+
// Check if repository is clean for caching purposes
161+
worktree, err := repo.Worktree()
162+
if err != nil {
163+
return false, metadata, xerrors.Errorf("failed to get worktree: %w", err)
164+
}
165+
166+
status, err := worktree.Status()
126167
if err != nil {
127-
return "", xerrors.Errorf("failed to get HEAD: %w", err)
168+
return false, metadata, xerrors.Errorf("failed to get status: %w", err)
128169
}
129170

130-
return head.Hash().String(), nil
171+
// Return clean status and metadata
172+
return status.IsClean(), metadata, nil
131173
}
132174

133175
func (a Artifact) Inspect(ctx context.Context) (artifact.Reference, error) {
@@ -138,7 +180,7 @@ func (a Artifact) Inspect(ctx context.Context) (artifact.Reference, error) {
138180
}
139181

140182
// Check if the cache exists only when it's a clean git repository
141-
if a.commitHash != "" {
183+
if a.isClean && a.repoMetadata.Commit != "" {
142184
_, missingBlobs, err := a.cache.MissingBlobs(cacheKey, []string{cacheKey})
143185
if err != nil {
144186
return artifact.Reference{}, xerrors.Errorf("unable to get missing blob: %w", err)
@@ -231,10 +273,11 @@ func (a Artifact) Inspect(ctx context.Context) (artifact.Reference, error) {
231273
}
232274

233275
return artifact.Reference{
234-
Name: hostName,
235-
Type: a.artifactOption.Type,
236-
ID: cacheKey, // use a cache key as pseudo artifact ID
237-
BlobIDs: []string{cacheKey},
276+
Name: hostName,
277+
Type: a.artifactOption.Type,
278+
ID: cacheKey, // use a cache key as pseudo artifact ID
279+
BlobIDs: []string{cacheKey},
280+
RepoMetadata: a.repoMetadata,
238281
}, nil
239282
}
240283

@@ -295,16 +338,16 @@ func (a Artifact) analyzeWithTraversal(ctx context.Context, root, relativePath s
295338

296339
func (a Artifact) Clean(reference artifact.Reference) error {
297340
// Don't delete cache if it's a clean git repository
298-
if a.commitHash != "" {
341+
if a.isClean && a.repoMetadata.Commit != "" {
299342
return nil
300343
}
301344
return a.cache.DeleteBlobs(reference.BlobIDs)
302345
}
303346

304347
func (a Artifact) calcCacheKey() (string, error) {
305348
// If this is a clean git repository, use the commit hash as cache key
306-
if a.commitHash != "" {
307-
return cache.CalcKey(a.commitHash, artifactVersion, a.analyzer.AnalyzerVersions(), a.handlerManager.Versions(), a.artifactOption)
349+
if a.isClean && a.repoMetadata.Commit != "" {
350+
return cache.CalcKey(a.repoMetadata.Commit, artifactVersion, a.analyzer.AnalyzerVersions(), a.handlerManager.Versions(), a.artifactOption)
308351
}
309352

310353
// For non-git repositories or dirty git repositories, use UUID as cache key

pkg/fanal/artifact/repo/git_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,15 @@ func TestArtifact_Inspect(t *testing.T) {
185185
BlobIDs: []string{
186186
"sha256:dc7c6039424c9fce969d3c2972d261af442a33f13e7494464386dbe280612d4c", // Calculated from commit hash
187187
},
188+
RepoMetadata: artifact.RepoMetadata{
189+
RepoURL: ts.URL + "/test-repo.git",
190+
Branch: "main",
191+
Tags: []string{"v0.0.1"},
192+
Commit: "8a19b492a589955c3e70c6ad8efd1e4ec6ae0d35",
193+
CommitMsg: "Update README.md",
194+
Author: "Teppei Fukuda <[email protected]>",
195+
Committer: "GitHub <[email protected]>",
196+
},
188197
},
189198
wantBlobInfo: &types.BlobInfo{
190199
SchemaVersion: types.BlobJSONSchemaVersion,
@@ -200,6 +209,15 @@ func TestArtifact_Inspect(t *testing.T) {
200209
BlobIDs: []string{
201210
"sha256:dc7c6039424c9fce969d3c2972d261af442a33f13e7494464386dbe280612d4c", // Calculated from commit hash
202211
},
212+
RepoMetadata: artifact.RepoMetadata{
213+
RepoURL: "https://github.com/aquasecurity/trivy-test-repo/",
214+
Branch: "main",
215+
Tags: []string{"v0.0.1"},
216+
Commit: "8a19b492a589955c3e70c6ad8efd1e4ec6ae0d35",
217+
CommitMsg: "Update README.md",
218+
Author: "Teppei Fukuda <[email protected]>",
219+
Committer: "GitHub <[email protected]>",
220+
},
203221
},
204222
wantBlobInfo: &types.BlobInfo{
205223
SchemaVersion: types.BlobJSONSchemaVersion,
@@ -221,6 +239,15 @@ func TestArtifact_Inspect(t *testing.T) {
221239
BlobIDs: []string{
222240
"sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098",
223241
},
242+
RepoMetadata: artifact.RepoMetadata{
243+
RepoURL: "https://github.com/aquasecurity/trivy-test-repo/",
244+
Branch: "main",
245+
Tags: []string{"v0.0.1"},
246+
Commit: "8a19b492a589955c3e70c6ad8efd1e4ec6ae0d35",
247+
CommitMsg: "Update README.md",
248+
Author: "Teppei Fukuda <[email protected]>",
249+
Committer: "GitHub <[email protected]>",
250+
},
224251
},
225252
wantBlobInfo: &types.BlobInfo{
226253
SchemaVersion: types.BlobJSONSchemaVersion,

pkg/scan/service.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,15 @@ func (s Service) ScanArtifact(ctx context.Context, options types.ScanOptions) (t
207207
ImageConfig: artifactInfo.ImageMetadata.ConfigFile,
208208
Size: scanResponse.Layers.TotalSize(),
209209
Layers: lo.Ternary(len(scanResponse.Layers) > 0, scanResponse.Layers, nil),
210+
211+
// Git repository
212+
RepoURL: artifactInfo.RepoMetadata.RepoURL,
213+
Branch: artifactInfo.RepoMetadata.Branch,
214+
Tags: artifactInfo.RepoMetadata.Tags,
215+
Commit: artifactInfo.RepoMetadata.Commit,
216+
CommitMsg: artifactInfo.RepoMetadata.CommitMsg,
217+
Author: artifactInfo.RepoMetadata.Author,
218+
Committer: artifactInfo.RepoMetadata.Committer,
210219
},
211220
Results: scanResponse.Results,
212221
BOM: artifactInfo.BOM,

pkg/types/report.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ type Metadata struct {
3434
RepoDigests []string `json:",omitempty"`
3535
ImageConfig v1.ConfigFile `json:",omitzero"`
3636
Layers ftypes.Layers `json:",omitzero"`
37+
38+
// Git repository
39+
RepoURL string `json:",omitzero"`
40+
Branch string `json:",omitzero"`
41+
Tags []string `json:",omitzero"`
42+
Commit string `json:",omitzero"`
43+
CommitMsg string `json:",omitzero"`
44+
Author string `json:",omitzero"`
45+
Committer string `json:",omitzero"`
3746
}
3847

3948
// Results to hold list of Result

0 commit comments

Comments
 (0)