mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 15:50:15 -08:00
feat(rust): add root and workspace relationships/package for cargo lock files (#8676)
This commit is contained in:
@@ -294,9 +294,10 @@ This feature allows you to focus on vulnerabilities in specific types of depende
|
||||
In Trivy, there are four types of package relationships:
|
||||
|
||||
1. `root`: The root package being scanned
|
||||
2. `direct`: Direct dependencies of the root package
|
||||
3. `indirect`: Transitive dependencies
|
||||
4. `unknown`: Packages whose relationship cannot be determined
|
||||
2. `workspace`: Workspaces of the root package (Currently only `pom.xml` and `cargo.lock` files are supported)
|
||||
3. `direct`: Direct dependencies of the root/workspace package
|
||||
4. `indirect`: Transitive dependencies
|
||||
5. `unknown`: Packages whose relationship cannot be determined
|
||||
|
||||
The available relationships may vary depending on the ecosystem.
|
||||
To see which relationships are supported for a particular project, you can use the JSON output format and check the `Relationship` field:
|
||||
|
||||
@@ -286,6 +286,15 @@ func TestRepository(t *testing.T) {
|
||||
},
|
||||
golden: "testdata/composer.lock.json.golden",
|
||||
},
|
||||
{
|
||||
name: "cargo.lock",
|
||||
args: args{
|
||||
scanner: types.VulnerabilityScanner,
|
||||
listAllPkgs: true,
|
||||
input: "testdata/fixtures/repo/cargo",
|
||||
},
|
||||
golden: "testdata/cargo.lock.json.golden",
|
||||
},
|
||||
{
|
||||
name: "multiple lockfiles",
|
||||
args: args{
|
||||
|
||||
165
integration/testdata/cargo.lock.json.golden
vendored
Normal file
165
integration/testdata/cargo.lock.json.golden
vendored
Normal file
@@ -0,0 +1,165 @@
|
||||
{
|
||||
"SchemaVersion": 2,
|
||||
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
|
||||
"ArtifactName": "testdata/fixtures/repo/cargo",
|
||||
"ArtifactType": "repository",
|
||||
"Metadata": {
|
||||
"ImageConfig": {
|
||||
"architecture": "",
|
||||
"created": "0001-01-01T00:00:00Z",
|
||||
"os": "",
|
||||
"rootfs": {
|
||||
"type": "",
|
||||
"diff_ids": null
|
||||
},
|
||||
"config": {}
|
||||
}
|
||||
},
|
||||
"Results": [
|
||||
{
|
||||
"Target": "Cargo.lock",
|
||||
"Class": "lang-pkgs",
|
||||
"Type": "cargo",
|
||||
"Packages": [
|
||||
{
|
||||
"ID": "app@0.1.0",
|
||||
"Name": "app",
|
||||
"Identifier": {
|
||||
"PURL": "pkg:cargo/app@0.1.0",
|
||||
"UID": "a4ce1e2c46af5d56"
|
||||
},
|
||||
"Version": "0.1.0",
|
||||
"Relationship": "root",
|
||||
"DependsOn": [
|
||||
"memchr@1.0.2",
|
||||
"regex@1.7.3"
|
||||
],
|
||||
"Layer": {},
|
||||
"Locations": [
|
||||
{
|
||||
"StartLine": 14,
|
||||
"EndLine": 21
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": "memchr@1.0.2",
|
||||
"Name": "memchr",
|
||||
"Identifier": {
|
||||
"PURL": "pkg:cargo/memchr@1.0.2",
|
||||
"UID": "427a73f0e28dc7df"
|
||||
},
|
||||
"Version": "1.0.2",
|
||||
"Relationship": "direct",
|
||||
"DependsOn": [
|
||||
"libc@0.2.171"
|
||||
],
|
||||
"Layer": {},
|
||||
"Locations": [
|
||||
{
|
||||
"StartLine": 29,
|
||||
"EndLine": 36
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": "regex@1.7.3",
|
||||
"Name": "regex",
|
||||
"Identifier": {
|
||||
"PURL": "pkg:cargo/regex@1.7.3",
|
||||
"UID": "4633c68363763fad"
|
||||
},
|
||||
"Version": "1.7.3",
|
||||
"Relationship": "direct",
|
||||
"DependsOn": [
|
||||
"aho-corasick@0.7.20",
|
||||
"memchr@2.7.4",
|
||||
"regex-syntax@0.6.29"
|
||||
],
|
||||
"Layer": {},
|
||||
"Locations": [
|
||||
{
|
||||
"StartLine": 44,
|
||||
"EndLine": 53
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": "aho-corasick@0.7.20",
|
||||
"Name": "aho-corasick",
|
||||
"Identifier": {
|
||||
"PURL": "pkg:cargo/aho-corasick@0.7.20",
|
||||
"UID": "994a6d343f8da957"
|
||||
},
|
||||
"Version": "0.7.20",
|
||||
"Indirect": true,
|
||||
"Relationship": "indirect",
|
||||
"DependsOn": [
|
||||
"memchr@2.7.4"
|
||||
],
|
||||
"Layer": {},
|
||||
"Locations": [
|
||||
{
|
||||
"StartLine": 5,
|
||||
"EndLine": 12
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": "libc@0.2.171",
|
||||
"Name": "libc",
|
||||
"Identifier": {
|
||||
"PURL": "pkg:cargo/libc@0.2.171",
|
||||
"UID": "5395cf65d65d1f19"
|
||||
},
|
||||
"Version": "0.2.171",
|
||||
"Indirect": true,
|
||||
"Relationship": "indirect",
|
||||
"Layer": {},
|
||||
"Locations": [
|
||||
{
|
||||
"StartLine": 23,
|
||||
"EndLine": 27
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": "memchr@2.7.4",
|
||||
"Name": "memchr",
|
||||
"Identifier": {
|
||||
"PURL": "pkg:cargo/memchr@2.7.4",
|
||||
"UID": "3f037d5da23e5826"
|
||||
},
|
||||
"Version": "2.7.4",
|
||||
"Indirect": true,
|
||||
"Relationship": "indirect",
|
||||
"Layer": {},
|
||||
"Locations": [
|
||||
{
|
||||
"StartLine": 38,
|
||||
"EndLine": 42
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": "regex-syntax@0.6.29",
|
||||
"Name": "regex-syntax",
|
||||
"Identifier": {
|
||||
"PURL": "pkg:cargo/regex-syntax@0.6.29",
|
||||
"UID": "2c8dd93ce2f15b00"
|
||||
},
|
||||
"Version": "0.6.29",
|
||||
"Indirect": true,
|
||||
"Relationship": "indirect",
|
||||
"Layer": {},
|
||||
"Locations": [
|
||||
{
|
||||
"StartLine": 55,
|
||||
"EndLine": 59
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
81
integration/testdata/fixtures/repo/cargo/Cargo.lock
generated
vendored
Normal file
81
integration/testdata/fixtures/repo/cargo/Cargo.lock
generated
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
|
||||
dependencies = [
|
||||
"memchr 2.7.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "app"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"memchr 1.0.2",
|
||||
"regex",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.171"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr 2.7.4",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
15
integration/testdata/fixtures/repo/cargo/Cargo.toml
vendored
Normal file
15
integration/testdata/fixtures/repo/cargo/Cargo.toml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "app"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
regex = "=1.7.3"
|
||||
|
||||
[target.'cfg(not(target_os = "windows"))'.dependencies]
|
||||
memchr = { version = "1.*", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
winapi = "*"
|
||||
@@ -12,13 +12,16 @@ import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/mitchellh/hashstructure/v2"
|
||||
"github.com/samber/lo"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/aquasecurity/go-version/pkg/semver"
|
||||
goversion "github.com/aquasecurity/go-version/pkg/version"
|
||||
"github.com/aquasecurity/trivy/pkg/dependency"
|
||||
"github.com/aquasecurity/trivy/pkg/dependency/parser/rust/cargo"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/library/compare"
|
||||
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
|
||||
@@ -107,7 +110,7 @@ func (a cargoAnalyzer) parseCargoLock(filePath string, r io.Reader) (*types.Appl
|
||||
|
||||
func (a cargoAnalyzer) removeDevDependencies(fsys fs.FS, dir string, app *types.Application) error {
|
||||
cargoTOMLPath := path.Join(dir, types.CargoToml)
|
||||
directDeps, err := a.parseRootCargoTOML(fsys, cargoTOMLPath)
|
||||
root, workspaces, directDeps, err := a.parseRootCargoTOML(fsys, cargoTOMLPath)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
a.logger.Debug("Cargo.toml not found", log.FilePath(cargoTOMLPath))
|
||||
return nil
|
||||
@@ -148,6 +151,37 @@ func (a cargoAnalyzer) removeDevDependencies(fsys fs.FS, dir string, app *types.
|
||||
a.walkIndirectDependencies(pkg, pkgIDs, pkgs)
|
||||
}
|
||||
|
||||
// Identify root and workspace packages
|
||||
for pkgID, pkg := range pkgIDs {
|
||||
switch {
|
||||
case pkgID == root:
|
||||
pkg.Relationship = types.RelationshipRoot
|
||||
case slices.Contains(workspaces, pkgID):
|
||||
pkg.Relationship = types.RelationshipWorkspace
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
// Root/workspace package may include dev dependencies in lock file, so we need to remove them.
|
||||
pkg.DependsOn = lo.Filter(pkg.DependsOn, func(dep string, _ int) bool {
|
||||
_, ok := pkgs[dep]
|
||||
return ok
|
||||
})
|
||||
pkgs[pkgID] = pkg
|
||||
}
|
||||
|
||||
// Cargo allows creating cargo.toml files without name and version.
|
||||
// In this case, the lock file will not include this package.
|
||||
// e.g. when root cargo.toml contains only workspaces.
|
||||
// So we have to add it ourselves, and the ID in this case will be the hash of the toml file.
|
||||
if _, ok := pkgs[root]; !ok {
|
||||
pkgs[root] = types.Package{
|
||||
ID: root,
|
||||
Relationship: types.RelationshipRoot,
|
||||
DependsOn: workspaces,
|
||||
}
|
||||
}
|
||||
|
||||
pkgSlice := lo.Values(pkgs)
|
||||
sort.Sort(types.Packages(pkgSlice))
|
||||
|
||||
@@ -157,11 +191,17 @@ func (a cargoAnalyzer) removeDevDependencies(fsys fs.FS, dir string, app *types.
|
||||
}
|
||||
|
||||
type cargoToml struct {
|
||||
Package Package `toml:"package"`
|
||||
Dependencies Dependencies `toml:"dependencies"`
|
||||
Target map[string]map[string]Dependencies `toml:"target"`
|
||||
Workspace cargoTomlWorkspace `toml:"workspace"`
|
||||
}
|
||||
|
||||
type Package struct {
|
||||
Name string `toml:"name"`
|
||||
Version string `toml:"version"`
|
||||
}
|
||||
|
||||
type cargoTomlWorkspace struct {
|
||||
Dependencies Dependencies `toml:"dependencies"`
|
||||
Members []string `toml:"members"`
|
||||
@@ -171,20 +211,24 @@ type Dependencies map[string]any
|
||||
|
||||
// parseRootCargoTOML parses top-level Cargo.toml and returns dependencies.
|
||||
// It also parses workspace members and their dependencies.
|
||||
func (a cargoAnalyzer) parseRootCargoTOML(fsys fs.FS, filePath string) (map[string]string, error) {
|
||||
dependencies, members, err := parseCargoTOML(fsys, filePath)
|
||||
func (a cargoAnalyzer) parseRootCargoTOML(fsys fs.FS, filePath string) (string, []string, map[string]string, error) {
|
||||
rootPkg, dependencies, members, err := a.parseCargoTOML(fsys, filePath)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("unable to parse %s: %w", filePath, err)
|
||||
return "", nil, nil, xerrors.Errorf("unable to parse %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
// According to Cargo workspace RFC, workspaces can't be nested:
|
||||
// https://github.com/nox/rust-rfcs/blob/master/text/1525-cargo-workspace.md#validating-a-workspace
|
||||
var workspaces []string
|
||||
for _, member := range members {
|
||||
memberPath := path.Join(path.Dir(filePath), member, types.CargoToml)
|
||||
memberDeps, _, err := parseCargoTOML(fsys, memberPath)
|
||||
memberPkg, memberDeps, _, err := a.parseCargoTOML(fsys, memberPath)
|
||||
if err != nil {
|
||||
a.logger.Warn("Unable to parse Cargo.toml", log.String("member_path", memberPath), log.Err(err))
|
||||
continue
|
||||
}
|
||||
workspaces = append(workspaces, memberPkg)
|
||||
|
||||
// Member dependencies shouldn't overwrite dependencies from root cargo.toml file
|
||||
maps.Copy(memberDeps, dependencies)
|
||||
dependencies = memberDeps
|
||||
@@ -209,7 +253,7 @@ func (a cargoAnalyzer) parseRootCargoTOML(fsys fs.FS, filePath string) (map[stri
|
||||
}
|
||||
}
|
||||
|
||||
return deps, nil
|
||||
return rootPkg, workspaces, deps, nil
|
||||
}
|
||||
|
||||
func (a cargoAnalyzer) walkIndirectDependencies(pkg types.Package, pkgIDs, deps map[string]types.Package) {
|
||||
@@ -254,11 +298,11 @@ func (a cargoAnalyzer) matchVersion(currentVersion, constraint string) (bool, er
|
||||
return c.Check(ver), nil
|
||||
}
|
||||
|
||||
func parseCargoTOML(fsys fs.FS, filePath string) (Dependencies, []string, error) {
|
||||
func (a cargoAnalyzer) parseCargoTOML(fsys fs.FS, filePath string) (string, Dependencies, []string, error) {
|
||||
// Parse Cargo.toml
|
||||
f, err := fsys.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, nil, xerrors.Errorf("file open error: %w", err)
|
||||
return "", nil, nil, xerrors.Errorf("file open error: %w", err)
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
@@ -268,9 +312,11 @@ func parseCargoTOML(fsys fs.FS, filePath string) (Dependencies, []string, error)
|
||||
// declare `dependencies` to avoid panic
|
||||
dependencies := Dependencies{}
|
||||
if _, err = toml.NewDecoder(f).Decode(&tomlFile); err != nil {
|
||||
return nil, nil, xerrors.Errorf("toml decode error: %w", err)
|
||||
return "", nil, nil, xerrors.Errorf("toml decode error: %w", err)
|
||||
}
|
||||
|
||||
pkgID := a.packageID(tomlFile)
|
||||
|
||||
maps.Copy(dependencies, tomlFile.Dependencies)
|
||||
|
||||
// https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#platform-specific-dependencies
|
||||
@@ -281,5 +327,23 @@ func parseCargoTOML(fsys fs.FS, filePath string) (Dependencies, []string, error)
|
||||
// https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#inheriting-a-dependency-from-a-workspace
|
||||
maps.Copy(dependencies, tomlFile.Workspace.Dependencies)
|
||||
// https://doc.rust-lang.org/cargo/reference/workspaces.html#the-members-and-exclude-fields
|
||||
return dependencies, tomlFile.Workspace.Members, nil
|
||||
return pkgID, dependencies, tomlFile.Workspace.Members, nil
|
||||
}
|
||||
|
||||
// packageID builds PackageID by Package name and version.
|
||||
// If name is empty - use hash of cargoToml.
|
||||
func (a cargoAnalyzer) packageID(cargoToml cargoToml) string {
|
||||
if cargoToml.Package.Name != "" {
|
||||
return dependency.ID(types.Cargo, cargoToml.Package.Name, cargoToml.Package.Version)
|
||||
}
|
||||
|
||||
hash, err := hashstructure.Hash(cargoToml, hashstructure.FormatV2, &hashstructure.HashOptions{
|
||||
ZeroNil: true,
|
||||
IgnoreZeroValue: true,
|
||||
})
|
||||
if err != nil {
|
||||
a.logger.Warn("unable to hash package", log.String("package", cargoToml.Package.Name), log.Err(err))
|
||||
}
|
||||
|
||||
return strconv.FormatUint(hash, 16)
|
||||
}
|
||||
|
||||
@@ -27,6 +27,23 @@ func Test_cargoAnalyzer_Analyze(t *testing.T) {
|
||||
Type: types.Cargo,
|
||||
FilePath: "Cargo.lock",
|
||||
Packages: types.Packages{
|
||||
{
|
||||
ID: "app@0.1.0",
|
||||
Name: "app",
|
||||
Version: "0.1.0",
|
||||
Relationship: types.RelationshipRoot,
|
||||
Locations: []types.Location{
|
||||
{
|
||||
StartLine: 13,
|
||||
EndLine: 20,
|
||||
},
|
||||
},
|
||||
DependsOn: []string{
|
||||
"memchr@1.0.2",
|
||||
"regex-syntax@0.5.6",
|
||||
"regex@1.7.3",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "memchr@1.0.2",
|
||||
Name: "memchr",
|
||||
@@ -153,6 +170,21 @@ func Test_cargoAnalyzer_Analyze(t *testing.T) {
|
||||
Type: types.Cargo,
|
||||
FilePath: "Cargo.lock",
|
||||
Packages: types.Packages{
|
||||
{
|
||||
ID: "app@0.1.0",
|
||||
Name: "app",
|
||||
Version: "0.1.0",
|
||||
Relationship: types.RelationshipRoot,
|
||||
Locations: []types.Location{
|
||||
{
|
||||
StartLine: 4,
|
||||
EndLine: 9,
|
||||
},
|
||||
},
|
||||
DependsOn: []string{
|
||||
"memchr@2.5.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "memchr@2.5.0",
|
||||
Name: "memchr",
|
||||
@@ -413,6 +445,44 @@ func Test_cargoAnalyzer_Analyze(t *testing.T) {
|
||||
Type: types.Cargo,
|
||||
FilePath: "Cargo.lock",
|
||||
Packages: types.Packages{
|
||||
{
|
||||
ID: "d0e1231acd612a0f",
|
||||
Relationship: types.RelationshipRoot,
|
||||
DependsOn: []string{
|
||||
"member@0.1.0",
|
||||
"member2@0.1.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "member@0.1.0",
|
||||
Name: "member",
|
||||
Version: "0.1.0",
|
||||
Relationship: types.RelationshipWorkspace,
|
||||
Locations: []types.Location{
|
||||
{
|
||||
StartLine: 30,
|
||||
EndLine: 35,
|
||||
},
|
||||
},
|
||||
DependsOn: []string{
|
||||
"gdb-command@0.7.6",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "member2@0.1.0",
|
||||
Name: "member2",
|
||||
Version: "0.1.0",
|
||||
Relationship: types.RelationshipWorkspace,
|
||||
Locations: []types.Location{
|
||||
{
|
||||
StartLine: 37,
|
||||
EndLine: 42,
|
||||
},
|
||||
},
|
||||
DependsOn: []string{
|
||||
"regex@1.10.2",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "gdb-command@0.7.6",
|
||||
Name: "gdb-command",
|
||||
|
||||
Reference in New Issue
Block a user