From c8b8b1fe47431e2631ef24fdf890a107a95bd736 Mon Sep 17 00:00:00 2001 From: Eilon Lipton Date: Mon, 22 Dec 2014 15:17:14 -0800 Subject: [PATCH] Initial commit --- .gitignore | 66 +++---- NuGetPackageVerifier.sln | 22 +++ NuGetPackageVerifier/App.config | 6 + .../AssemblyHasDocumentFileRule.cs | 39 ++++ .../AssemblyStrongNameRule.cs | 181 ++++++++++++++++++ .../AuthenticodeSigningRule.cs | 82 ++++++++ NuGetPackageVerifier/IPackageVerifierRule.cs | 12 ++ NuGetPackageVerifier/IssueIgnore.cs | 10 + NuGetPackageVerifier/IssueProcessor.cs | 34 ++++ NuGetPackageVerifier/IssueReport.cs | 20 ++ .../Logging/IPackageVerifierLogger.cs | 7 + NuGetPackageVerifier/Logging/LogLevel.cs | 9 + .../Logging/PackageVerifierLogger.cs | 30 +++ .../PackageVerifierLoggerExtensions.cs | 25 +++ .../NuGetPackageVerifier.csproj | 89 +++++++++ NuGetPackageVerifier/PackageAnalyzer.cs | 31 +++ NuGetPackageVerifier/PackageIssue.cs | 45 +++++ NuGetPackageVerifier/PackageIssueFactory.cs | 94 +++++++++ NuGetPackageVerifier/PackageIssueLevel.cs | 8 + .../PowerShellScriptIsSignedRule.cs | 48 +++++ NuGetPackageVerifier/Program.cs | 131 +++++++++++++ .../Properties/AssemblyInfo.cs | 36 ++++ .../RequiredAttributesRule.cs | 46 +++++ NuGetPackageVerifier/SatellitePackageRule.cs | 29 +++ .../StrictSemanticVersionValidationRule.cs | 22 +++ NuGetPackageVerifier/WinTrust.cs | 134 +++++++++++++ NuGetPackageVerifier/packages.config | 5 + 27 files changed, 1219 insertions(+), 42 deletions(-) create mode 100644 NuGetPackageVerifier.sln create mode 100644 NuGetPackageVerifier/App.config create mode 100644 NuGetPackageVerifier/AssemblyHasDocumentFileRule.cs create mode 100644 NuGetPackageVerifier/AssemblyStrongNameRule.cs create mode 100644 NuGetPackageVerifier/AuthenticodeSigningRule.cs create mode 100644 NuGetPackageVerifier/IPackageVerifierRule.cs create mode 100644 NuGetPackageVerifier/IssueIgnore.cs create mode 100644 NuGetPackageVerifier/IssueProcessor.cs create mode 100644 NuGetPackageVerifier/IssueReport.cs create mode 100644 NuGetPackageVerifier/Logging/IPackageVerifierLogger.cs create mode 100644 NuGetPackageVerifier/Logging/LogLevel.cs create mode 100644 NuGetPackageVerifier/Logging/PackageVerifierLogger.cs create mode 100644 NuGetPackageVerifier/Logging/PackageVerifierLoggerExtensions.cs create mode 100644 NuGetPackageVerifier/NuGetPackageVerifier.csproj create mode 100644 NuGetPackageVerifier/PackageAnalyzer.cs create mode 100644 NuGetPackageVerifier/PackageIssue.cs create mode 100644 NuGetPackageVerifier/PackageIssueFactory.cs create mode 100644 NuGetPackageVerifier/PackageIssueLevel.cs create mode 100644 NuGetPackageVerifier/PowerShellScriptIsSignedRule.cs create mode 100644 NuGetPackageVerifier/Program.cs create mode 100644 NuGetPackageVerifier/Properties/AssemblyInfo.cs create mode 100644 NuGetPackageVerifier/RequiredAttributesRule.cs create mode 100644 NuGetPackageVerifier/SatellitePackageRule.cs create mode 100644 NuGetPackageVerifier/StrictSemanticVersionValidationRule.cs create mode 100644 NuGetPackageVerifier/WinTrust.cs create mode 100644 NuGetPackageVerifier/packages.config diff --git a/.gitignore b/.gitignore index 96374c4..79f538b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,43 +1,25 @@ -# Windows image file caches -Thumbs.db -ehthumbs.db +*.ide +[Oo]bj/ +[Bb]in/ +TestResults/ +.nuget/ +_ReSharper.*/ +/packages +artifacts/ +PublishProfiles/ +*.user +*.suo +*.cache +*.docstates +_ReSharper.* +nuget.exe +*.psess +*.vsp +*.pidb +*.userprefs +*DS_Store +*.ncrunchsolution +*.*sdf +*.ipch +/*.vspx -# Folder config file -Desktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msm -*.msp - -# Windows shortcuts -*.lnk - -# ========================= -# Operating System Files -# ========================= - -# OSX -# ========================= - -.DS_Store -.AppleDouble -.LSOverride - -# Thumbnails -._* - -# Files that might appear on external disk -.Spotlight-V100 -.Trashes - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk diff --git a/NuGetPackageVerifier.sln b/NuGetPackageVerifier.sln new file mode 100644 index 0000000..ba4fda5 --- /dev/null +++ b/NuGetPackageVerifier.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.22310.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGetPackageVerifier", "NuGetPackageVerifier\NuGetPackageVerifier.csproj", "{2ED95FF6-1941-4053-99CE-F9748048762C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2ED95FF6-1941-4053-99CE-F9748048762C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2ED95FF6-1941-4053-99CE-F9748048762C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2ED95FF6-1941-4053-99CE-F9748048762C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2ED95FF6-1941-4053-99CE-F9748048762C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/NuGetPackageVerifier/App.config b/NuGetPackageVerifier/App.config new file mode 100644 index 0000000..d0feca6 --- /dev/null +++ b/NuGetPackageVerifier/App.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/NuGetPackageVerifier/AssemblyHasDocumentFileRule.cs b/NuGetPackageVerifier/AssemblyHasDocumentFileRule.cs new file mode 100644 index 0000000..8c8e00f --- /dev/null +++ b/NuGetPackageVerifier/AssemblyHasDocumentFileRule.cs @@ -0,0 +1,39 @@ +using NuGet; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NuGetPackageVerifier.Logging; + +namespace NuGetPackageVerifier +{ + public class AssemblyHasDocumentFileRule : IPackageVerifierRule + { + public IEnumerable Validate(IPackageRepository packageRepo, IPackage package, IPackageVerifierLogger logger) + { + if (!package.IsSatellitePackage()) + { + IEnumerable allXmlFiles = + from file in package.GetLibFiles() + select file.Path into path + where path.EndsWith(".xml", StringComparison.OrdinalIgnoreCase) + select path; + + foreach (IPackageFile current in package.GetLibFiles()) + { + string assemblyPath = current.Path; + // TODO: Does this need to check for just managed code? + if (assemblyPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) + { + string docFilePath = Path.ChangeExtension(assemblyPath, ".xml"); + if (!allXmlFiles.Contains(docFilePath, StringComparer.OrdinalIgnoreCase)) + { + yield return PackageIssueFactory.AssemblyHasNoDocFile(assemblyPath); + } + } + } + } + yield break; + } + } +} diff --git a/NuGetPackageVerifier/AssemblyStrongNameRule.cs b/NuGetPackageVerifier/AssemblyStrongNameRule.cs new file mode 100644 index 0000000..6475955 --- /dev/null +++ b/NuGetPackageVerifier/AssemblyStrongNameRule.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using NuGet; +using NuGetPackageVerifier.Logging; + +namespace NuGetPackageVerifier +{ + public class AssemblyStrongNameRule : IPackageVerifierRule + { + public IEnumerable Validate(IPackageRepository packageRepo, IPackage package, IPackageVerifierLogger logger) + { + foreach (IPackageFile currentFile in package.GetFiles()) + { + string extension = Path.GetExtension(currentFile.Path); + if (extension.Equals(".dll", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".exe", StringComparison.OrdinalIgnoreCase)) + { + string assemblyPath = Path.ChangeExtension(Path.Combine(Path.GetTempPath(), Path.GetTempFileName()), extension); + var isManagedCode = false; + var isStrongNameSigned = false; + int hresult = 0; + try + { + using (Stream packageFileStream = currentFile.GetStream()) + { + var _assemblyBytes = new byte[packageFileStream.Length]; + packageFileStream.Read(_assemblyBytes, 0, _assemblyBytes.Length); + + using (var fileStream = new FileStream(assemblyPath, FileMode.Create)) + { + packageFileStream.Seek(0, SeekOrigin.Begin); + packageFileStream.CopyTo(fileStream); + fileStream.Flush(true); + } + + if (IsAssemblyManaged(assemblyPath)) + { + isManagedCode = true; + var clrStrongName = (IClrStrongName)RuntimeEnvironment.GetRuntimeInterfaceAsObject(new Guid("B79B0ACD-F5CD-409b-B5A5-A16244610B92"), new Guid("9FD93CCF-3280-4391-B3A9-96E1CDE77C8D")); + bool verificationForced; + hresult = clrStrongName.StrongNameSignatureVerificationEx(assemblyPath, true, out verificationForced); + if (hresult == 0) + { + isStrongNameSigned = true; + } + } + } + } + catch (Exception ex) + { + logger.LogError("Error while verifying strong name signature for {0}: {1}", currentFile.Path, ex.Message); + } + finally + { + if (File.Exists(assemblyPath)) + { + File.Delete(assemblyPath); + } + } + if (isManagedCode && !isStrongNameSigned) + { + yield return PackageIssueFactory.AssemblyNotStrongNameSigned(currentFile.Path, hresult); + } + } + } + yield break; + } + + private static bool IsAssemblyManaged(string assemblyPath) + { + // From http://msdn.microsoft.com/en-us/library/ms173100.aspx + try + { + var testAssembly = AssemblyName.GetAssemblyName(assemblyPath); + return true; + } + catch (FileNotFoundException) + { + // The file cannot be found + } + catch (BadImageFormatException) + { + // The file is not an assembly + } + catch (FileLoadException) + { + // The assembly has already been loaded + } + return false; + } + + [ComConversionLoss, Guid("9FD93CCF-3280-4391-B3A9-96E1CDE77C8D"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [ComImport] + internal interface IClrStrongName + { + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + int GetHashFromAssemblyFile([MarshalAs(UnmanagedType.LPStr)] [In] string pszFilePath, [MarshalAs(UnmanagedType.U4)] [In] [Out] ref int piHashAlg, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] [Out] byte[] pbHash, [MarshalAs(UnmanagedType.U4)] [In] int cchHash, [MarshalAs(UnmanagedType.U4)] out int pchHash); + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + int GetHashFromAssemblyFileW([MarshalAs(UnmanagedType.LPWStr)] [In] string pwzFilePath, [MarshalAs(UnmanagedType.U4)] [In] [Out] ref int piHashAlg, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] [Out] byte[] pbHash, [MarshalAs(UnmanagedType.U4)] [In] int cchHash, [MarshalAs(UnmanagedType.U4)] out int pchHash); + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + int GetHashFromBlob([In] IntPtr pbBlob, [MarshalAs(UnmanagedType.U4)] [In] int cchBlob, [MarshalAs(UnmanagedType.U4)] [In] [Out] ref int piHashAlg, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 4)] [Out] byte[] pbHash, [MarshalAs(UnmanagedType.U4)] [In] int cchHash, [MarshalAs(UnmanagedType.U4)] out int pchHash); + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + int GetHashFromFile([MarshalAs(UnmanagedType.LPStr)] [In] string pszFilePath, [MarshalAs(UnmanagedType.U4)] [In] [Out] ref int piHashAlg, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] [Out] byte[] pbHash, [MarshalAs(UnmanagedType.U4)] [In] int cchHash, [MarshalAs(UnmanagedType.U4)] out int pchHash); + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + int GetHashFromFileW([MarshalAs(UnmanagedType.LPWStr)] [In] string pwzFilePath, [MarshalAs(UnmanagedType.U4)] [In] [Out] ref int piHashAlg, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] [Out] byte[] pbHash, [MarshalAs(UnmanagedType.U4)] [In] int cchHash, [MarshalAs(UnmanagedType.U4)] out int pchHash); + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + int GetHashFromHandle([In] IntPtr hFile, [MarshalAs(UnmanagedType.U4)] [In] [Out] ref int piHashAlg, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] [Out] byte[] pbHash, [MarshalAs(UnmanagedType.U4)] [In] int cchHash, [MarshalAs(UnmanagedType.U4)] out int pchHash); + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + [return: MarshalAs(UnmanagedType.U4)] + int StrongNameCompareAssemblies([MarshalAs(UnmanagedType.LPWStr)] [In] string pwzAssembly1, [MarshalAs(UnmanagedType.LPWStr)] [In] string pwzAssembly2, [MarshalAs(UnmanagedType.U4)] out int dwResult); + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + int StrongNameFreeBuffer([In] IntPtr pbMemory); + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + int StrongNameGetBlob([MarshalAs(UnmanagedType.LPWStr)] [In] string pwzFilePath, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] [Out] byte[] pbBlob, [MarshalAs(UnmanagedType.U4)] [In] [Out] ref int pcbBlob); + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + int StrongNameGetBlobFromImage([In] IntPtr pbBase, [MarshalAs(UnmanagedType.U4)] [In] int dwLength, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] [Out] byte[] pbBlob, [MarshalAs(UnmanagedType.U4)] [In] [Out] ref int pcbBlob); + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + int StrongNameGetPublicKey([MarshalAs(UnmanagedType.LPWStr)] [In] string pwzKeyContainer, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] [In] byte[] pbKeyBlob, [MarshalAs(UnmanagedType.U4)] [In] int cbKeyBlob, out IntPtr ppbPublicKeyBlob, [MarshalAs(UnmanagedType.U4)] out int pcbPublicKeyBlob); + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + [return: MarshalAs(UnmanagedType.U4)] + int StrongNameHashSize([MarshalAs(UnmanagedType.U4)] [In] int ulHashAlg, [MarshalAs(UnmanagedType.U4)] out int cbSize); + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + int StrongNameKeyDelete([MarshalAs(UnmanagedType.LPWStr)] [In] string pwzKeyContainer); + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + int StrongNameKeyGen([MarshalAs(UnmanagedType.LPWStr)] [In] string pwzKeyContainer, [MarshalAs(UnmanagedType.U4)] [In] int dwFlags, out IntPtr ppbKeyBlob, [MarshalAs(UnmanagedType.U4)] out int pcbKeyBlob); + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + int StrongNameKeyGenEx([MarshalAs(UnmanagedType.LPWStr)] [In] string pwzKeyContainer, [MarshalAs(UnmanagedType.U4)] [In] int dwFlags, [MarshalAs(UnmanagedType.U4)] [In] int dwKeySize, out IntPtr ppbKeyBlob, [MarshalAs(UnmanagedType.U4)] out int pcbKeyBlob); + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + int StrongNameKeyInstall([MarshalAs(UnmanagedType.LPWStr)] [In] string pwzKeyContainer, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] [In] byte[] pbKeyBlob, [MarshalAs(UnmanagedType.U4)] [In] int cbKeyBlob); + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + int StrongNameSignatureGeneration([MarshalAs(UnmanagedType.LPWStr)] [In] string pwzFilePath, [MarshalAs(UnmanagedType.LPWStr)] [In] string pwzKeyContainer, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] [In] byte[] pbKeyBlob, [MarshalAs(UnmanagedType.U4)] [In] int cbKeyBlob, [In] [Out] IntPtr ppbSignatureBlob, [MarshalAs(UnmanagedType.U4)] out int pcbSignatureBlob); + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + int StrongNameSignatureGenerationEx([MarshalAs(UnmanagedType.LPWStr)] [In] string wszFilePath, [MarshalAs(UnmanagedType.LPWStr)] [In] string wszKeyContainer, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] [In] byte[] pbKeyBlob, [MarshalAs(UnmanagedType.U4)] [In] int cbKeyBlob, [In] [Out] IntPtr ppbSignatureBlob, [MarshalAs(UnmanagedType.U4)] out int pcbSignatureBlob, [MarshalAs(UnmanagedType.U4)] [In] int dwFlags); + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + int StrongNameSignatureSize([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] [In] byte[] pbPublicKeyBlob, [MarshalAs(UnmanagedType.U4)] [In] int cbPublicKeyBlob, [MarshalAs(UnmanagedType.U4)] out int pcbSize); + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + [return: MarshalAs(UnmanagedType.U4)] + int StrongNameSignatureVerification([MarshalAs(UnmanagedType.LPWStr)] [In] string pwzFilePath, [MarshalAs(UnmanagedType.U4)] [In] int dwInFlags, [MarshalAs(UnmanagedType.U4)] out int dwOutFlags); + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + [return: MarshalAs(UnmanagedType.U4)] + int StrongNameSignatureVerificationEx([MarshalAs(UnmanagedType.LPWStr)] [In] string pwzFilePath, [MarshalAs(UnmanagedType.I1)] [In] bool fForceVerification, [MarshalAs(UnmanagedType.I1)] out bool fWasVerified); + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + [return: MarshalAs(UnmanagedType.U4)] + int StrongNameSignatureVerificationFromImage([In] IntPtr pbBase, [MarshalAs(UnmanagedType.U4)] [In] int dwLength, [MarshalAs(UnmanagedType.U4)] [In] int dwInFlags, [MarshalAs(UnmanagedType.U4)] out int dwOutFlags); + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + int StrongNameTokenFromAssembly([MarshalAs(UnmanagedType.LPWStr)] [In] string pwzFilePath, out IntPtr ppbStrongNameToken, [MarshalAs(UnmanagedType.U4)] out int pcbStrongNameToken); + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + int StrongNameTokenFromAssemblyEx([MarshalAs(UnmanagedType.LPWStr)] [In] string pwzFilePath, out IntPtr ppbStrongNameToken, [MarshalAs(UnmanagedType.U4)] out int pcbStrongNameToken, out IntPtr ppbPublicKeyBlob, [MarshalAs(UnmanagedType.U4)] out int pcbPublicKeyBlob); + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)] + int StrongNameTokenFromPublicKey([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] [In] byte[] pbPublicKeyBlob, [MarshalAs(UnmanagedType.U4)] [In] int cbPublicKeyBlob, out IntPtr ppbStrongNameToken, [MarshalAs(UnmanagedType.U4)] out int pcbStrongNameToken); + } + } +} diff --git a/NuGetPackageVerifier/AuthenticodeSigningRule.cs b/NuGetPackageVerifier/AuthenticodeSigningRule.cs new file mode 100644 index 0000000..fd2d408 --- /dev/null +++ b/NuGetPackageVerifier/AuthenticodeSigningRule.cs @@ -0,0 +1,82 @@ +using NuGet; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using NuGetPackageVerifier.Logging; + +namespace NuGetPackageVerifier +{ + public class AuthenticodeSigningRule : IPackageVerifierRule + { + public IEnumerable Validate(IPackageRepository packageRepo, IPackage package, IPackageVerifierLogger logger) + { + string packagePath = packageRepo.Source + "\\" + package.Id + "." + package.Version.ToString() + ".nupkg"; + string nupkgWithoutExt = Path.Combine(Path.GetDirectoryName(packagePath), Path.GetFileNameWithoutExtension(packagePath)); + try + { + UnzipPackage(nupkgWithoutExt); + + foreach (IPackageFile current in package.GetFiles()) + { + //string packagePath = package.FileSystem.Root + "\\" + Id + "." + Version + ".nupkg" + string extension = Path.GetExtension(current.Path); + + // TODO: Need to add more extensions? + if (extension.Equals(".dll", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".exe", StringComparison.OrdinalIgnoreCase)) + { + string pathOfFileToScan = Path.Combine(nupkgWithoutExt, current.Path); + var realAssemblyPath = pathOfFileToScan; + if (!File.Exists(realAssemblyPath)) + { + realAssemblyPath = pathOfFileToScan.Replace("+", "%2B"); + if (!File.Exists(realAssemblyPath)) + { + logger.LogError("The assembly '{0}' in this package can't be found (a bug in this tool, most likely).", current.Path); + continue; + } + } + bool isAuthenticodeSigned = WinTrust.IsAuthenticodeSigned(realAssemblyPath); + if (!isAuthenticodeSigned) + { + yield return PackageIssueFactory.PEFileNotAuthenticodeSigned(current.Path); + } + } + } + } + finally + { + CleanUpFolder(nupkgWithoutExt, logger); + } + + yield break; + } + + private void UnzipPackage(string path) + { + if (Directory.Exists(path)) + { + Directory.Delete(path, recursive: true); + } + Directory.CreateDirectory(path); + + ZipFile.ExtractToDirectory(path + ".nupkg", path); + } + + private void CleanUpFolder(string path, IPackageVerifierLogger logger) + { + try + { + if (Directory.Exists(path)) + { + Directory.Delete(path, recursive: true); + } + } + catch (Exception ex) + { + logger.LogWarning("Couldn't clean temp unzip folder: " + ex.Message); + } + } + } +} diff --git a/NuGetPackageVerifier/IPackageVerifierRule.cs b/NuGetPackageVerifier/IPackageVerifierRule.cs new file mode 100644 index 0000000..8457f43 --- /dev/null +++ b/NuGetPackageVerifier/IPackageVerifierRule.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using NuGet; +using NuGetPackageVerifier.Logging; + +namespace NuGetPackageVerifier +{ + public interface IPackageVerifierRule + { + IEnumerable Validate(IPackageRepository packageRepo, IPackage package, IPackageVerifierLogger logger); + } +} + diff --git a/NuGetPackageVerifier/IssueIgnore.cs b/NuGetPackageVerifier/IssueIgnore.cs new file mode 100644 index 0000000..6ecc4d1 --- /dev/null +++ b/NuGetPackageVerifier/IssueIgnore.cs @@ -0,0 +1,10 @@ +namespace NuGetPackageVerifier +{ + public class IssueIgnore + { + public string PackageId { get; set; } + public string IssueId { get; set; } + public string Instance { get; set; } + public string Justification { get; set; } + } +} diff --git a/NuGetPackageVerifier/IssueProcessor.cs b/NuGetPackageVerifier/IssueProcessor.cs new file mode 100644 index 0000000..3b42ca3 --- /dev/null +++ b/NuGetPackageVerifier/IssueProcessor.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NuGet; + +namespace NuGetPackageVerifier +{ + public class IssueProcessor + { + public IssueProcessor(IEnumerable issuesToIgnore) + { + IssuesToIgnore = issuesToIgnore; + } + + public IEnumerable IssuesToIgnore + { + get; + private set; + } + + public IssueReport GetIssueReport(MyPackageIssue packageIssue, IPackage package) + { + var ignoredRules = IssuesToIgnore.Where( + issueIgnore => + string.Equals(issueIgnore.IssueId, packageIssue.IssueId, StringComparison.OrdinalIgnoreCase) && + string.Equals(issueIgnore.Instance, packageIssue.Instance, StringComparison.OrdinalIgnoreCase) && + string.Equals(issueIgnore.PackageId, package.Id, StringComparison.OrdinalIgnoreCase)); + + var firstIgnoreRule = ignoredRules.FirstOrDefault(); + + return new IssueReport(packageIssue, firstIgnoreRule != null, firstIgnoreRule?.Justification); + } + } +} diff --git a/NuGetPackageVerifier/IssueReport.cs b/NuGetPackageVerifier/IssueReport.cs new file mode 100644 index 0000000..897de2a --- /dev/null +++ b/NuGetPackageVerifier/IssueReport.cs @@ -0,0 +1,20 @@ +using NuGetPackageVerifier.Logging; + +namespace NuGetPackageVerifier +{ + public class IssueReport + { + public IssueReport(MyPackageIssue packageIssue, bool ignore, string ignoreJustification) + { + PackageIssue = packageIssue; + IssueLevel = ignore ? LogLevel.Info : packageIssue.Level == MyPackageIssueLevel.Warning ? LogLevel.Warning : LogLevel.Error; + IgnoreJustification = ignoreJustification; + } + + public MyPackageIssue PackageIssue { get; private set; } + + public LogLevel IssueLevel { get; private set; } + + public string IgnoreJustification { get; set; } + } +} diff --git a/NuGetPackageVerifier/Logging/IPackageVerifierLogger.cs b/NuGetPackageVerifier/Logging/IPackageVerifierLogger.cs new file mode 100644 index 0000000..73c0dd9 --- /dev/null +++ b/NuGetPackageVerifier/Logging/IPackageVerifierLogger.cs @@ -0,0 +1,7 @@ +namespace NuGetPackageVerifier.Logging +{ + public interface IPackageVerifierLogger + { + void Log(LogLevel logLevel, string message); + } +} diff --git a/NuGetPackageVerifier/Logging/LogLevel.cs b/NuGetPackageVerifier/Logging/LogLevel.cs new file mode 100644 index 0000000..f1b2012 --- /dev/null +++ b/NuGetPackageVerifier/Logging/LogLevel.cs @@ -0,0 +1,9 @@ +namespace NuGetPackageVerifier.Logging +{ + public enum LogLevel + { + Info, + Warning, + Error, + } +} diff --git a/NuGetPackageVerifier/Logging/PackageVerifierLogger.cs b/NuGetPackageVerifier/Logging/PackageVerifierLogger.cs new file mode 100644 index 0000000..bad369a --- /dev/null +++ b/NuGetPackageVerifier/Logging/PackageVerifierLogger.cs @@ -0,0 +1,30 @@ +using System; + +namespace NuGetPackageVerifier.Logging +{ + public class PackageVerifierLogger : IPackageVerifierLogger + { + public void Log(LogLevel logLevel, string message) + { + ConsoleColor foreColor; + switch (logLevel) + { + case LogLevel.Error: + foreColor = ConsoleColor.Red; + break; + + case LogLevel.Warning: + foreColor = ConsoleColor.Yellow; + break; + + default: + case LogLevel.Info: + foreColor = ConsoleColor.Gray; + break; + } + Console.ForegroundColor = foreColor; + Console.WriteLine("{0}: {1}", logLevel.ToString().ToUpperInvariant(), message); + Console.ForegroundColor = ConsoleColor.Gray; + } + } +} diff --git a/NuGetPackageVerifier/Logging/PackageVerifierLoggerExtensions.cs b/NuGetPackageVerifier/Logging/PackageVerifierLoggerExtensions.cs new file mode 100644 index 0000000..504039c --- /dev/null +++ b/NuGetPackageVerifier/Logging/PackageVerifierLoggerExtensions.cs @@ -0,0 +1,25 @@ +namespace NuGetPackageVerifier.Logging +{ + public static class PackageVerifierLoggerExtensions + { + public static void Log(this IPackageVerifierLogger logger, LogLevel logLevel, string message, params object[] args) + { + logger.Log(logLevel, string.Format(message, args)); + } + + public static void LogWarning(this IPackageVerifierLogger logger, string message, params object[] args) + { + logger.Log(LogLevel.Warning, message, args); + } + + public static void LogError(this IPackageVerifierLogger logger, string message, params object[] args) + { + logger.Log(LogLevel.Error, message, args); + } + + public static void LogInfo(this IPackageVerifierLogger logger, string message, params object[] args) + { + logger.Log(LogLevel.Info, message, args); + } + } +} diff --git a/NuGetPackageVerifier/NuGetPackageVerifier.csproj b/NuGetPackageVerifier/NuGetPackageVerifier.csproj new file mode 100644 index 0000000..737bb4a --- /dev/null +++ b/NuGetPackageVerifier/NuGetPackageVerifier.csproj @@ -0,0 +1,89 @@ + + + + + Debug + AnyCPU + {2ED95FF6-1941-4053-99CE-F9748048762C} + Exe + Properties + NuGetPackageVerifier + NuGetPackageVerifier + v4.5.1 + 512 + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Microsoft.Web.Xdt.2.1.0\lib\net40\Microsoft.Web.XmlTransform.dll + + + False + ..\packages\NuGet.Core.2.8.3\lib\net40-Client\NuGet.Core.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/NuGetPackageVerifier/PackageAnalyzer.cs b/NuGetPackageVerifier/PackageAnalyzer.cs new file mode 100644 index 0000000..c0bfee4 --- /dev/null +++ b/NuGetPackageVerifier/PackageAnalyzer.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using NuGet; +using NuGetPackageVerifier.Logging; + +namespace NuGetPackageVerifier +{ + public class PackageAnalyzer + { + private IList _rules = new List(); + + public IList Rules + { + get + { + return _rules; + } + } + + public IEnumerable AnalyzePackage(IPackageRepository packageRepo, IPackage package, IPackageVerifierLogger logger) + { + IEnumerable packageIssues = new List(); + foreach (var rule in Rules) + { + var issues = rule.Validate(packageRepo, package, logger).ToList(); + packageIssues = packageIssues.Concat(issues); + } + return packageIssues; + } + } +} diff --git a/NuGetPackageVerifier/PackageIssue.cs b/NuGetPackageVerifier/PackageIssue.cs new file mode 100644 index 0000000..0b9d5f4 --- /dev/null +++ b/NuGetPackageVerifier/PackageIssue.cs @@ -0,0 +1,45 @@ +namespace NuGetPackageVerifier +{ + public class MyPackageIssue + { + public MyPackageIssue(string issueId, string issue, MyPackageIssueLevel level) + : this(issueId, instance: null, issue: issue, level: level) + { + } + + public MyPackageIssue(string issueId, string instance, string issue, MyPackageIssueLevel level) + { + Instance = instance; + IssueId = issueId; + Issue = issue; + Level = level; + } + + public MyPackageIssueLevel Level + { + get; + private set; + } + + public string Issue + { + get; + private set; + } + + public string IssueId + { + get; private set; + } + + public string Instance + { + get; set; + } + + public override string ToString() + { + return string.Format("{0} @ {1}: {2}: {3}", IssueId, Instance, Level.ToString().ToUpperInvariant(), Issue); + } + } +} diff --git a/NuGetPackageVerifier/PackageIssueFactory.cs b/NuGetPackageVerifier/PackageIssueFactory.cs new file mode 100644 index 0000000..ab271cf --- /dev/null +++ b/NuGetPackageVerifier/PackageIssueFactory.cs @@ -0,0 +1,94 @@ +using NuGet; + +namespace NuGetPackageVerifier +{ + public static class PackageIssueFactory + { + public static MyPackageIssue AssemblyNotStrongNameSigned(string assemblyPath, int hResult) + { + // TODO: Translate common HRESULTS http://blogs.msdn.com/b/yizhang/ + return new MyPackageIssue("SIGN_STRONGNAME", assemblyPath, string.Format("The managed assembly '{0}' in this package is either not signed or is delay signed. HRESULT=0x{1:X}", assemblyPath, hResult), MyPackageIssueLevel.Error); + } + + public static MyPackageIssue NotSemanticVersion(SemanticVersion version) + { + return new MyPackageIssue("VERSION_NOTSEMANTIC", + string.Format("Version '{0}' does not follow semantic versioning guidelines.", version), MyPackageIssueLevel.Error); + } + + public static MyPackageIssue Satellite_PackageSummaryNotLocalized() + { + return new MyPackageIssue("LOC_SUMMARY", "Package summary is not localized correctly", MyPackageIssueLevel.Error); + } + + public static MyPackageIssue Satellite_PackageTitleNotLocalized() + { + return new MyPackageIssue("LOC_TITLE", "Package title is not localized correctly", MyPackageIssueLevel.Error); + } + + public static MyPackageIssue Satellite_PackageDescriptionNotLocalized() + { + return new MyPackageIssue("LOC_DESC", "Package description is not localized correctly", MyPackageIssueLevel.Error); + } + + public static MyPackageIssue RequiredCopyright() + { + return RequiredCore("NUSPEC_COPYRIGHT", "Copyright", MyPackageIssueLevel.Error); + } + + public static MyPackageIssue RequiredLicenseUrl() + { + return RequiredCore("NUSPEC_LICENSEURL", "License Url", MyPackageIssueLevel.Error); + } + + public static MyPackageIssue RequiredIconUrl() + { + return RequiredCore("NUSPEC_ICONURL", "Icon Url", MyPackageIssueLevel.Warning); + } + + public static MyPackageIssue RequiredTags() + { + return RequiredCore("NUSPEC_TAGS", "Tags", MyPackageIssueLevel.Warning); + } + + public static MyPackageIssue RequiredTitle() + { + return RequiredCore("NUSPEC_TITLE", "Title", MyPackageIssueLevel.Error); + } + + public static MyPackageIssue RequiredSummary() + { + return RequiredCore("NUSPEC_SUMMARY", "Summary", MyPackageIssueLevel.Warning); + } + + public static MyPackageIssue RequiredProjectUrl() + { + return RequiredCore("NUSPEC_PROJECTURL", "Project Url", MyPackageIssueLevel.Warning); + } + + public static MyPackageIssue RequiredRequireLicenseAcceptanceTrue() + { + return new MyPackageIssue("NUSPEC_ACCEPTLICENSE", string.Format("NuSpec Require License Acceptance is not set to true"), MyPackageIssueLevel.Error); + } + + private static MyPackageIssue RequiredCore(string issueId, string attributeName, MyPackageIssueLevel issueLevel) + { + return new MyPackageIssue(issueId, string.Format("NuSpec {0} attribute is missing", attributeName), issueLevel); + } + + public static MyPackageIssue PowerShellScriptNotSigned(string scriptPath) + { + return new MyPackageIssue("SIGN_POWERSHELL", scriptPath, string.Format("The PowerShell script '{0}' is not signed.", scriptPath), MyPackageIssueLevel.Error); + } + + public static MyPackageIssue PEFileNotAuthenticodeSigned(string assemblyPath) + { + return new MyPackageIssue("SIGN_AUTHENTICODE", assemblyPath, string.Format("The PE file '{0}' in this package is not authenticode signed.", assemblyPath), MyPackageIssueLevel.Error); + } + + public static MyPackageIssue AssemblyHasNoDocFile(string assemblyPath) + { + return new MyPackageIssue("DOC_MISSING", assemblyPath, string.Format("The assembly '{0}' doesn't have a corresponding XML document file.", assemblyPath), MyPackageIssueLevel.Warning); + } + } +} diff --git a/NuGetPackageVerifier/PackageIssueLevel.cs b/NuGetPackageVerifier/PackageIssueLevel.cs new file mode 100644 index 0000000..8e30544 --- /dev/null +++ b/NuGetPackageVerifier/PackageIssueLevel.cs @@ -0,0 +1,8 @@ +namespace NuGetPackageVerifier +{ + public enum MyPackageIssueLevel + { + Warning, + Error + } +} diff --git a/NuGetPackageVerifier/PowerShellScriptIsSignedRule.cs b/NuGetPackageVerifier/PowerShellScriptIsSignedRule.cs new file mode 100644 index 0000000..fa8f29a --- /dev/null +++ b/NuGetPackageVerifier/PowerShellScriptIsSignedRule.cs @@ -0,0 +1,48 @@ +using NuGet; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NuGetPackageVerifier.Logging; + +namespace NuGetPackageVerifier +{ + public class PowerShellScriptIsSignedRule : IPackageVerifierRule + { + private static readonly string[] PowerShellExtensions = new string[] + { + ".ps1", + ".psm1", + ".psd1", + ".ps1xml" + }; + + public IEnumerable Validate(IPackageRepository packageRepo, IPackage package, IPackageVerifierLogger logger) + { + foreach (IPackageFile current in package.GetFiles()) + { + string extension = Path.GetExtension(current.Path); + if (PowerShellScriptIsSignedRule.PowerShellExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + { + if (!VerifySigned(current)) + { + yield return PackageIssueFactory.PowerShellScriptNotSigned(current.Path); + } + } + } + yield break; + } + + private static bool VerifySigned(IPackageFile packageFile) + { + bool result; + using (Stream stream = packageFile.GetStream()) + { + System.IO.StreamReader streamReader = new System.IO.StreamReader(stream); + string text = streamReader.ReadToEnd(); + result = (text.IndexOf("# SIG # Begin signature block", StringComparison.OrdinalIgnoreCase) > -1 && text.IndexOf("# SIG # End signature block", StringComparison.OrdinalIgnoreCase) > -1); + } + return result; + } + } +} diff --git a/NuGetPackageVerifier/Program.cs b/NuGetPackageVerifier/Program.cs new file mode 100644 index 0000000..ec846a5 --- /dev/null +++ b/NuGetPackageVerifier/Program.cs @@ -0,0 +1,131 @@ +using System; +using System.Diagnostics; +using System.Linq; +using NuGet; +using NuGetPackageVerifier.Logging; + +namespace NuGetPackageVerifier +{ + public static class Program + { + private const int ReturnOk = 0; + private const int ReturnBadArgs = 1; + private const int ReturnErrorsOrWarnings = 2; + + public static int Main(string[] args) + { + // TODO: Take a switch saying whether to use TeamCity logger + + // TODO: Add a way to read ignores from file + + if (args.Length != 1) + { + Console.WriteLine(@"USAGE: NuGetSuperBVT.exe c:\path\to\packages"); + return ReturnBadArgs; + } + + var totalTimeStopWatch = Stopwatch.StartNew(); + + var nupkgsPath = args[0]; + + var issuesToIgnore = new[] + { + new IssueIgnore { IssueId = "NUSPEC_TAGS", Instance = null, PackageId = "EntityFramework", Justification = "Because no tags" }, + new IssueIgnore { IssueId = "NUSPEC_SUMMARY", Instance = null, PackageId = "EntityFramework", Justification = "Because no summary" }, + new IssueIgnore { IssueId = "XYZ", Instance = "ZZZ", PackageId = "ABC", Justification = "Because" }, + new IssueIgnore { IssueId = "XYZ", Instance = "ZZZ", PackageId = "ABC", Justification = "Because" }, + new IssueIgnore { IssueId = "XYZ", Instance = "ZZZ", PackageId = "ABC", Justification = "Because" }, + new IssueIgnore { IssueId = "XYZ", Instance = "ZZZ", PackageId = "ABC", Justification = "Because" }, + }; + var issueProcessor = new IssueProcessor(issuesToIgnore); + + var analyzer = new PackageAnalyzer(); + analyzer.Rules.Add(new AssemblyHasDocumentFileRule()); + analyzer.Rules.Add(new AssemblyStrongNameRule()); + analyzer.Rules.Add(new AuthenticodeSigningRule()); + analyzer.Rules.Add(new PowerShellScriptIsSignedRule()); + analyzer.Rules.Add(new RequiredAttributesRule()); + analyzer.Rules.Add(new SatellitePackageRule()); + analyzer.Rules.Add(new StrictSemanticVersionValidationRule()); + + var logger = new PackageVerifierLogger(); + + // TODO: Switch this to a custom IFileSystem that has only the packages we want (maybe?) + var localPackageRepo = new LocalPackageRepository(nupkgsPath); + + var numPackagesInRepo = localPackageRepo.GetPackages().Count(); + logger.LogInfo("Found {0} packages in {1}", numPackagesInRepo, nupkgsPath); + + bool anyErrorOrWarnings = false; + + foreach (var package in localPackageRepo.GetPackages()) + { + var packageTimeStopWatch = Stopwatch.StartNew(); + + logger.LogInfo("Analyzing {0} ({1})", package.Id, package.Version); + var issues = analyzer.AnalyzePackage(localPackageRepo, package, logger).ToList(); + + var issuesToReport = issues.Select(issue => issueProcessor.GetIssueReport(issue, package)).ToList(); + + if (issuesToReport.Count > 0) + { + var infos = issuesToReport.Where(issueReport => issueReport.IssueLevel == LogLevel.Info).ToList(); + var warnings = issuesToReport.Where(issueReport => issueReport.IssueLevel == LogLevel.Warning).ToList(); + var errors = issuesToReport.Where(issueReport => issueReport.IssueLevel == LogLevel.Error).ToList(); + + LogLevel errorLevel = LogLevel.Info; + if (warnings.Count > 0) + { + errorLevel = LogLevel.Warning; + anyErrorOrWarnings = true; + } + if (errors.Count > 0) + { + errorLevel = LogLevel.Error; + anyErrorOrWarnings = true; + } + logger.Log( + errorLevel, + "{0} error(s), {1} warning(s), and {2} info(s) found with package {3} ({4})", + errors.Count, warnings.Count, infos.Count, package.Id, package.Version); + + foreach (var current in issuesToReport) + { + PrintPackageIssue(logger, current); + } + } + else + { + logger.LogInfo("No issues found with package {0}", package.Id, package.Version); + } + + packageTimeStopWatch.Stop(); + logger.LogInfo("Took {0}ms", packageTimeStopWatch.ElapsedMilliseconds); + Console.WriteLine(); + } + + totalTimeStopWatch.Stop(); + logger.LogInfo("Total took {0}ms", totalTimeStopWatch.ElapsedMilliseconds); + + + return anyErrorOrWarnings ? ReturnErrorsOrWarnings : ReturnOk; + } + + private static void PrintPackageIssue(IPackageVerifierLogger logger, IssueReport issue) + { + // TODO: Support this: https://confluence.jetbrains.com/display/TCD8/Build+Script+Interaction+with+TeamCity + + var issueInfo = issue.PackageIssue.IssueId; + if (issue.PackageIssue.Instance == null) + { + issueInfo += " @ " + issue.PackageIssue.Instance; + } + + logger.Log(issue.IssueLevel, "{0}: {1}", issueInfo, issue.PackageIssue.Issue); + if (issue.IgnoreJustification != null) + { + logger.Log(issue.IssueLevel, "Justification: {0}", issue.IgnoreJustification); + } + } + } +} diff --git a/NuGetPackageVerifier/Properties/AssemblyInfo.cs b/NuGetPackageVerifier/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..f309fc4 --- /dev/null +++ b/NuGetPackageVerifier/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("NuGetSuperBVT")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("NuGetSuperBVT")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("2ed95ff6-1941-4053-99ce-f9748048762c")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/NuGetPackageVerifier/RequiredAttributesRule.cs b/NuGetPackageVerifier/RequiredAttributesRule.cs new file mode 100644 index 0000000..afbd0b5 --- /dev/null +++ b/NuGetPackageVerifier/RequiredAttributesRule.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using NuGet; +using NuGetPackageVerifier.Logging; + +namespace NuGetPackageVerifier +{ + public class RequiredAttributesRule : IPackageVerifierRule + { + public IEnumerable Validate(IPackageRepository packageRepo, IPackage package, IPackageVerifierLogger logger) + { + if (string.IsNullOrEmpty(package.Copyright)) + { + yield return PackageIssueFactory.RequiredCopyright(); + } + if (package.LicenseUrl == null) + { + yield return PackageIssueFactory.RequiredLicenseUrl(); + } + if (package.IconUrl == null) + { + yield return PackageIssueFactory.RequiredIconUrl(); + } + if (string.IsNullOrEmpty(package.Tags)) + { + yield return PackageIssueFactory.RequiredTags(); + } + if (string.IsNullOrEmpty(package.Title)) + { + yield return PackageIssueFactory.RequiredTitle(); + } + if (string.IsNullOrEmpty(package.Summary)) + { + yield return PackageIssueFactory.RequiredSummary(); + } + if (package.ProjectUrl == null) + { + yield return PackageIssueFactory.RequiredProjectUrl(); + } + if (!package.RequireLicenseAcceptance) + { + yield return PackageIssueFactory.RequiredRequireLicenseAcceptanceTrue(); + } + yield break; + } + } +} diff --git a/NuGetPackageVerifier/SatellitePackageRule.cs b/NuGetPackageVerifier/SatellitePackageRule.cs new file mode 100644 index 0000000..64c7ac8 --- /dev/null +++ b/NuGetPackageVerifier/SatellitePackageRule.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using NuGet; +using NuGetPackageVerifier.Logging; + +namespace NuGetPackageVerifier +{ + public class SatellitePackageRule : IPackageVerifierRule + { + public IEnumerable Validate(IPackageRepository packageRepo, IPackage package, IPackageVerifierLogger logger) + { + if (package.IsSatellitePackage()) + { + if (package.Summary.Contains("{")) + { + yield return PackageIssueFactory.Satellite_PackageSummaryNotLocalized(); + } + if (package.Title.Contains("{")) + { + yield return PackageIssueFactory.Satellite_PackageTitleNotLocalized(); + } + if (package.Description.Contains("{")) + { + yield return PackageIssueFactory.Satellite_PackageDescriptionNotLocalized(); + } + } + yield break; + } + } +} diff --git a/NuGetPackageVerifier/StrictSemanticVersionValidationRule.cs b/NuGetPackageVerifier/StrictSemanticVersionValidationRule.cs new file mode 100644 index 0000000..d9de664 --- /dev/null +++ b/NuGetPackageVerifier/StrictSemanticVersionValidationRule.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using NuGet; +using NuGetPackageVerifier.Logging; + +namespace NuGetPackageVerifier +{ + public class StrictSemanticVersionValidationRule : IPackageVerifierRule + { + public IEnumerable Validate(IPackageRepository packageRepo, IPackage package, IPackageVerifierLogger logger) + { + SemanticVersion semanticVersion; + if (SemanticVersion.TryParseStrict(package.Version.ToString(), out semanticVersion)) + { + yield break; + } + else + { + yield return PackageIssueFactory.NotSemanticVersion(package.Version); + } + } + } +} diff --git a/NuGetPackageVerifier/WinTrust.cs b/NuGetPackageVerifier/WinTrust.cs new file mode 100644 index 0000000..4bc42c5 --- /dev/null +++ b/NuGetPackageVerifier/WinTrust.cs @@ -0,0 +1,134 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace NuGetPackageVerifier +{ + // See http://msdn.microsoft.com/en-us/library/aa388205(v=VS.85).aspx + public class WinTrust + { + // The GUID action ID for using the AuthentiCode policy provider (see softpub.h) + private const string WINTRUST_ACTION_GENERIC_VERIFY_V2 = "{00AAC56B-CD44-11d0-8CC2-00C04FC295EE}"; + + // See wintrust.h + enum UIChoice + { + WTD_UI_ALL = 1, + WTD_UI_NONE, + WTD_UI_NOBAD, + WTD_UI_NOGOOD + } + + enum RevocationChecks + { + WTD_REVOKE_NONE, + WTD_REVOKE_WHOLECHAIN + } + + enum UnionChoice + { + WTD_CHOICE_FILE = 1, + WTD_CHOICE_CATALOG, + WTD_CHOICE_BLOB, + WTD_CHOICE_SIGNER, + WTD_CHOICE_CERT + } + + enum StateAction + { + WTD_STATEACTION_IGNORE, + WTD_STATEACTION_VERIFY, + WTD_STATEACTION_CLOSE, + WTD_STATEACTION_AUTO_CACHE, + WTD_STATEACTION_AUTO_CACHE_FLUSH + } + + enum Provider + { + WTD_USE_IE4_TRUST_FLAG = 0x00000001, + WTD_NO_IE4_CHAIN_FLAG = 0x00000002, + WTD_NO_POLICY_USAGE_FLAG = 0x00000004, + WTD_REVOCATION_CHECK_NONE = 0x00000010, + WTD_REVOCATION_CHECK_END_CERT = 0x00000020, + WTD_REVOCATION_CHECK_CHAIN = 0x00000040, + WTD_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT = 0x00000080, + WTD_SAFER_FLAG = 0x00000100, + WTD_HASH_ONLY_FLAG = 0x00000200, + WTD_USE_DEFAULT_OSVER_CHECK = 0x00000400, + WTD_LIFETIME_SIGNING_FLAG = 0x00000800, + WTD_CACHE_ONLY_URL_RETRIEVAL = 0x00001000 + } + + [StructLayout(LayoutKind.Sequential)] + struct WinTrustData + { + public uint cbStruct; + public IntPtr pPolicyCallbackData; + public IntPtr pSIPClientData; + public uint dwUIChoice; + public uint fdwRevocationChecks; + public uint dwUnionChoice; + public IntPtr pFile; // We're not interested in other union members + public uint dwStateAction; + public IntPtr hWVTStateData; + public IntPtr pwszURLReference; + public uint dwProvFlags; + public uint dwUIContext; + } + + [StructLayout(LayoutKind.Sequential)] + struct WinTrustFileInfo + { + public uint cbStruct; + [MarshalAs(UnmanagedType.LPTStr)] + public string pcwszFilePath; + public IntPtr hFile; + public IntPtr pgKnownSubject; + } + + public static bool IsAuthenticodeSigned(string path) + { + WinTrustFileInfo fileInfo = new WinTrustFileInfo() + { + cbStruct = (uint)Marshal.SizeOf(typeof(WinTrustFileInfo)), + pcwszFilePath = Path.GetFullPath(path), + hFile = IntPtr.Zero, + pgKnownSubject = IntPtr.Zero + }; + + WinTrustData data = new WinTrustData() + { + cbStruct = (uint)Marshal.SizeOf(typeof(WinTrustData)), + dwProvFlags = Convert.ToUInt32(Provider.WTD_SAFER_FLAG), + dwStateAction = Convert.ToUInt32(StateAction.WTD_STATEACTION_IGNORE), + dwUIChoice = Convert.ToUInt32(UIChoice.WTD_UI_NONE), + dwUIContext = 0, + dwUnionChoice = Convert.ToUInt32(UnionChoice.WTD_CHOICE_FILE), + fdwRevocationChecks = Convert.ToUInt32(RevocationChecks.WTD_REVOKE_NONE), + hWVTStateData = IntPtr.Zero, + pFile = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(WinTrustFileInfo))), + pPolicyCallbackData = IntPtr.Zero, + pSIPClientData = IntPtr.Zero, + pwszURLReference = IntPtr.Zero + }; + + // TODO: Potential memory leak. Need to invetigate + Marshal.StructureToPtr(fileInfo, data.pFile, false); + + IntPtr pGuid = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Guid))); + IntPtr pData = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(WinTrustData))); + Marshal.StructureToPtr(data, pData, true); + Marshal.StructureToPtr(new Guid(WINTRUST_ACTION_GENERIC_VERIFY_V2), pGuid, true); + + uint result = WinVerifyTrust(IntPtr.Zero, pGuid, pData); + + Marshal.FreeHGlobal(pGuid); + Marshal.FreeHGlobal(pData); + + return result == 0; + } + + [DllImport("wintrust.dll", SetLastError = true)] + internal static extern uint WinVerifyTrust(IntPtr hWnd, IntPtr pgActionID, IntPtr pWinTrustData); + } +} diff --git a/NuGetPackageVerifier/packages.config b/NuGetPackageVerifier/packages.config new file mode 100644 index 0000000..9507c8f --- /dev/null +++ b/NuGetPackageVerifier/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file