Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .git-hooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/bash

FILES=$(git diff --cached --name-only --diff-filter=ACM "*.cs")
if [ -n "$FILES" ]
then
dotnet format ./pgutil.slnx --no-restore --include $FILES
echo "$FILES" | xargs git add
fi
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -360,3 +360,6 @@ MigrationBackup/
# Fody - auto-generated XML schema
FodyWeavers.xsd
/pgutil/Properties/launchSettings.json

# Rider
.idea/*
18 changes: 18 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project>
<PropertyGroup>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<Authors>Inedo</Authors>
<Company>Inedo</Company>
<Product>ProGet</Product>
<Copyright>Copyright © Inedo 2026</Copyright>

<PackageProjectUrl>https://github.com/Inedo/pgutil</PackageProjectUrl>
<RepositoryUrl>https://github.com/Inedo/pgutil.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageTags>proget</PackageTags>
</PropertyGroup>
</Project>
13 changes: 13 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>

<ItemGroup>
<PackageVersion Include="Inedo.ConsoleMan" Version="1.0.2" />
<PackageVersion Include="Microsoft.Extensions.FileSystemGlobbing" Version="10.0.5" />
<PackageVersion Include="System.Linq.Async" Version="7.0.0" />
<PackageVersion Include="Tomlyn" Version="2.3.0" />
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
</ItemGroup>
</Project>
2 changes: 1 addition & 1 deletion Inedo.ProGet/BuildInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public sealed class BuildInfo
{
// Build version number (e.g. "1.0.0")
public required string Version { get; init; }

// Indicates whether the build is active
// * Default is "true"
// * Value is either "true" or "false"
Expand Down
2 changes: 1 addition & 1 deletion Inedo.ProGet/BuildIssue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public sealed class BuildIssue

// A list of reasons for the issue (e.g. "Vulnerability (PGV-2422512); No license detected")
public string? Detail { get; set; }

// The package URL of the package associated with the issue (e.g. "pkg:npm/express@4.18.2")
[JsonPropertyName("purl")]
public required string PUrl { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion Inedo.ProGet/FeedStorageType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public sealed class FeedStorageType

// Name of the feed storage (e.g. "Local Disk", "Amazon S3")
public string? Name { get; init; }

// Description of the feed storage type (e.g. "Local file system path or network share.")
public string? Description { get; init; }

Expand Down
30 changes: 9 additions & 21 deletions Inedo.ProGet/Inedo.ProGet.csproj
Original file line number Diff line number Diff line change
@@ -1,31 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAotCompatible>true</IsAotCompatible>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>CS1591</NoWarn>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/Inedo/pgutil</PackageProjectUrl>
<RepositoryUrl>https://github.com/Inedo/pgutil.git</RepositoryUrl>
<Authors>Inedo</Authors>
<Company>Inedo</Company>
<Product>ProGet</Product>
<Copyright>Copyright © Inedo 2026</Copyright>
<PackageTags>proget</PackageTags>
<RepositoryType>git</RepositoryType>
<Description>A HTTP Client that wraps ProGet's HTTP Endpoints.
<Description>A HTTP Client that wraps ProGet's HTTP Endpoints.

This is essentially the .NET-library version of pgutil, and is built from the pgutil GitHub code/repository.
This is essentially the .NET-library version of pgutil, and is built from the pgutil GitHub code/repository.

To learn how to use the library, see [Getting Started with Inedo.NuGet](https://docs.inedo.com/docs/proget-reference-api#net-library-nuget-package).</Description>
To learn how to use the library, see [Getting Started with Inedo.NuGet](https://docs.inedo.com/docs/proget-reference-api#net-library-nuget-package).</Description>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)'=='net8.0'">
<PackageReference Include="System.Linq.Async" Version="7.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)'=='net8.0'">
<PackageReference Include="System.Linq.Async" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Tomlyn" Version="2.3.0" />
<PackageReference Include="YamlDotNet" Version="16.3.0" />
<PackageReference Include="Tomlyn" />
<PackageReference Include="YamlDotNet" />
</ItemGroup>
</Project>
</Project>
2 changes: 1 addition & 1 deletion Inedo.ProGet/PackageMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public sealed class PackageMetadataVulnerabilityAssessment

// Severity of the assessment (E.g. "W", "E")
public required string Severity { get; init; }

// Indicates whether the vulnerability is blocked
// * Values are either "true" or "false"
public bool Blocked { get; init; }
Expand Down
20 changes: 10 additions & 10 deletions Inedo.ProGet/PackageVersionInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,34 +38,34 @@ public sealed class PackageVersionInfo
// PUrl of the package (see https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst)
[JsonPropertyName("purl")]
public required string PUrl { get; init; }

// Group of the package identifier if the package type supports groups
public string? Group { get; init; }

// Unique name of the package
public required string Name { get; init; }

// Version of the package
public required string Version { get; init; }

// Additional fields specific to the type of package (see https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst)
public string? Qualifier { get; init; }

// Total number of downloads of all versions of the package from the ProGet feed
public long TotalDownloads { get; init; }

// Number of downloads of the latest version of the package from the ProGet feed
public long Downloads { get; init; }

// Timestamp when the package was published to ProGet
public DateTime Published { get; init; }

// User which published the package to ProGet; this information may not always be available
public string? PublishedBy { get; init; }

// Size of the package file in bytes
public long Size { get; init; }

// Indicates whether the package is visible to searches (assume true when not specified)
public bool Listed { get; init; } = true;

Expand Down
10 changes: 5 additions & 5 deletions Inedo.ProGet/ProGetAuthenticationType.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
namespace Inedo.ProGet;

public enum ProGetAuthenticationType
{
None,
ApiKey,
UsernamePassword
public enum ProGetAuthenticationType
{
None,
ApiKey,
UsernamePassword
}
2 changes: 1 addition & 1 deletion Inedo.ProGet/ProGetClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public async Task<ProGetHealthInfo> GetInstanceHealthAsync(CancellationToken can
{
using var response = await this.http.GetAsync("health", cancellationToken).ConfigureAwait(false);
using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);

try
{
return (await JsonSerializer.DeserializeAsync(stream, ProGetApiJsonContext.Default.ProGetHealthInfo, cancellationToken).ConfigureAwait(false))!;
Expand Down
2 changes: 1 addition & 1 deletion Inedo.ProGet/ProGetHealthInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public sealed class ProGetHealthInfo

// Information on status of replication (if configured, else will be null).
public ReplicationStatusInfo? ReplicationStatus { get; init; }

// JSON Object used by ReplicationStatus property
// * Status property values will be either "OK" or "Error" if replication servers exist, else will be null
// * Error properties will return an error message if Status is "Error", else will be null
Expand Down
2 changes: 1 addition & 1 deletion Inedo.ProGet/RetentionRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public sealed class RetentionRule
// * When "false" and "sizeTriggerKb" is set to non-null "n", retention is run when the entire feed size is greater than `n` kilobytes
// * This value is ignored when "sizeTriggerKb" is "null"
public bool SizeExclusive { get; set; }

// When set to "n", the retention rule always keeps versions that have been downloaded more than "n" times
public int? TriggerDownloadCount { get; set; }

Expand Down
1 change: 1 addition & 0 deletions Inedo.ProGet/ScaPermissionInfo.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
namespace Inedo.ProGet;

public sealed class ScaPermissionInfo
{
public bool CanView { get; init; }
Expand Down
2 changes: 1 addition & 1 deletion Inedo.ProGet/Scan/BomWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public void AddPackage(string? group, string name, string version, string type,

var fullName = string.IsNullOrEmpty(group) ? Uri.EscapeDataString(name) : $"{Uri.EscapeDataString(group)}/{Uri.EscapeDataString(name)}";

this.writer.WriteElementString("purl", ns, $"pkg:{type}/{fullName}@{version}{(string.IsNullOrWhiteSpace(qualifier) ? string.Empty : ("?"+qualifier))}");
this.writer.WriteElementString("purl", ns, $"pkg:{type}/{fullName}@{version}{(string.IsNullOrWhiteSpace(qualifier) ? string.Empty : ("?" + qualifier))}");

this.writer.WriteEndElement(); // component
}
Expand Down
7 changes: 4 additions & 3 deletions Inedo.ProGet/Scan/ComposerDependencyScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Inedo.DependencyScan;

namespace Inedo.ProGet.Scan;

internal class ComposerDependencyScanner(CreateDependencyScannerArgs args) : DependencyScanner(args)
{
public override DependencyScannerType Type => DependencyScannerType.Composer;
Expand All @@ -24,13 +25,13 @@ public override async Task<IReadOnlyCollection<ScannedProject>> ResolveDependenc
using var lockFileStream = await this.FileSystem.OpenReadAsync(composerLockFile.FullName, cancellationToken).ConfigureAwait(false);
using var doc = await JsonDocument.ParseAsync(lockFileStream, cancellationToken: cancellationToken).ConfigureAwait(false);

if(!doc.RootElement.TryGetProperty("packages", out var packages) && packages.ValueKind != JsonValueKind.Array)
if (!doc.RootElement.TryGetProperty("packages", out var packages) && packages.ValueKind != JsonValueKind.Array)
throw new DependencyScannerException($"composer.lock at {searchDirectory} does not contain any packages");

var dependencies = new List<DependencyPackage>();
dependencies.AddRange(readDependencies(packages));
if(this.CreateArgs.IncludeDevDependencies && doc.RootElement.TryGetProperty("packages-dev", out var devPackages) && devPackages.ValueKind == JsonValueKind.Array)

if (this.CreateArgs.IncludeDevDependencies && doc.RootElement.TryGetProperty("packages-dev", out var devPackages) && devPackages.ValueKind == JsonValueKind.Array)
dependencies.AddRange(readDependencies(devPackages));

projects.Add(new ScannedProject(projectName.GetString()!, dependencies.Distinct()));
Expand Down
18 changes: 9 additions & 9 deletions Inedo.ProGet/Scan/DependencyScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ private static async Task<string> GetImplicitFileAsync(DependencyScannerType sca
{
var files = await SourceFileSystem.Default.FindFilesAsync(folder, "requirements.txt", true, cancellationToken).ToListAsync(cancellationToken);
if (files.Count == 1)
return files[0].FullName;
return files[0].FullName;
if (files.Count > 1)
throw new DependencyScannerException("Multiple requirements.txt files found in directory. Specify which requirements.txt file you would like to scan be using \"--input\" argument.");

Expand All @@ -124,22 +124,22 @@ private static async Task<string> GetImplicitFileAsync(DependencyScannerType sca

private static async Task<(DependencyScannerType scannerType, string filePath)> GetImplicitTypeAsync(ISourceFileSystem fileSystem, string fileName, CancellationToken cancellationToken = default)
{
if(fileSystem.IsDirectoryAsync(fileName))
if (fileSystem.IsDirectoryAsync(fileName))
{
var files = await fileSystem.FindFilesAsync(fileName, "*.slnx", true, cancellationToken).ToListAsync(cancellationToken);
if(files.Count == 1)
if (files.Count == 1)
return (scannerType: DependencyScannerType.NuGet, filePath: files[0].FullName);
else if (files.Count > 1)
throw new DependencyScannerException("Multiple solution files found in directory. Specify which solution file you would like to scan be using \"--input\" argument.");

files = await fileSystem.FindFilesAsync(fileName, "*.sln", true, cancellationToken).ToListAsync(cancellationToken);
if(files.Count == 1)
if (files.Count == 1)
return (scannerType: DependencyScannerType.NuGet, filePath: files[0].FullName);
else if (files.Count > 1)
throw new DependencyScannerException("Multiple solution files found in directory. Specify which solution file you would like to scan be using \"--input\" argument.");

files = await fileSystem.FindFilesAsync(fileName, "*.csproj", true, cancellationToken).ToListAsync(cancellationToken);
if(files.Count == 1)
if (files.Count == 1)
return (scannerType: DependencyScannerType.NuGet, filePath: files[0].FullName);
else if (files.Count > 1)
throw new DependencyScannerException("Multiple project files found in directory. Specify which project file you would like to scan be using \"--input\" argument.");
Expand All @@ -149,15 +149,15 @@ private static async Task<string> GetImplicitFileAsync(DependencyScannerType sca
return (scannerType: DependencyScannerType.Npm, filePath: files[0].FullName);

files = await fileSystem.FindFilesAsync(fileName, "package-lock.json", true, cancellationToken).ToListAsync(cancellationToken);
if(files.Count > 0)
if (files.Count > 0)
return (scannerType: DependencyScannerType.Npm, filePath: fileName);

files = await fileSystem.FindFilesAsync(fileName, "Cargo.lock", true, cancellationToken).ToListAsync(cancellationToken);
if(files.Count > 0)
if (files.Count > 0)
return (scannerType: DependencyScannerType.Cargo, filePath: fileName);

files = await fileSystem.FindFilesAsync(fileName, "composer.lock", true, cancellationToken).ToListAsync(cancellationToken);
if(files.Count > 0)
if (files.Count > 0)
return (scannerType: DependencyScannerType.Composer, filePath: fileName);

files = await fileSystem.FindFilesAsync(fileName, "requirements.txt", true, cancellationToken).ToListAsync(cancellationToken);
Expand Down
8 changes: 4 additions & 4 deletions Inedo.ProGet/Scan/NpmDependencyScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public override async Task<IReadOnlyCollection<ScannedProject>> ResolveDependenc
// Handle pnpm lock file
if (this.SourcePath.EndsWith("pnpm-lock.yaml"))
{
if(!await this.FileSystem.FileExistsAsync(this.SourcePath, cancellationToken))
if (!await this.FileSystem.FileExistsAsync(this.SourcePath, cancellationToken))
throw new FileNotFoundException("The specified pnpm lock file was not found.", this.SourcePath);
using var stream = await this.FileSystem.OpenReadAsync(this.SourcePath, cancellationToken).ConfigureAwait(false);
using var reader = new StreamReader(stream);
Expand All @@ -32,7 +32,7 @@ public override async Task<IReadOnlyCollection<ScannedProject>> ResolveDependenc

if (string.IsNullOrEmpty(projectName))
throw new InvalidOperationException($"Unable to determine project name from package.json in directory: {this.FileSystem.GetDirectoryName(this.SourcePath)}");

var dependencies = ReadPnpmLockFile(rootNode).ToList();
projects.Add(new ScannedProject(projectName, dependencies));
}
Expand All @@ -46,7 +46,7 @@ public override async Task<IReadOnlyCollection<ScannedProject>> ResolveDependenc
? this.FileSystem.GetDirectoryName(this.SourcePath)
: this.SourcePath;

if(await this.FileSystem.FindFilesAsync(searchDirectory, "pnpm-lock.yaml", true, cancellationToken).AnyAsync(cancellationToken: cancellationToken))
if (await this.FileSystem.FindFilesAsync(searchDirectory, "pnpm-lock.yaml", true, cancellationToken).AnyAsync(cancellationToken: cancellationToken))
Console.WriteLine("Warning: pnpm-lock.yaml file detected in the scan directory. To parse pNPM lock files, specify the the pnpm-lock.yaml file in the Source Path argument.");

await foreach (var packageLockFile in this.FileSystem.FindFilesAsync(searchDirectory, "package-lock.json", !this.SourcePath.EndsWith("package-lock.json"), cancellationToken))
Expand All @@ -64,7 +64,7 @@ public override async Task<IReadOnlyCollection<ScannedProject>> ResolveDependenc
return projects;
}


}

private IEnumerable<DependencyPackage> ReadPackageLockFile(JsonDocument doc)
Expand Down
4 changes: 2 additions & 2 deletions Inedo.ProGet/SecurityTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ public sealed class SecurityTask
public required string Name { get; init; }

// Description of the task (e.g. "Allows basic feed access")
public string? Description { get; init; }
public string? Description { get; init; }

// Indicates if the task is feed-scoped ("true") or global ("false")
public bool FeedScoped { get; init; }
public bool FeedScoped { get; init; }

// The task attributes that the task enables (e.g. ["Feeds_ViewFeed", "Feeds_DownloadPackage"])
public required string[] Attributes { get; init; }
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ It's built from the pgutil GitHub code/repository and is mostly a HTTP Client th

To learn how to use the library, see [Getting Started with Inedo.NuGet](https://docs.inedo.com/docs/proget-reference-api#net-library-nuget-package).

# Samples

Sample projects and examples are available in the [`samples`](samples) directory.

## Pull Requests & Issues

We're very open to your feedback and ideas for improving `pgutil` and `Inedo.ProGet`!
Expand Down
6 changes: 6 additions & 0 deletions global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"sdk": {
"version": "10.0.103",
"rollForward": "latestFeature"
}
}
4 changes: 2 additions & 2 deletions pgutil/ApiKeys/Create/SystemCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public static async Task<int> ExecuteAsync(CommandContext context, CancellationT

var apis = apiValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

if (apis.Length == 0
if (apis.Length == 0
|| apis.Contains("full-control") && apis.Length > 1
|| apis.Any(a => a != "feeds" && a != "sca" && a != "sbom-upload" && a != "full-control"))
{
Expand All @@ -66,7 +66,7 @@ private sealed class ApisOption : IConsoleOption
{
public static bool Required => false;
public static string Name => "--apis";
public static string Description =>
public static string Description =>
$"Specifies the individual APIs to give access to when creating a system API key. " +
$"Value is either full-control or a comma-separated list of any combination of: feeds, sca, sbom-upload";
}
Expand Down
Loading