diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..8e503cbd --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +*/bin/* +*/obj/* +*.dll + +*.pdb + +*.cache + +Src/.idea/.idea.NuGetDefense/.idea/ + +*.blob + +Src/.idea/.idea.NuGetDefense/ diff --git a/Src/NuGetDefense.csproj b/Src/NuGetDefense.csproj new file mode 100644 index 00000000..c47dd8d4 --- /dev/null +++ b/Src/NuGetDefense.csproj @@ -0,0 +1,41 @@ + + + + Exe + netcoreapp3.1 + true + 0.0.1 + NuGetDefense + Curtis Carter + NuGetDefense ~ Check for Known Vulnerabilities at Build + NuGetDefense was inspired by [OWASP SafeNuGet](https://nuget.org/packages/SafeNuGet/) but aims to check with multiple sources for known vulnerabilities. + Curtis Carter 2020 + https://github.com/DigitalCoyote/NuGetDefense + MIT + Initial Release: Support for checking for OSS Vulnerabilities listed on OSS Index + Security + NuGetDefense + NuGetDefense.nuspec + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Src/NuGetDefense.nuspec b/Src/NuGetDefense.nuspec new file mode 100644 index 00000000..efb4a13d --- /dev/null +++ b/Src/NuGetDefense.nuspec @@ -0,0 +1,28 @@ + + + + NuGetDefense + NuGetDefense + 0.0.1 + Curtis Carter + Curtis Carter + https://github.com/DigitalCoyote/NuGetDefense + false + NuGetDefense ~ Check for Known Vulnerabilities at Build + NuGetDefense was inspired by [OWASP SafeNuGet](https://nuget.org/packages/SafeNuGet/) but aims to check with multiple sources for known vulnerabilities. + First version: OSS Index used as initial source for vulnerabilities + + MIT + Security + + + + + + + + + + + + \ No newline at end of file diff --git a/Src/NuGetDefense.sln b/Src/NuGetDefense.sln new file mode 100644 index 00000000..a9dde097 --- /dev/null +++ b/Src/NuGetDefense.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGetDefense", "NuGetDefense.csproj", "{B5542178-4516-45C6-B77D-9AF9C1DCC8EA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B5542178-4516-45C6-B77D-9AF9C1DCC8EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5542178-4516-45C6-B77D-9AF9C1DCC8EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5542178-4516-45C6-B77D-9AF9C1DCC8EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5542178-4516-45C6-B77D-9AF9C1DCC8EA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Src/NuGetPackage.cs b/Src/NuGetPackage.cs new file mode 100644 index 00000000..37550142 --- /dev/null +++ b/Src/NuGetPackage.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Xml.Serialization; + +namespace NuGetDefense +{ + public class NuGetPackage + { + public string Id { get; set; } + + public string Version { get; set; } + + public string PackageUrl => $@"pkg:nuget/{Id}@{Version}"; + public int LinePosition { get; set; } + public int LineNumber { get; set; } + } +} \ No newline at end of file diff --git a/Src/OSSIndex/ComponentReport.cs b/Src/OSSIndex/ComponentReport.cs new file mode 100644 index 00000000..bc6843e3 --- /dev/null +++ b/Src/OSSIndex/ComponentReport.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Serialization; + +namespace NuGetDefense.OSSIndex +{ + public class ComponentReport + { + /// + /// Description of the package + /// + [JsonPropertyName("description")] + public string Description { get; set; } + + /// + /// packageUrl for this package + /// + [JsonPropertyName("coordinates")] + public string Coordinates { get; set; } + + /// + /// Component Details Reference + /// + [JsonPropertyName("reference")] + public string Reference { get; set; } + + [JsonPropertyName("vulnerabilities")] + public ComponentReportVulnerability[] Vulnerabilities { get; set; } + } +} \ No newline at end of file diff --git a/Src/OSSIndex/ComponentReportRequest.cs b/Src/OSSIndex/ComponentReportRequest.cs new file mode 100644 index 00000000..beecb4ea --- /dev/null +++ b/Src/OSSIndex/ComponentReportRequest.cs @@ -0,0 +1,7 @@ +namespace NuGetDefense.OSSIndex +{ + public class ComponentReportRequest + { + public string[] coordinates { get; set; } + } +} \ No newline at end of file diff --git a/Src/OSSIndex/ComponentReportVulnerability.cs b/Src/OSSIndex/ComponentReportVulnerability.cs new file mode 100644 index 00000000..fff2634d --- /dev/null +++ b/Src/OSSIndex/ComponentReportVulnerability.cs @@ -0,0 +1,66 @@ +using System.Text.Json.Serialization; + +namespace NuGetDefense.OSSIndex +{ + /// + /// Vulnerability report for a specific package + /// + public class ComponentReportVulnerability + { + /// + /// public identifier + /// + [JsonPropertyName("id")] + public string Id { get; set; } + + /// + /// Vulnerability Title + /// + [JsonPropertyName("title")] + public string Title { get; set; } + + /// + /// Vulnerability Description + /// + [JsonPropertyName("description")] + public string Description { get; set; } + + /// + /// CVSS score (https://en.wikipedia.org/wiki/Common_Vulnerability_Scoring_System) + /// + [JsonPropertyName("cvssScore")] + public float CvssScore { get; set; } + + /// + /// CVSS vector + /// + [JsonPropertyName("cvssVector")] + public string CvssVector { get; set; } + + /// + /// Common Weakness Enumeration + /// + /// + [JsonPropertyName("cwe")] + public string Cwe { get; set; } + + /// + /// Common Vulnerability Enumeration + /// + [JsonPropertyName("cve")] + public string Cve { get; set; } + + /// + /// Vulnerability details reference + /// + [JsonPropertyName("reference")] + public string Reference { get; set; } + + /// + /// Affected version ranges + /// + /// + [JsonPropertyName("versionRanges")] + public string[] VersionRanges { get; set; } + } +} \ No newline at end of file diff --git a/Src/OSSIndex/RestApi.cs b/Src/OSSIndex/RestApi.cs new file mode 100644 index 00000000..38206339 --- /dev/null +++ b/Src/OSSIndex/RestApi.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using System.Text.Json; + +namespace NuGetDefense.OSSIndex +{ + + /// + /// Handles interaction with the OSS Index Rest API (https://ossindex.sonatype.org/doc/rest) + /// + public static class RestApi + { + private const string ResponseContentType = "application/vnd.ossindex.component-report.v1+json"; + private const string RequestContentType = "application/vnd.ossindex.component-report-request.v1+json"; + + + /// + /// Gets vulnerabilities for a single NuGet Package. + /// + /// NuGetPackage to check + /// + private static async Task GetReportForPackageAsync(NuGetPackage pkg) + { + using (var client = new HttpClient()) + { + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(ResponseContentType)); + var response = await client.GetStringAsync($"https://ossindex.sonatype.org/api/v3/component-report/{pkg.PackageUrl}"); + return JsonSerializer.Deserialize(response, new JsonSerializerOptions()); + } + } + + /// + /// Gets Vulnerabilities for a set of NuGet Packages + /// + /// Packages to Check + /// + private static async Task GetReportsForPackagesAsync(NuGetPackage[] pkgs) + { + using var client = new HttpClient(); + var content = JsonSerializer.Serialize(new ComponentReportRequest(){ coordinates = pkgs.Select(p => p.PackageUrl).ToArray()}); + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(ResponseContentType)); + var response = await client + .PostAsync("https://ossindex.sonatype.org/api/v3/component-report", + new StringContent(content, Encoding.UTF8, RequestContentType)); + return await JsonSerializer.DeserializeAsync(response.Content.ReadAsStreamAsync().Result, new JsonSerializerOptions()); + } + + /// + /// Gets vulnerabilities for a single NuGet Package + /// + /// NuGetPAckage to check + /// + public static ComponentReportVulnerability[] GetVulnerabilitiesForPackage(NuGetPackage pkg) + { + return GetReportForPackageAsync(pkg).Result.Vulnerabilities; + } + + /// + /// Gets Vulnerabilities for a set of NuGet Packages + /// + /// Packages to Check + /// + public static IEnumerable GetVulnerabilitiesForPackages(NuGetPackage[] pkgs) + { + return GetReportsForPackagesAsync(pkgs).Result.Where(report => report.Vulnerabilities.Length > 0); + } + } +} \ No newline at end of file diff --git a/Src/Program.cs b/Src/Program.cs new file mode 100644 index 00000000..49f6adc7 --- /dev/null +++ b/Src/Program.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Xml; +using System.Xml.Linq; +using NuGetDefense.OSSIndex; + +namespace NuGetDefense +{ + class Program + { + /// + /// args[0] is expected to be the path to the project file. + /// + /// + static void Main(string[] args) + { + var exitCode = 0; + var pkgConfig = Path.Combine(Path.GetDirectoryName(args[0]), "packages.config"); + var pkgs = LoadPackages(File.Exists(pkgConfig) ? pkgConfig : args[0]); + + var reports = OSSIndex.RestApi.GetVulnerabilitiesForPackages(pkgs).ToArray(); + foreach (var report in reports) + { + var StdErrWriter = Console.Error; + var pkg = pkgs.First(p => p.PackageUrl == report.Coordinates); + Console.WriteLine("*************************************"); + //Plan to use Warning: for warnings later + //Plan to combine messages into a single Console.Write. + StdErrWriter.WriteLine($"{args[0]}({pkg.LineNumber},{pkg.LinePosition}) : Error : Vulnerabilities found for {pkg.Id} @ {pkg.Version}"); + Console.WriteLine($"Description: {report.Description}"); + Console.WriteLine($"Reference: {report.Reference}"); + foreach (var vulnerability in report.Vulnerabilities) + { + exitCode++; + Console.WriteLine($"Title: {vulnerability.Title}"); + Console.WriteLine($"Description: {vulnerability.Description}"); + Console.WriteLine($"Id: {vulnerability.Id}"); + Console.WriteLine($"CVE: {vulnerability.Cve}"); + Console.WriteLine($"CWE: {vulnerability.Cwe}"); + Console.WriteLine($"CVSS Score: {vulnerability.CvssScore.ToString(CultureInfo.InvariantCulture)}"); + Console.WriteLine($"CVSS Vector: {vulnerability.CvssVector}"); + if(vulnerability.VersionRanges?.Length > 0)Console.Error.WriteLine($"Affected Versions: {vulnerability.VersionRanges}"); + Console.WriteLine("---------------------------"); + } + } + } + + + /// + /// Loads NuGet packages in use form packages.config or PackageReferences in the project file + /// + /// + public static NuGetPackage[] LoadPackages(string packageSource) + { + if(Path.GetFileName(packageSource) == "packages.config") + { + return XElement.Load(packageSource, LoadOptions.SetLineInfo).DescendantsAndSelf("package").Select(x => new NuGetPackage() + {Id = x.Attribute("id").Value, Version = x.Attribute("version").Value, LineNumber = ((IXmlLineInfo)x).LineNumber, LinePosition = ((IXmlLineInfo)x).LinePosition}).ToArray(); + } + + return XElement.Load(packageSource, LoadOptions.SetLineInfo).DescendantsAndSelf("PackageReference").Select(x => new NuGetPackage() + {Id = x.Attribute("Include").Value, Version = x.Attribute("Version").Value, LineNumber = ((IXmlLineInfo)x).LineNumber, LinePosition = ((IXmlLineInfo)x).LinePosition}).ToArray(); + } + } +} \ No newline at end of file diff --git a/Src/TestFiles/packages.config b/Src/TestFiles/packages.config new file mode 100644 index 00000000..acd74bf4 --- /dev/null +++ b/Src/TestFiles/packages.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Src/build/nugetdefense.targets b/Src/build/nugetdefense.targets new file mode 100644 index 00000000..8f06f4f0 --- /dev/null +++ b/Src/build/nugetdefense.targets @@ -0,0 +1,11 @@ + + + + + $(MSBuildThisFileDirectory)../tools/netcoreapp3.1/NuGetDefense + + + + + + \ No newline at end of file diff --git a/Src/lib/net461/_._ b/Src/lib/net461/_._ new file mode 100644 index 00000000..e69de29b diff --git a/Src/lib/netcoreapp3.1/_._ b/Src/lib/netcoreapp3.1/_._ new file mode 100644 index 00000000..e69de29b diff --git a/Src/lib/netstandard2.0/_._ b/Src/lib/netstandard2.0/_._ new file mode 100644 index 00000000..e69de29b