From 340b5e17f09bd8afbf732b215b159dd6ce115447 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Fri, 6 Dec 2024 09:23:18 -0500 Subject: [PATCH] Add relationships for rust audit binary packages (#3500) * add rust audit binary pkg relationships Signed-off-by: Alex Goodman * fix linting Signed-off-by: Alex Goodman --------- Signed-off-by: Alex Goodman --- cmd/syft/internal/options/source.go | 2 +- syft/internal/fileresolver/file_indexer.go | 3 +- .../fileresolver/file_indexer_test.go | 8 +- .../fileresolver/filetree_resolver_test.go | 5 +- syft/pkg/cataloger/rust/cataloger.go | 4 +- syft/pkg/cataloger/rust/cataloger_test.go | 272 ++++++++++++++++-- syft/pkg/cataloger/rust/package.go | 14 +- syft/pkg/cataloger/rust/parse_audit_binary.go | 71 ++++- .../rust/test-fixtures/image-audit/Cargo.lock | 96 +++++++ .../rust/test-fixtures/image-audit/Cargo.toml | 7 + .../rust/test-fixtures/image-audit/Dockerfile | 15 +- .../test-fixtures/image-audit/src/main.rs | 13 + 12 files changed, 460 insertions(+), 50 deletions(-) create mode 100644 syft/pkg/cataloger/rust/test-fixtures/image-audit/Cargo.lock create mode 100644 syft/pkg/cataloger/rust/test-fixtures/image-audit/Cargo.toml create mode 100644 syft/pkg/cataloger/rust/test-fixtures/image-audit/src/main.rs diff --git a/cmd/syft/internal/options/source.go b/cmd/syft/internal/options/source.go index 53078305c2e..97e81b9459f 100644 --- a/cmd/syft/internal/options/source.go +++ b/cmd/syft/internal/options/source.go @@ -5,11 +5,11 @@ import ( "sort" "strings" - stereoscopeFile "github.com/anchore/stereoscope/pkg/file" "github.com/dustin/go-humanize" "github.com/scylladb/go-set/strset" "github.com/anchore/clio" + stereoscopeFile "github.com/anchore/stereoscope/pkg/file" "github.com/anchore/syft/syft/source/sourceproviders" ) diff --git a/syft/internal/fileresolver/file_indexer.go b/syft/internal/fileresolver/file_indexer.go index fe110c4ced9..bc6d660ac6a 100644 --- a/syft/internal/fileresolver/file_indexer.go +++ b/syft/internal/fileresolver/file_indexer.go @@ -5,11 +5,12 @@ import ( "os" "path/filepath" + "github.com/wagoodman/go-progress" + "github.com/anchore/stereoscope/pkg/file" "github.com/anchore/stereoscope/pkg/filetree" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/internal/windows" - "github.com/wagoodman/go-progress" ) type fileIndexer struct { diff --git a/syft/internal/fileresolver/file_indexer_test.go b/syft/internal/fileresolver/file_indexer_test.go index cce3981382d..165bc39d022 100644 --- a/syft/internal/fileresolver/file_indexer_test.go +++ b/syft/internal/fileresolver/file_indexer_test.go @@ -1,13 +1,15 @@ package fileresolver import ( - "github.com/anchore/stereoscope/pkg/file" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "io/fs" "os" "path" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anchore/stereoscope/pkg/file" ) // - Verify that both the parent and the path are indexed diff --git a/syft/internal/fileresolver/filetree_resolver_test.go b/syft/internal/fileresolver/filetree_resolver_test.go index e385ec8501f..1a3d661722d 100644 --- a/syft/internal/fileresolver/filetree_resolver_test.go +++ b/syft/internal/fileresolver/filetree_resolver_test.go @@ -14,13 +14,14 @@ import ( "testing" "time" - stereoscopeFile "github.com/anchore/stereoscope/pkg/file" - "github.com/anchore/syft/syft/file" "github.com/google/go-cmp/cmp" "github.com/scylladb/go-set/strset" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/goleak" + + stereoscopeFile "github.com/anchore/stereoscope/pkg/file" + "github.com/anchore/syft/syft/file" ) // Tests for filetree resolver when directory is used for index diff --git a/syft/pkg/cataloger/rust/cataloger.go b/syft/pkg/cataloger/rust/cataloger.go index 8951f1b2864..3f83f522347 100644 --- a/syft/pkg/cataloger/rust/cataloger.go +++ b/syft/pkg/cataloger/rust/cataloger.go @@ -9,6 +9,8 @@ import ( "github.com/anchore/syft/syft/pkg/cataloger/generic" ) +const cargoAuditBinaryCatalogerName = "cargo-auditable-binary-cataloger" + // NewCargoLockCataloger returns a new Rust Cargo lock file cataloger object. func NewCargoLockCataloger() pkg.Cataloger { return generic.NewCataloger("rust-cargo-lock-cataloger"). @@ -18,6 +20,6 @@ func NewCargoLockCataloger() pkg.Cataloger { // NewAuditBinaryCataloger returns a new Rust auditable binary cataloger object that can detect dependencies // in binaries produced with https://github.com/Shnatsel/rust-audit func NewAuditBinaryCataloger() pkg.Cataloger { - return generic.NewCataloger("cargo-auditable-binary-cataloger"). + return generic.NewCataloger(cargoAuditBinaryCatalogerName). WithParserByMimeTypes(parseAuditBinary, mimetype.ExecutableMIMETypeSet.List()...) } diff --git a/syft/pkg/cataloger/rust/cataloger_test.go b/syft/pkg/cataloger/rust/cataloger_test.go index 3a7a356db21..62b7bc099c4 100644 --- a/syft/pkg/cataloger/rust/cataloger_test.go +++ b/syft/pkg/cataloger/rust/cataloger_test.go @@ -3,48 +3,270 @@ package rust import ( "testing" + "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" ) func TestNewAuditBinaryCataloger(t *testing.T) { + locations := file.NewLocationSet(file.NewVirtualLocation("/usr/local/bin/hello_world", "/usr/local/bin/hello_world")) + + argh := pkg.Package{ + Name: "argh", + Version: "0.1.12", + PURL: "pkg:cargo/argh@0.1.12", + FoundBy: "cargo-auditable-binary-cataloger", + Locations: locations, + Language: pkg.Rust, + Type: pkg.RustPkg, + Metadata: pkg.RustBinaryAuditEntry{ + Name: "argh", + Version: "0.1.12", + Source: "crates.io", + }, + } + + arghDerive := pkg.Package{ + Name: "argh_derive", + Version: "0.1.12", + PURL: "pkg:cargo/argh_derive@0.1.12", + FoundBy: "cargo-auditable-binary-cataloger", + Locations: locations, + Language: pkg.Rust, + Type: pkg.RustPkg, + Metadata: pkg.RustBinaryAuditEntry{ + Name: "argh_derive", + Version: "0.1.12", + Source: "crates.io", + }, + } + + arghShared := pkg.Package{ + Name: "argh_shared", + Version: "0.1.12", + PURL: "pkg:cargo/argh_shared@0.1.12", + FoundBy: "cargo-auditable-binary-cataloger", + Locations: locations, + Language: pkg.Rust, + Type: pkg.RustPkg, + Metadata: pkg.RustBinaryAuditEntry{ + Name: "argh_shared", + Version: "0.1.12", + Source: "crates.io", + }, + } + + helloWorld := pkg.Package{ + Name: "hello_world", + Version: "0.1.0", + PURL: "pkg:cargo/hello_world@0.1.0", + FoundBy: "cargo-auditable-binary-cataloger", + Locations: locations, + Language: pkg.Rust, + Type: pkg.RustPkg, + Metadata: pkg.RustBinaryAuditEntry{ + Name: "hello_world", + Version: "0.1.0", + Source: "local", + }, + } + + procMacro2 := pkg.Package{ + Name: "proc-macro2", + Version: "1.0.92", + PURL: "pkg:cargo/proc-macro2@1.0.92", + FoundBy: "cargo-auditable-binary-cataloger", + Locations: locations, + Language: pkg.Rust, + Type: pkg.RustPkg, + Metadata: pkg.RustBinaryAuditEntry{ + Name: "proc-macro2", + Version: "1.0.92", + Source: "crates.io", + }, + } + + quote := pkg.Package{ + Name: "quote", + Version: "1.0.37", + PURL: "pkg:cargo/quote@1.0.37", + FoundBy: "cargo-auditable-binary-cataloger", + Locations: locations, + Language: pkg.Rust, + Type: pkg.RustPkg, + Metadata: pkg.RustBinaryAuditEntry{ + Name: "quote", + Version: "1.0.37", + Source: "crates.io", + }, + } + + serde := pkg.Package{ + Name: "serde", + Version: "1.0.215", + PURL: "pkg:cargo/serde@1.0.215", + FoundBy: "cargo-auditable-binary-cataloger", + Locations: locations, + Language: pkg.Rust, + Type: pkg.RustPkg, + Metadata: pkg.RustBinaryAuditEntry{ + Name: "serde", + Version: "1.0.215", + Source: "crates.io", + }, + } + + serdeDerive := pkg.Package{ + Name: "serde_derive", + Version: "1.0.215", + PURL: "pkg:cargo/serde_derive@1.0.215", + FoundBy: "cargo-auditable-binary-cataloger", + Locations: locations, + Language: pkg.Rust, + Type: pkg.RustPkg, + Metadata: pkg.RustBinaryAuditEntry{ + Name: "serde_derive", + Version: "1.0.215", + Source: "crates.io", + }, + } + + syn := pkg.Package{ + Name: "syn", + Version: "2.0.90", + PURL: "pkg:cargo/syn@2.0.90", + FoundBy: "cargo-auditable-binary-cataloger", + Locations: locations, + Language: pkg.Rust, + Type: pkg.RustPkg, + Metadata: pkg.RustBinaryAuditEntry{ + Name: "syn", + Version: "2.0.90", + Source: "crates.io", + }, + } + + unicodeIdent := pkg.Package{ + Name: "unicode-ident", + Version: "1.0.14", + PURL: "pkg:cargo/unicode-ident@1.0.14", + FoundBy: "cargo-auditable-binary-cataloger", + Locations: locations, + Language: pkg.Rust, + Type: pkg.RustPkg, + Metadata: pkg.RustBinaryAuditEntry{ + Name: "unicode-ident", + Version: "1.0.14", + Source: "crates.io", + }, + } expectedPkgs := []pkg.Package{ + argh, + arghDerive, + arghShared, + helloWorld, + procMacro2, + quote, + serde, + serdeDerive, + syn, + unicodeIdent, + } + + expectedRelationships := []artifact.Relationship{ { - Name: "auditable", - Version: "0.1.0", - PURL: "pkg:cargo/auditable@0.1.0", - FoundBy: "cargo-auditable-binary-cataloger", - Locations: file.NewLocationSet(file.NewVirtualLocation("/hello-auditable", "/hello-auditable")), - Language: pkg.Rust, - Type: pkg.RustPkg, - Metadata: pkg.RustBinaryAuditEntry{ - Name: "auditable", - Version: "0.1.0", - Source: "local", - }, + From: argh, + To: helloWorld, + Type: artifact.DependencyOfRelationship, }, { - Name: "hello-auditable", - Version: "0.1.0", - PURL: "pkg:cargo/hello-auditable@0.1.0", - FoundBy: "cargo-auditable-binary-cataloger", - Locations: file.NewLocationSet(file.NewVirtualLocation("/hello-auditable", "/hello-auditable")), - Language: pkg.Rust, - Type: pkg.RustPkg, - Metadata: pkg.RustBinaryAuditEntry{ - Name: "hello-auditable", - Version: "0.1.0", - Source: "local", - }, + From: arghDerive, + To: argh, + Type: artifact.DependencyOfRelationship, + }, + { + From: arghShared, + To: argh, + Type: artifact.DependencyOfRelationship, + }, + { + From: arghShared, + To: arghDerive, + Type: artifact.DependencyOfRelationship, + }, + { + From: procMacro2, + To: arghDerive, + Type: artifact.DependencyOfRelationship, + }, + { + From: procMacro2, + To: quote, + Type: artifact.DependencyOfRelationship, + }, + { + From: procMacro2, + To: serdeDerive, + Type: artifact.DependencyOfRelationship, + }, + { + From: procMacro2, + To: syn, + Type: artifact.DependencyOfRelationship, + }, + { + From: quote, + To: arghDerive, + Type: artifact.DependencyOfRelationship, + }, + { + From: quote, + To: serdeDerive, + Type: artifact.DependencyOfRelationship, + }, + { + From: quote, + To: syn, + Type: artifact.DependencyOfRelationship, + }, + { + From: serde, + To: arghShared, + Type: artifact.DependencyOfRelationship, + }, + { + From: serdeDerive, + To: serde, + Type: artifact.DependencyOfRelationship, + }, + { + From: syn, + To: arghDerive, + Type: artifact.DependencyOfRelationship, + }, + { + From: syn, + To: serdeDerive, + Type: artifact.DependencyOfRelationship, + }, + { + From: unicodeIdent, + To: procMacro2, + Type: artifact.DependencyOfRelationship, + }, + { + From: unicodeIdent, + To: syn, + Type: artifact.DependencyOfRelationship, }, } pkgtest.NewCatalogTester(). WithImageResolver(t, "image-audit"). IgnoreLocationLayer(). // this fixture can be rebuilt, thus the layer ID will change - Expects(expectedPkgs, nil). + Expects(expectedPkgs, expectedRelationships). TestCataloger(t, NewAuditBinaryCataloger()) } diff --git a/syft/pkg/cataloger/rust/package.go b/syft/pkg/cataloger/rust/package.go index be67d96a936..6af92745df6 100644 --- a/syft/pkg/cataloger/rust/package.go +++ b/syft/pkg/cataloger/rust/package.go @@ -25,19 +25,6 @@ func newPackageFromCargoMetadata(m pkg.RustCargoLockEntry, locations ...file.Loc return p } -func newPackagesFromAudit(location file.Location, versionInfo rustaudit.VersionInfo) []pkg.Package { - var pkgs []pkg.Package - - for _, dep := range versionInfo.Packages { - p := newPackageFromAudit(&dep, location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)) - if pkg.IsValid(&p) && dep.Kind == rustaudit.Runtime { - pkgs = append(pkgs, p) - } - } - - return pkgs -} - func newPackageFromAudit(dep *rustaudit.Package, locations ...file.Location) pkg.Package { p := pkg.Package{ Name: dep.Name, @@ -46,6 +33,7 @@ func newPackageFromAudit(dep *rustaudit.Package, locations ...file.Location) pkg Language: pkg.Rust, Type: pkg.RustPkg, Locations: file.NewLocationSet(locations...), + FoundBy: cargoAuditBinaryCatalogerName, Metadata: pkg.RustBinaryAuditEntry{ Name: dep.Name, Version: dep.Version, diff --git a/syft/pkg/cataloger/rust/parse_audit_binary.go b/syft/pkg/cataloger/rust/parse_audit_binary.go index 25525e91656..76e1509b7f0 100644 --- a/syft/pkg/cataloger/rust/parse_audit_binary.go +++ b/syft/pkg/cataloger/rust/parse_audit_binary.go @@ -5,9 +5,10 @@ import ( "errors" "fmt" - rustaudit "github.com/microsoft/go-rustaudit" + "github.com/microsoft/go-rustaudit" "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/internal/relationship" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/internal/unionreader" @@ -18,6 +19,7 @@ import ( // Catalog identifies executables then attempts to read Rust dependency information from them func parseAuditBinary(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { var pkgs []pkg.Package + var relationships []artifact.Relationship unionReader, err := unionreader.GetUnionReader(reader.ReadCloser) if err != nil { @@ -26,10 +28,12 @@ func parseAuditBinary(_ context.Context, _ file.Resolver, _ *generic.Environment infos, err := parseAuditBinaryEntry(unionReader, reader.RealPath) for _, versionInfo := range infos { - pkgs = append(pkgs, newPackagesFromAudit(reader.Location, versionInfo)...) + auditPkgs, auditRelationships := processAuditVersionInfo(reader.Location, versionInfo) + pkgs = append(pkgs, auditPkgs...) + relationships = append(relationships, auditRelationships...) } - return pkgs, nil, err + return pkgs, relationships, err } // scanFile scans file to try to report the Rust crate dependencies @@ -61,3 +65,64 @@ func parseAuditBinaryEntry(reader unionreader.UnionReader, filename string) ([]r return versionInfos, nil } + +// auditPkgPair is a helper struct to track the original index of the package in the original audit report + the syft package created for it +type auditPkgPair struct { + pkg *pkg.Package + rustPkg rustaudit.Package + index int +} + +func processAuditVersionInfo(location file.Location, versionInfo rustaudit.VersionInfo) ([]pkg.Package, []artifact.Relationship) { + var pkgs []pkg.Package + + // first pass: create packages for all runtime dependencies (skip dev and invalid dependencies) + pairsByOgIndex := make(map[int]auditPkgPair) + for idx, dep := range versionInfo.Packages { + p := newPackageFromAudit(&dep, location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)) + pair := auditPkgPair{ + rustPkg: dep, + index: idx, + } + if pkg.IsValid(&p) && dep.Kind == rustaudit.Runtime { + pkgs = append(pkgs, p) + pair.pkg = &pkgs[len(pkgs)-1] + } + pairsByOgIndex[idx] = pair + } + + // second pass: create relationships between any packages created + // we have all the original audit package indices + info, but not all audit packages will have syft packages. + // we need to be careful to not create relationships for packages that were not created. + var rels []artifact.Relationship + for _, parentPair := range pairsByOgIndex { + // the rust-audit report lists dependencies by index from the original version info object. We need to find + // the syft packages created for each listed dependency from that original object. + for _, ogPkgIndex := range parentPair.rustPkg.Dependencies { + if ogPkgIndex >= uint(len(versionInfo.Packages)) { + log.WithFields("pkg", parentPair.pkg).Trace("cargo audit dependency index out of range: %d", ogPkgIndex) + continue + } + depPair, ok := pairsByOgIndex[int(ogPkgIndex)] + if !ok { + log.WithFields("pkg", parentPair.pkg).Trace("cargo audit dependency not found: %d", ogPkgIndex) + continue + } + + if depPair.pkg == nil || parentPair.pkg == nil { + // skip relationships for syft packages that were not created from the original report (no matter the reason) + continue + } + + rels = append(rels, artifact.Relationship{ + From: *depPair.pkg, + To: *parentPair.pkg, + Type: artifact.DependencyOfRelationship, + }) + } + } + + relationship.Sort(rels) + + return pkgs, rels +} diff --git a/syft/pkg/cataloger/rust/test-fixtures/image-audit/Cargo.lock b/syft/pkg/cataloger/rust/test-fixtures/image-audit/Cargo.lock new file mode 100644 index 00000000000..c00250ea60f --- /dev/null +++ b/syft/pkg/cataloger/rust/test-fixtures/image-audit/Cargo.lock @@ -0,0 +1,96 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "argh" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7af5ba06967ff7214ce4c7419c7d185be7ecd6cc4965a8f6e1d8ce0398aad219" +dependencies = [ + "argh_derive", + "argh_shared", +] + +[[package]] +name = "argh_derive" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56df0aeedf6b7a2fc67d06db35b09684c3e8da0c95f8f27685cb17e08413d87a" +dependencies = [ + "argh_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "argh_shared" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5693f39141bda5760ecc4111ab08da40565d1771038c4a0250f03457ec707531" +dependencies = [ + "serde", +] + +[[package]] +name = "hello_world" +version = "0.1.0" +dependencies = [ + "argh", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" diff --git a/syft/pkg/cataloger/rust/test-fixtures/image-audit/Cargo.toml b/syft/pkg/cataloger/rust/test-fixtures/image-audit/Cargo.toml new file mode 100644 index 00000000000..649c3178267 --- /dev/null +++ b/syft/pkg/cataloger/rust/test-fixtures/image-audit/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "hello_world" +version = "0.1.0" +edition = "2021" + +[dependencies] +argh = "0.1" diff --git a/syft/pkg/cataloger/rust/test-fixtures/image-audit/Dockerfile b/syft/pkg/cataloger/rust/test-fixtures/image-audit/Dockerfile index f86c781dcae..23a3b3d499d 100644 --- a/syft/pkg/cataloger/rust/test-fixtures/image-audit/Dockerfile +++ b/syft/pkg/cataloger/rust/test-fixtures/image-audit/Dockerfile @@ -1 +1,14 @@ -FROM docker.io/tofay/hello-rust-auditable:latest@sha256:1d35d1e007180b3f7500aae5e27560697909132ca9a6d480c4c825534c1c47a9 \ No newline at end of file +FROM rust:1.74.0 AS builder + +WORKDIR /app + +RUN cargo install cargo-auditable --locked +COPY Cargo.toml Cargo.lock ./ +COPY src ./src +RUN cargo fetch +RUN cargo auditable build --release + +FROM scratch + +COPY --from=builder /app/target/release/hello_world /usr/local/bin/hello_world + diff --git a/syft/pkg/cataloger/rust/test-fixtures/image-audit/src/main.rs b/syft/pkg/cataloger/rust/test-fixtures/image-audit/src/main.rs new file mode 100644 index 00000000000..6f9e66167c2 --- /dev/null +++ b/syft/pkg/cataloger/rust/test-fixtures/image-audit/src/main.rs @@ -0,0 +1,13 @@ +use argh::FromArgs; + +#[derive(FromArgs)] +#[argh(description = "A simple Hello World CLI application.")] +struct Args { + #[argh(option, description = "name to greet")] + name: String, +} + +fn main() { + let args: Args = argh::from_env(); + println!("Hello, {}!", args.name); +} \ No newline at end of file