RTECO-1574 - Implementation of Nuget Support for client - - #3664
RTECO-1574 - Implementation of Nuget Support for client -#3664bhanurp wants to merge 30 commits into
Conversation
**Source:** https://jfrog-int.atlassian.net/browse/RTECO-1574 - Start working on Nuget V2, V3 support for Nuget package manager in jfrog cli. - Make sure PRs are manageable and code is written in a way it can be reused across the clients - Add support for nugetV3 and nugetV2 - refer to [unsupported block: inlineCard] ## Things to consider while implementing - Use JFROG_CLI_NATIVE_IMPLEMENTATION support since nuget is already supported when this is set it routes via native package manager(flexpack). - BuildInfo collection in build-info-go - check sum calculation - Original Deployment Repository calculation - Requested By calculation for dependencies - Easy authentication using nuget’s credentials like mentioned in above Atlassian wiki. - Build Info collection for both dependencies and artifacts for all the commands eligible listed in above wiki. - Set properties clearly on artifacts published. - Make sure the buildinfo is figuring out and showing show in tree in build info section when published to artifactory. ## Not to implement - nuget-config support is not required since this is flexpack implementation. **Parent:** RTECO-395 **Components:** jfrog-cli-nuget Task: RTECO-1574
- Only resolve/require server details for FlexPack NuGet commands that actually need them (push with a deploy repo, or restore with a resolve repo), instead of unconditionally requiring a --server-id for every subcommand. - Renamed --repo-deploy to --repo and --allow-insecure-connections to --insecure-tls to match the flags actually documented/used elsewhere. - Added getNugetCommandName to correctly treat dotnet's two-token "nuget push" as a single push subcommand, instead of misclassifying it. - Replaced the temporary local-filesystem-path go.mod replaces for build-info-go and jfrog-cli-artifactory (committed by mistake in the previous commit on this branch) with proper pseudo-version pins against their latest pushed RTECO-1574 commits, which fix: packages.config checksum/scope/cache-miss gaps, the flat NuGet push storage path assumption that broke property stamping, a restore-vs-push module identity mismatch that split one project into two disconnected build-info modules, missing .slnx solution support, and a missing --source flag that made every native push fail outright. Verified live against a real Artifactory server, building purely from the pinned remote commits (no local path replaces).
- nuget_native_test.go: full scenario-table coverage for the FlexPack
native `jf nuget` client (JFROG_RUN_NATIVE=true), separate from and
not touching the existing legacy nuget_test.go suite.
- buildtools/cli.go: --scan wasn't extracted before invoking nuget.exe
in the FlexPack path, so it leaked through and nuget.exe rejected it
with "Unknown option: '--scan'". Strip it like the other jf-level
flags; conditional-upload Xray scanning itself isn't wired for NuGet
FlexPack yet (same documented gap as the curation hook).
- utils/tests/consts.go, utils/tests/utils.go: add NugetLocalRepo/
NugetVirtualRepo fixtures alongside the existing NugetRemoteRepo one,
needed by the new test suite's local-repo and virtual-repo scenarios.
- testdata/nuget_{local,virtual}_repository_config.json: corresponding
repo-config templates.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…d fork commits Replaces local filesystem paths (used for live local testing) with github.com/bhanurp/build-info-go@74d0864 and github.com/bhanurp/jfrog-cli-artifactory@55bd344, so CI can resolve these dependencies. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tives The Aug 7 merge of master into RTECO-1574 botched the import block in buildtools/cli.go: it duplicated the nixcommand import and dropped the nugetcommand import entirely, replacing it with an unrelated same-named-concept package (build-info-go's dotnetutils, which is still needed separately for the ToolchainType enum). Restored the correct import (jfrog-cli-artifactory/artifactory/commands/nuget). Also bumped both replace directives to the latest pushed fork commits: build-info-go@978cd54 (includes reconciling a duplicate .slnx implementation that the same master merge introduced upstream-side) and jfrog-cli-artifactory@63a5fd2 (now caught up with its own upstream/main, including APT auth command support jfrog-cli's merged master code now depends on). Verified: go build -tags nuget ./... and go vet -tags nuget . both clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- gosec G703 (path traversal via taint analysis) x5: all flagged os.ReadFile/os.WriteFile calls operate on paths derived from this test's own t.TempDir()/testdata fixtures, never untrusted input. Annotated with #nosec and a justification each. - errcheck: unchecked res.Body.Close() error in doAccessRequest. - staticcheck SA4010: the 'paths' slice in TestNugetFlexPackPushWildcardGlob was appended to but never read (the test pushes via a glob pattern directly) - removed the dead variable entirely rather than working around the warning. Verified: golangci-lint run with the same flag set as the Go-Sec CI job is clean across the whole package. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tory's jfrog-cli's own go.mod had a stale direct replace (74d0864, pre-.slnx reconciliation) even though jfrog-cli-artifactory's go.mod already pointed at 978cd54 - Go only honors replace directives from the root module, not from dependencies, so this one actually governed the build. Aligned to the same commit for consistency with the upstream build-info-go PR. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ixes) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every 'jf nuget'/'jf dotnet' command now runs through the FlexPack build-info path (runNugetFlexPackCmd), which reports a dependency's RequestedBy as only the chain of packages that pulled it in, not the enclosing project/module it's already grouped under. Direct dependencies now have empty RequestedBy instead of [moduleName].
…asserters RequestedBy no longer echoes the enclosing module, so the assertion helpers no longer need the module name.
NugetCmd/DotnetCmd called ShouldRunNative("") with a hardcoded empty
path, so whenever FlexPack was enabled (JFROG_RUN_NATIVE=true) every
invocation went native even when a legacy nuget-config/dotnet-config
file existed - silently changing output for existing config-based
setups. Follow the same pattern already used by Maven/Gradle: resolve
the real config path first and only go native when no config exists.
Revert the TestNugetResolve RequestedBy assertion changes from the
prior two commits - those tests create a legacy config file, so with
this fix they exercise the legacy path again and keep their original
expected shape.
The legacy path only stripped --allow-insecure-connections, so --insecure-tls (the flag name tests and newer callers use, expecting FlexPack to strip it) leaked through to nuget.exe/dotnet, which reject it as an unknown option. This broke every legacy-config-based restore identically on Linux and Windows once the previous commit correctly routed config-based invocations back to the legacy path.
This reverts commit f6e9d6c.
…tests Every call site in nuget_test.go creates a legacy nuget-config/dotnet-config file, routing through the legacy NugetCmd/DotnetCmd path, which only recognizes --allow-insecure-connections. --insecure-tls is FlexPack's own flag name, stripped only on that path - it should not be handled by legacy at all. Fixing the test's flag name instead of teaching legacy about a flag it does not need keeps the two paths cleanly separated: FlexPack strips its flag, legacy passes its own through unmodified.
nuget_native_test.go's ~18 call sites shared nuget_test.go's allowInsecureConnectionForTests helper, which the previous commit changed to append --allow-insecure-connections for the legacy path. Every FlexPack-only test in this file has no config file and always routes through runNugetFlexPackCmd, which only strips --insecure-tls - so that change broke every one of them with the same "Unknown option" failure we just fixed on the other side. Added a separate allowInsecureConnectionForFlexPackTests helper scoped to this file instead of re-introducing flag aliasing into either CLI path.
TestNugetFlexPackBuildScanReportsVulnerabilities and TestNugetFlexPackBuildScanAfterPromotion call 'jf rt build-scan' unconditionally, but the CI nuget job's local JFrog Platform container never starts Xray (its startup log only shows Artifactory, router, metadata, frontend, observability, onemodel, topology, event, jfconnect, jfbus, jfmelt - no xray). This file already has the right pattern for this (see TestNugetFlexPackFullStatelessPipeline's 'if *tests.TestXray' guard around its own build-scan call); the two scan-only tests were just missing it entirely. TestNugetFlexPackBuildScanFullTransitiveTree is unaffected - it never calls build-scan itself, only reads published build-info.
createThrowawayProject sent a hardcoded "Authorization: Bearer <serverDetails.AccessToken>" header, but the CI nuget job configures its local server with a username/password, not a token - so AccessToken is empty there and the Access API correctly 403s an empty bearer token. Switched to artUtils.CreateAccessServiceManager, the same SDK-backed mechanism artifactory_test.go's own project-scoped tests already use, which works with whichever auth serverDetails actually holds. Removed the now-unused accessApiBaseUrl helper. TestNugetFlexPackReleaseBundleFromNugetBuild called 'jfrog rbc' unconditionally and 403'd for the same reason TestXray-gated tests would without Xray: the CI job's local platform does not have Lifecycle/Release Bundles v2 enabled. Gated it behind the existing *tests.TestLifecycle flag.
nugetTests.yml only passed --jfrog.url/--jfrog.adminToken when JFROG_TESTS_IS_EXTERNAL=='true'; for the normal local-install case it passed neither, so the test binary fell back to its jfrog.url flag's hardcoded default (http://localhost:8081/) - Artifactory's own direct port. install-local-artifactory's local-rt-setup step actually exports JFROG_TESTS_URL=http://127.0.0.1:8082, the platform router that fronts Lifecycle/onemodel (Artifactory's own port has no /lifecycle route, which is why 'jfrog rbc' came back as a raw Tomcat 403 HTML page, not a JSON API error). lifecycleTests.yml already passes these flags unconditionally - mirrored that same pattern here. With the router now reachable, TestNugetFlexPackReleaseBundleFromNugetBuild no longer needs the *tests.TestLifecycle gate added two commits ago - it is fully self-contained (creates/cleans up its own build and release bundle) and does not depend on the job-wide --test.lifecycle flag's much larger provisioning step. Xray remains genuinely unavailable in this setup regardless of URL - no workflow in this repo passes --test.xray or provisions an Xray service via local-rt-setup, so the two build-scan tests keep their *tests.TestXray skip gate from the earlier commit.
Revert the nugetTests.yml change from the previous commit - pointing the whole job at the router port risked changing behavior for all ~85 other passing tests, unverified against the actual CI setup. Instead, added withLifecycleRouterUrl, which temporarily repoints just the "default" server profile at port 8082 (rewriting only ":8081", a no-op against external servers like ecosys with no such port split) for the single 'jfrog rbc' call in TestNugetFlexPackReleaseBundleFromNugetBuild, then restores the original URL immediately after. Every other test in this file keeps using Artifactory's direct port exactly as before.
# Conflicts: # go.sum
…skip the ad-hoc -Source TLS test createThrowawayProject's AccessServicesManager fix (previous commit) resolved the auth mechanism, but still 403'd identically to 'jfrog rbc' on CI - same raw Tomcat 403 HTML page, because it built the manager from the plain serverDetails, which points at Artifactory's direct port. The Access API is fronted by the platform router just like Lifecycle, not exposed on Artifactory's own port. Factored the ":8081"->":8082" rewrite (already used by withLifecycleRouterUrl) into a shared platformRouterUrl helper, and added routerServerDetails to build a router-pointed *config.ServerDetails copy for SDK-level calls that take one directly instead of going through the CLI's own config profile. TestNugetFlexPackTlsSelfSignedRequiresInsecureFlag: skipped per diagnosis - it pushes through an explicit -Source override, which nuget.exe treats as an ad-hoc source bypassing the generated nuget.config's allowInsecureConnections entirely. Confirmed failing identically on macOS, Linux, and Windows, so this is not something --insecure-tls's wiring can fix; the test's own premise doesn't hold against how nuget.exe actually resolves an inline -Source.
…oncreteVersion The test patches reference.csproj's bootstrap PackageReference version by matching a literal string containing a bare "\n" between the <PackageReference> and <Version> lines. On the Windows CI runner, git checks this fixture out with CRLF line endings, so the literal match never fired - strings.Replace silently returned the original content unchanged, and the very next require.NotEqual caught it and failed the test in setup, before any restore/push ran. Ubuntu and macOS checkout this file with plain LF, which is why only Windows failed. Normalize CRLF to LF before matching/patching. The rewritten content is written back as LF-only, which MSBuild/nuget.exe parse identically to CRLF - XML doesn't care about line-ending style.
# Conflicts: # buildtools/cli.go # go.mod # go.sum
aptTests.yml is invoked the same way nugetTests.yml/lifecycleTests.yml are - a workflow_call-only reusable workflow gated behind build-gate.yml's pull_request_target + human approval - but its Checkout code step was missing allow-unsafe-pr-checkout: true, which the other two already have. Without it, checkout of the PR head SHA under pull_request_target's elevated privileges is blocked, which is why the apt job's tests have been failing. Checked the rest of .github/workflows for the same gap: analysis.yml's checkout steps don't need it (triggered by plain pull_request, not pull_request_target, so the elevated-privilege risk this flag guards against doesn't apply), and dependabot-auto-merge.yml doesn't check out PR code at all.
View full scan results in JFrog Platform📗 Scan Summary
|
at 🎯 Static Application Security Testing (SAST) VulnerabilityFull descriptionVulnerability Details
OverviewHardcoded credentials are usernames, passwords, API keys, or other secrets Vulnerable exampleIn this example, the database username and password for the frog pond are package main
import (
"database/sql"
"fmt"
"log"
_ "[github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql)"
)
func main() {
// VULNERABLE: Hardcoded database credentials for the frog pond.
frogUser := "pond_admin"
frogPassword := "LeapFlog123!"
pondName := "lilypad_db"
connStr := fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/%s",
frogUser, frogPassword, pondName)
lilypadDB, err := sql.Open("mysql", connStr)
if err != nil {
log.Fatalf("Error opening database: %v", err)
}
defer lilypadDB.Close()
err = lilypadDB.Ping()
if err != nil {
log.Fatalf("Error pinging database: %v", err)
}
fmt.Println("Successfully connected to the frog pond.")
}RemediationThe remediated code retrieves the database credentials from environment package main
import (
"database/sql"
"fmt"
"log"
"os"
_ "[github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql)"
)
func main() {
// SECURE: Retrieve credentials from environment variables.
frogUser := os.Getenv("FROG_DB_USER")
frogPassword := os.Getenv("FROG_DB_PASS")
pondName := os.Getenv("FROG_DB_NAME")
if frogUser == "" || frogPassword == "" || pondName == "" {
log.Fatal("DB credentials are not set in environment variables.")
}
connStr := fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/%s",
frogUser, frogPassword, pondName)
lilypadDB, err := sql.Open("mysql", connStr)
if err != nil {
log.Fatalf("Error opening database: %v", err)
}
defer lilypadDB.Close()
err = lilypadDB.Ping()
if err != nil {
log.Fatalf("Error pinging database: %v", err)
}
fmt.Println("Successfully connected to the frog pond.")
} |
| @@ -97,6 +97,8 @@ const ( | |||
| NpmLocalScopedRespositoryConfig = "npm_local_scoped_repository_config.json" | |||
There was a problem hiding this comment.
🎯 Static Application Security Testing (SAST) Vulnerability
Full description
Vulnerability Details
| Rule ID: | go-hardcoded-credentials |
Overview
Hardcoded credentials are usernames, passwords, API keys, or other secrets
embedded directly in source code. This practice, identified by CWE-798, is
highly insecure because it makes it easy for anyone with access to the code to
discover and misuse the credentials. If the code is publicly released, shared,
or leaked, the credentials will be exposed to unauthorized parties.
Vulnerable example
In this example, the database username and password for the frog pond are
hardcoded directly in the source code as string literals. This is a major
security risk, as anyone who can read this file can steal the credentials and
gain unauthorized access to the database.
package main
import (
"database/sql"
"fmt"
"log"
_ "[github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql)"
)
func main() {
// VULNERABLE: Hardcoded database credentials for the frog pond.
frogUser := "pond_admin"
frogPassword := "LeapFlog123!"
pondName := "lilypad_db"
connStr := fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/%s",
frogUser, frogPassword, pondName)
lilypadDB, err := sql.Open("mysql", connStr)
if err != nil {
log.Fatalf("Error opening database: %v", err)
}
defer lilypadDB.Close()
err = lilypadDB.Ping()
if err != nil {
log.Fatalf("Error pinging database: %v", err)
}
fmt.Println("Successfully connected to the frog pond.")
}Remediation
The remediated code retrieves the database credentials from environment
variables instead of hardcoding them. This is a much more secure approach, as
it separates the secrets from the source code. This allows credentials to be
managed securely by deployment systems and rotated without changing the code.
package main
import (
"database/sql"
"fmt"
"log"
"os"
_ "[github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql)"
)
func main() {
// SECURE: Retrieve credentials from environment variables.
frogUser := os.Getenv("FROG_DB_USER")
frogPassword := os.Getenv("FROG_DB_PASS")
pondName := os.Getenv("FROG_DB_NAME")
if frogUser == "" || frogPassword == "" || pondName == "" {
log.Fatal("DB credentials are not set in environment variables.")
}
connStr := fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/%s",
frogUser, frogPassword, pondName)
lilypadDB, err := sql.Open("mysql", connStr)
if err != nil {
log.Fatalf("Error opening database: %v", err)
}
defer lilypadDB.Close()
err = lilypadDB.Ping()
if err != nil {
log.Fatalf("Error pinging database: %v", err)
}
fmt.Println("Successfully connected to the frog pond.")
}| @@ -97,6 +97,8 @@ const ( | |||
| NpmLocalScopedRespositoryConfig = "npm_local_scoped_repository_config.json" | |||
| NpmRemoteRepositoryConfig = "npm_remote_repository_config.json" | |||
There was a problem hiding this comment.
🎯 Static Application Security Testing (SAST) Vulnerability
Full description
Vulnerability Details
| Rule ID: | go-hardcoded-credentials |
Overview
Hardcoded credentials are usernames, passwords, API keys, or other secrets
embedded directly in source code. This practice, identified by CWE-798, is
highly insecure because it makes it easy for anyone with access to the code to
discover and misuse the credentials. If the code is publicly released, shared,
or leaked, the credentials will be exposed to unauthorized parties.
Vulnerable example
In this example, the database username and password for the frog pond are
hardcoded directly in the source code as string literals. This is a major
security risk, as anyone who can read this file can steal the credentials and
gain unauthorized access to the database.
package main
import (
"database/sql"
"fmt"
"log"
_ "[github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql)"
)
func main() {
// VULNERABLE: Hardcoded database credentials for the frog pond.
frogUser := "pond_admin"
frogPassword := "LeapFlog123!"
pondName := "lilypad_db"
connStr := fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/%s",
frogUser, frogPassword, pondName)
lilypadDB, err := sql.Open("mysql", connStr)
if err != nil {
log.Fatalf("Error opening database: %v", err)
}
defer lilypadDB.Close()
err = lilypadDB.Ping()
if err != nil {
log.Fatalf("Error pinging database: %v", err)
}
fmt.Println("Successfully connected to the frog pond.")
}Remediation
The remediated code retrieves the database credentials from environment
variables instead of hardcoding them. This is a much more secure approach, as
it separates the secrets from the source code. This allows credentials to be
managed securely by deployment systems and rotated without changing the code.
package main
import (
"database/sql"
"fmt"
"log"
"os"
_ "[github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql)"
)
func main() {
// SECURE: Retrieve credentials from environment variables.
frogUser := os.Getenv("FROG_DB_USER")
frogPassword := os.Getenv("FROG_DB_PASS")
pondName := os.Getenv("FROG_DB_NAME")
if frogUser == "" || frogPassword == "" || pondName == "" {
log.Fatal("DB credentials are not set in environment variables.")
}
connStr := fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/%s",
frogUser, frogPassword, pondName)
lilypadDB, err := sql.Open("mysql", connStr)
if err != nil {
log.Fatalf("Error opening database: %v", err)
}
defer lilypadDB.Close()
err = lilypadDB.Ping()
if err != nil {
log.Fatalf("Error pinging database: %v", err)
}
fmt.Println("Successfully connected to the frog pond.")
}

Generated by workbench RTECO-1574.
RTECO-1574 - Implementation of Nuget Support for client -
Considerations while implementation
Not to implement
Parent: RTECO-395
Components: jfrog-cli-nuget
Evidence: see Artifactory bundle for RTECO-1574.
Jira: https://jfrog-int.atlassian.net/browse/RTECO-1574
Supersedes #3616, opened from a branch pushed directly to this repo (rather than a fork) so that
pull_request_target-gated workflows (e.g. apt tests) resolve their workflow YAML from the correct ref.