Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Closed
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
1 change: 1 addition & 0 deletions Inedo.ProGet/Inedo.ProGet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ To learn how to use the library, see [Getting Started with Inedo.NuGet](https://
<ItemGroup>
<PackageReference Include="System.Linq.Async" Version="6.0.3" />
<PackageReference Include="Tomlyn" Version="0.19.0" />
<PackageReference Include="YamlDotNet" Version="16.2.1" />
</ItemGroup>
</Project>
8 changes: 8 additions & 0 deletions Inedo.ProGet/Scan/DependencyScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public static async Task<DependencyScanner> GetScannerAsync(CreateDependencyScan
{
DependencyScannerType.NuGet => new NuGetDependencyScanner(args),
DependencyScannerType.Npm => new NpmDependencyScanner(args),
DependencyScannerType.Pnpm => new PnpmDependencyScanner(args),
DependencyScannerType.PyPI => new PypiDependencyScanner(args),
DependencyScannerType.Conda => new CondaDependencyScanner(args),
DependencyScannerType.Cargo => new CargoDependencyScanner(args),
Expand All @@ -91,6 +92,8 @@ private static async Task<string> GetImplicitFileAsync(DependencyScannerType sca
{
if (scannerType == DependencyScannerType.Npm)
return folder;
else if (scannerType == DependencyScannerType.Pnpm)
return folder;
else if (scannerType == DependencyScannerType.Cargo)
return folder;
else if (scannerType == DependencyScannerType.Composer)
Expand Down Expand Up @@ -148,6 +151,10 @@ private static async Task<string> GetImplicitFileAsync(DependencyScannerType sca
if(files.Count > 0)
return (scannerType: DependencyScannerType.Npm, filePath: fileName);

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

files = await fileSystem.FindFilesAsync(fileName, "Cargo.lock", true, cancellationToken).ToListAsync(cancellationToken);
if(files.Count > 0)
return (scannerType: DependencyScannerType.Cargo, filePath: fileName);
Expand All @@ -173,6 +180,7 @@ private static async Task<string> GetImplicitFileAsync(DependencyScannerType sca
".slnx" or ".sln" or ".csproj" => (DependencyScannerType.NuGet, fileName),
".toml" => (DependencyScannerType.Cargo, fileName),
".lock" => Path.GetFileName(fileName).Equals("Cargo.lock", StringComparison.OrdinalIgnoreCase) ? (DependencyScannerType.Cargo, fileName) : (DependencyScannerType.Composer, fileName),
".yaml" => Path.GetFileName(fileName).Equals("pnpm-lock.yaml", StringComparison.OrdinalIgnoreCase) ? (DependencyScannerType.Pnpm, fileName) : (DependencyScannerType.Auto, fileName),
".json" => Path.GetFileName(fileName).Equals("composer.json", StringComparison.OrdinalIgnoreCase) ? (DependencyScannerType.Composer, fileName) : (DependencyScannerType.Npm, fileName),
_ => Path.GetFileName(fileName).Equals("requirements.txt", StringComparison.OrdinalIgnoreCase) ? (getPythonScannerType(fileName), fileName) : (DependencyScannerType.Auto, fileName)
};
Expand Down
4 changes: 4 additions & 0 deletions Inedo.ProGet/Scan/DependencyScannerType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ public enum DependencyScannerType
/// </summary>
Npm,
/// <summary>
/// pnpm.
/// </summary>
Pnpm,
/// <summary>
/// PyPI.
/// </summary>
PyPI,
Expand Down
159 changes: 159 additions & 0 deletions Inedo.ProGet/Scan/PnpmDependencyScanner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
using YamlDotNet.RepresentationModel;

namespace Inedo.DependencyScan;

internal sealed class PnpmDependencyScanner(CreateDependencyScannerArgs args) : DependencyScanner(args)
{
private readonly bool includeDevDependencies = args.IncludeDevDependencies;

public override DependencyScannerType Type => DependencyScannerType.Pnpm;

public override async Task<IReadOnlyCollection<ScannedProject>> ResolveDependenciesAsync(CancellationToken cancellationToken = default)
{
var projects = new List<ScannedProject>();
var searchDirectory = (await this.FileSystem.FileExistsAsync(this.SourcePath, cancellationToken))
? this.FileSystem.GetDirectoryName(this.SourcePath)
: this.SourcePath;

await foreach (var pnpmLockFile in this.FileSystem.FindFilesAsync(searchDirectory, "pnpm-lock.yaml", !this.SourcePath.EndsWith("pnpm-lock.yaml"), cancellationToken))
{
using var stream = await this.FileSystem.OpenReadAsync(pnpmLockFile.FullName, cancellationToken).ConfigureAwait(false);
using var reader = new StreamReader(stream);

var yaml = new YamlStream();
yaml.Load(reader);

if (yaml.Documents.Count > 0 && yaml.Documents[0].RootNode is YamlMappingNode rootNode)
{
// Try to get project name from package.json in the same directory
var projectName = await GetProjectNameAsync(pnpmLockFile.FullName, cancellationToken).ConfigureAwait(false);

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

var dependencies = ReadPnpmLockFile(rootNode).ToList();
projects.Add(new ScannedProject(projectName, dependencies));
}
}

return projects;
}

private async Task<string?> GetProjectNameAsync(string pnpmLockPath, CancellationToken cancellationToken)
{
try
{
var directory = this.FileSystem.GetDirectoryName(pnpmLockPath);
var packageJsonPath = Path.Combine(directory, "package.json");

if (await this.FileSystem.FileExistsAsync(packageJsonPath, cancellationToken).ConfigureAwait(false))
{
using var stream = await this.FileSystem.OpenReadAsync(packageJsonPath, cancellationToken).ConfigureAwait(false);
using var doc = await System.Text.Json.JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false);

if (doc.RootElement.TryGetProperty("name", out var nameProperty))
{
return nameProperty.GetString();
}
}
}
catch
{
// Ignore errors when reading package.json
// Later a meaningful exception will be thrown, when the project name is empty
}

return null;
}

private IEnumerable<DependencyPackage> ReadPnpmLockFile(YamlMappingNode rootNode)
{
// Find the "packages" node
var packagesKey = new YamlScalarNode("packages");
if (!rootNode.Children.TryGetValue(packagesKey, out var packagesNode) || packagesNode is not YamlMappingNode packagesMapping)
yield break;

foreach (var package in packagesMapping.Children)
{
if (package.Key is not YamlScalarNode keyNode || package.Value is not YamlMappingNode valueNode)
continue;

// Package key format in pnpm-lock.yaml is typically:
// - "/@scope/package@version" or "/package@version" for regular dependencies
// - "/@scope/package@version(peer-deps)" for packages with peer dependencies

var packageKey = keyNode.Value;

// Skip root package
if (string.IsNullOrEmpty(packageKey) || packageKey == ".")
continue;

// Remove leading slash if present
if (packageKey.StartsWith('/'))
packageKey = packageKey[1..];

// Check if it's a dev dependency
var devKey = new YamlScalarNode("dev");
if (valueNode.Children.TryGetValue(devKey, out var devNode) &&
devNode is YamlScalarNode devScalar &&
bool.TryParse(devScalar.Value, out var isDev) &&
isDev && !this.includeDevDependencies)
continue;

// Parse package name and version
var (name, version) = ParsePackageKey(packageKey);

if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(version))
{
yield return CreateNpmDependencyPackage(name, version);
}
}
}

private static (string name, string version) ParsePackageKey(string packageKey)
{
// Remove anything in parentheses (peer dependency info)
var parenIndex = packageKey.IndexOf('(');
if (parenIndex >= 0)
packageKey = packageKey[..parenIndex];

// Find the last @ symbol which separates name from version
// For scoped packages like @scope/[email protected], we need to find the @ after the scope
var lastAtIndex = packageKey.LastIndexOf('@');

if (lastAtIndex <= 0) // No version found or @ is at the start (scoped package without version)
return (packageKey, string.Empty);

// For scoped packages, make sure we don't split on the first @
if (packageKey.StartsWith('@'))
{
// This is a scoped package
var secondAtIndex = packageKey.IndexOf('@', 1);
if (secondAtIndex > 0)
lastAtIndex = secondAtIndex;
}

var name = packageKey[..lastAtIndex];
var version = packageKey[(lastAtIndex + 1)..];

return (name, version);
}

private static DependencyPackage CreateNpmDependencyPackage(string name, string version)
{
// Check for scoped name ("@scopename/packagename")
string? group = null;
var parts = name.Split('/', 2);
if (name.StartsWith('@') && parts.Length == 2)
{
// group = scope name
group = parts[0];
// name = package name
name = parts[1];
}

return new DependencyPackage { Group = group, Name = name, Version = version, Type = "npm" };
}
}
2 changes: 1 addition & 1 deletion pgutil/Vulns/AuditCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ private sealed class PackageTypeOption : IConsoleOption
public static bool Required => false;
public static string Name => "--type";
public static string Description => "Type of package to audit for vulnerabilities";
public static string[] ValidValues => ["apk", "deb", "maven", "nuget", "conda", "cran", "helm", "npm", "pypi", "rpm", "gem"];
public static string[] ValidValues => ["apk", "deb", "maven", "nuget", "conda", "cran", "helm", "npm", "pypi", "rpm", "gem", "pnpm"];
}

private sealed class ProjectOption : IConsoleOption
Expand Down
2 changes: 1 addition & 1 deletion pgutil/Vulns/PackageCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ private sealed class PackageTypeOption : IConsoleOption
public static bool Required => true;
public static string Name => "--type";
public static string Description => "Type of package to audit for vulnerabilities";
public static string[] ValidValues => ["apk", "deb", "maven", "nuget", "conda", "cran", "helm", "npm", "pypi", "rpm", "gem"];
public static string[] ValidValues => ["apk", "deb", "maven", "nuget", "conda", "cran", "helm", "npm", "pypi", "rpm", "gem", "pnpm"];
}

private sealed class PackageOption : IConsoleOption
Expand Down