Skip to content

Conversation

@matt-edmondson
Copy link
Contributor

No description provided.

- Updated package versions in CrossRepoActions.csproj for ktsu libraries and Microsoft packages.
- Added support for central package management in Dotnet class, including methods to check for central management, update packages, and handle outdated dependencies.
- Refactored UpdatePackages verb to accommodate both central and traditional package management approaches.
- Introduced comprehensive documentation outlining the features, installation instructions, usage examples, and command options for the CrossRepoActions .NET console application.
- Highlighted core commands such as UpdatePackages, BuildAndTest, and GitPull, along with their functionalities.
- Included prerequisites, configuration options, and dependencies to assist users in setting up and utilizing the application effectively.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Git Class Vulnerable to Command Injection

The Git class methods using RunCommand.Execute are vulnerable to command injection and failures. Parameters such as commit messages, file paths, and repository paths are directly interpolated into the shell command string without proper quoting or escaping. This allows shell metacharacters (e.g., spaces, quotes, semicolons, pipes) to break commands or execute arbitrary code. This regression occurred when switching from PowerShell's safe parameter handling to direct string interpolation.

CrossRepoActions/Git.cs#L35-L81

RunCommand.Execute($"git -C {repo} pull --all -v", new LineOutputHandler(s => results.Add(s.Trim()), s => results.Add(s.Trim())));
return results;
}
internal static IEnumerable<string> Push(AbsoluteDirectoryPath repo)
{
Collection<string> results = [];
RunCommand.Execute($"git -C {repo} push -v", new LineOutputHandler(s => results.Add(s.Trim()), s => results.Add(s.Trim())));
return results;
}
internal static IEnumerable<string> Status(AbsoluteDirectoryPath repo, AbsoluteFilePath filePath)
{
Collection<string> results = [];
RunCommand.Execute($"git -C {repo} status --short -- {filePath}", new LineOutputHandler(s => results.Add(s.Trim()), s => results.Add(s.Trim())));
return results;
}
internal static IEnumerable<string> Unstage(AbsoluteDirectoryPath repo)
{
Collection<string> results = [];
RunCommand.Execute($"git -C {repo} restore --staged {repo}", new LineOutputHandler(s => results.Add(s.Trim()), s => results.Add(s.Trim())));
return results;
}
internal static IEnumerable<string> Add(AbsoluteDirectoryPath repo, AbsoluteFilePath filePath)
{
Collection<string> results = [];
RunCommand.Execute($"git -C {repo} add {filePath}", new LineOutputHandler(s => results.Add(s.Trim()), s => results.Add(s.Trim())));
return results;
}
internal static IEnumerable<string> Commit(AbsoluteDirectoryPath repo, string message)
{
Collection<string> results = [];
RunCommand.Execute($"git -C {repo} commit -m {message}", new LineOutputHandler(s => results.Add(s.Trim()), s => results.Add(s.Trim())));

Fix in CursorFix in Web


Bug: Path Quoting and Test Output Issues

Multiple dotnet commands executed via RunCommand.Execute use unquoted project or solution file paths in their command strings. This causes commands to fail if paths contain spaces, as RunCommand does not automatically handle quoting like the previous PowerShell implementation. Affected methods include BuildProject, GetProjects, GetSolutionDependencies, GetOutdatedProjectDependencies, UpdatePackagesTraditional, GetProjectAssemblyName, GetProjectVersion, IsProjectPackable, GetOutdatedCentralPackageDependencies, and GetOutdatedPackagesJson.

Additionally, the RunTests() method now incorrectly filters its output to only error messages by calling GetErrors(), which breaks downstream logic expecting full test output to parse pass/fail results.

CrossRepoActions/Dotnet.cs#L31-L173

RunCommand.Execute($"dotnet build --nologo {projectFile}", new LineOutputHandler(results.Add, results.Add));
return GetErrors(results);
}
internal static Collection<string> RunTests()
{
Collection<string> results = [];
RunCommand.Execute($"dotnet vstest **/bin/**/*Test.dll --logger:console;verbosity=normal --nologo", new LineOutputHandler(results.Add, results.Add));
return GetErrors(results);
}
internal static Collection<string> GetTests()
{
Collection<string> results = [];
RunCommand.Execute($"dotnet vstest --ListTests --nologo **/bin/**/*Test.dll", new LineOutputHandler(results.Add, results.Add));
var filteredResults = results
.Where(r => r is not null && !r.StartsWith("The following") && !r.StartsWith("No test source"))
.ToCollection();
return filteredResults;
}
internal static Collection<string> GetProjects(AbsoluteFilePath solutionFile)
{
Collection<string> results = [];
RunCommand.Execute($"dotnet sln {solutionFile} list", new LineOutputHandler(results.Add, results.Add));
var filteredResults = results
.Where(r => r is not null && r.EndsWithOrdinal(".csproj"))
.ToCollection();
return filteredResults;
}
internal static Collection<Package> GetSolutionDependencies(AbsoluteFilePath solutionFile)
{
Collection<string> results = [];
RunCommand.Execute($"dotnet list {solutionFile} package --include-transitive", new LineOutputHandler(results.Add, results.Add));
var filteredResults = results
.Where(r => r is not null && r.StartsWithOrdinal(">"))
.ToCollection();
var dependencies = filteredResults
.Select(r =>
{
string[] parts = r.Split(' ');
return new Package()
{
Name = parts[1],
Version = parts.Last(),
};
})
.ToCollection();
return dependencies;
}
private const string packageJsonError = "Could not parse JSON output from 'dotnet list package --format-json'";
internal static Collection<Package> GetOutdatedProjectDependencies(AbsoluteFilePath projectFile)
{
Collection<string> results = [];
RunCommand.Execute($"dotnet list {projectFile} package --format=json", new LineOutputHandler(results.Add, results.Add));
string jsonString = string.Join("", results);
var rootObject = JsonNode.Parse(jsonString)?.AsObject()
?? throw new InvalidDataException(packageJsonError);
var projects = rootObject["projects"]?.AsArray()
?? throw new InvalidDataException(packageJsonError);
var frameworks = projects.Where(p =>
{
var pObj = p?.AsObject();
return pObj?["frameworks"]?.AsArray() != null;
})
.SelectMany(p =>
{
return p?.AsObject()?["frameworks"]?.AsArray()
?? throw new InvalidDataException(packageJsonError);
});
var packages = frameworks.SelectMany(f =>
{
return (f as JsonObject)?["topLevelPackages"]?.AsArray()
?? throw new InvalidDataException(packageJsonError);
})
.Select(ExtractPackageFromJsonNode)
.DistinctBy(p => p.Name)
.ToCollection();
return packages;
}
private static Package ExtractPackageFromJsonNode(JsonNode? p)
{
string name = p?["id"]?.AsValue().GetValue<string>()
?? throw new InvalidDataException(packageJsonError);
string version = p?["requestedVersion"]?.AsValue().GetValue<string>()
?? throw new InvalidDataException(packageJsonError);
return new Package()
{
Name = name,
Version = version,
};
}
internal static Collection<string> UpdatePackages(AbsoluteFilePath projectFile, IEnumerable<Package> packages)
{
// Find the solution file to determine if central package management is used
var solutionPath = FindSolutionForProject(projectFile);
if (solutionPath != null && UsesCentralPackageManagement(solutionPath))
{
// Use central package management update approach
return UpdatePackagesWithCentralManagement(solutionPath, packages);
}
// Use traditional per-project update approach
return UpdatePackagesTraditional(projectFile, packages);
}
/// <summary>
/// Updates packages using traditional per-project approach.
/// </summary>
private static Collection<string> UpdatePackagesTraditional(AbsoluteFilePath projectFile, IEnumerable<Package> packages)
{
Collection<string> output = [];
foreach (var package in packages)
{
Collection<string> results = [];
string pre = package.Version.Contains('-') ? "--prerelease" : "";
RunCommand.Execute($"dotnet add {projectFile} package {package.Name} {pre}", new LineOutputHandler(results.Add, results.Add));

Fix in CursorFix in Web


Bug: Unquoted Paths and Names Cause Command Failures

The dotnet add command is constructed using unquoted string interpolation for the project file path and package name. This leads to command execution failures if paths or package names contain spaces, and introduces a command injection vulnerability if package names contain shell metacharacters.

CrossRepoActions/Dotnet.cs#L172-L173

string pre = package.Version.Contains('-') ? "--prerelease" : "";
RunCommand.Execute($"dotnet add {projectFile} package {package.Name} {pre}", new LineOutputHandler(results.Add, results.Add));

Fix in CursorFix in Web


Bug: Missing Flag Causes Incorrect Package Listing

The GetOutdatedProjectDependencies method is missing the --outdated flag in its dotnet list package command. This causes the method to return all packages for a project instead of only the outdated ones, contrary to its intended purpose.

CrossRepoActions/Dotnet.cs#L101-L102

RunCommand.Execute($"dotnet list {projectFile} package --format=json", new LineOutputHandler(results.Add, results.Add));

Fix in CursorFix in Web


Was this report helpful? Give feedback by reacting with 👍 or 👎

@matt-edmondson matt-edmondson moved this to In Progress in ktsu.dev Jul 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

2 participants