/// <summary> /// Internal helper to allow other tasks to check for poisoned files. /// </summary> /// <param name="initialCandidates">Initial queue of candidate files (will be cleared when done)</param> /// <param name="catalogedPackagesFilePath">File path to the file hash catalog</param> /// <param name="markerFileName">Marker file name to check for in poisoned nupkgs</param> /// <returns>List of poisoned packages and files found and reasons for each</returns> internal IEnumerable <PoisonedFileEntry> GetPoisonedFiles(IEnumerable <string> initialCandidates, string catalogedPackagesFilePath, string markerFileName) { IEnumerable <CatalogPackageEntry> catalogedPackages = ReadCatalog(catalogedPackagesFilePath); var poisons = new List <PoisonedFileEntry>(); var candidateQueue = new Queue <string>(initialCandidates); // avoid collisions between nupkgs with the same name var dirCounter = 0; if (!string.IsNullOrWhiteSpace(OverrideTempPath)) { Directory.CreateDirectory(OverrideTempPath); } var tempDirName = Path.GetRandomFileName(); var tempDir = Directory.CreateDirectory(Path.Combine(OverrideTempPath ?? Path.GetTempPath(), tempDirName)); while (candidateQueue.Any()) { var checking = candidateQueue.Dequeue(); // if this is a zip or NuPkg, extract it, check for the poison marker, and // add its contents to the list to be checked. if (ZipFileExtensions.Concat(TarFileExtensions).Concat(TarGzFileExtensions).Any(e => checking.ToLowerInvariant().EndsWith(e))) { var tempCheckingDir = Path.Combine(tempDir.FullName, Path.GetRandomFileName(), Path.GetFileNameWithoutExtension(checking) + "." + (++dirCounter).ToString()); PoisonedFileEntry result = ExtractAndCheckZipFileOnly(catalogedPackages, checking, markerFileName, tempCheckingDir, candidateQueue); if (result != null) { poisons.Add(result); } } else { PoisonedFileEntry result = CheckSingleFile(catalogedPackages, tempDir.FullName, checking); if (result != null) { poisons.Add(result); } } } tempDir.Delete(true); return(poisons); }
private static PoisonedFileEntry ExtractAndCheckZipFileOnly(IEnumerable <CatalogPackageEntry> catalogedPackages, string zipToCheck, string markerFileName, string tempDir, Queue <string> futureFilesToCheck) { var poisonEntry = new PoisonedFileEntry(); poisonEntry.Path = zipToCheck; using (var sha = SHA256.Create()) using (var stream = File.OpenRead(zipToCheck)) { poisonEntry.Hash = sha.ComputeHash(stream); } // first check for a matching poisoned or non-poisoned hash match: // - non-poisoned is a potential error where the package was redownloaded. // - poisoned is a use of a local package we were not expecting. foreach (var matchingCatalogedPackage in catalogedPackages.Where(c => c.OriginalHash.SequenceEqual(poisonEntry.Hash) || (c.PoisonedHash?.SequenceEqual(poisonEntry.Hash) ?? false))) { poisonEntry.Type |= PoisonType.Hash; var match = new PoisonMatch { Package = matchingCatalogedPackage.Path, PackageId = matchingCatalogedPackage.Id, PackageVersion = matchingCatalogedPackage.Version, }; poisonEntry.Matches.Add(match); } // now extract and look for the marker file if (ZipFileExtensions.Any(e => zipToCheck.ToLowerInvariant().EndsWith(e))) { ZipFile.ExtractToDirectory(zipToCheck, tempDir); } else if (TarFileExtensions.Any(e => zipToCheck.ToLowerInvariant().EndsWith(e))) { Directory.CreateDirectory(tempDir); var psi = new ProcessStartInfo("tar", $"xf {zipToCheck} -C {tempDir}"); Process.Start(psi).WaitForExit(); } else if (TarGzFileExtensions.Any(e => zipToCheck.ToLowerInvariant().EndsWith(e))) { Directory.CreateDirectory(tempDir); var psi = new ProcessStartInfo("tar", $"xzf {zipToCheck} -C {tempDir}"); Process.Start(psi).WaitForExit(); } else { throw new ArgumentOutOfRangeException($"Don't know how to decompress {zipToCheck}"); } if (!string.IsNullOrWhiteSpace(markerFileName) && File.Exists(Path.Combine(tempDir, markerFileName))) { poisonEntry.Type |= PoisonType.NupkgFile; } foreach (var child in Directory.EnumerateFiles(tempDir, "*", SearchOption.AllDirectories)) { // also add anything in this zip/package for checking futureFilesToCheck.Enqueue(child); } return(poisonEntry.Type != PoisonType.None ? poisonEntry : null); }
private static PoisonedFileEntry CheckSingleFile(IEnumerable <CatalogPackageEntry> catalogedPackages, string rootPath, string fileToCheck) { // skip some common files that get copied verbatim from nupkgs - LICENSE, _._, etc as well as // file types that we never care about - text files, .gitconfig, etc. if (FileNamesToSkip.Any(f => Path.GetFileName(fileToCheck).ToLowerInvariant() == f.ToLowerInvariant()) || FileExtensionsToSkip.Any(e => Path.GetExtension(fileToCheck).ToLowerInvariant() == e.ToLowerInvariant())) { return(null); } var poisonEntry = new PoisonedFileEntry(); poisonEntry.Path = Utility.MakeRelativePath(fileToCheck, rootPath); // There seems to be some weird issues with using file streams both for hashing and assembly loading. // Copy everything into a memory stream to avoid these problems. var memStream = new MemoryStream(); using (var stream = File.OpenRead(fileToCheck)) { stream.CopyTo(memStream); } memStream.Seek(0, SeekOrigin.Begin); using (var sha = SHA256.Create()) { poisonEntry.Hash = sha.ComputeHash(memStream); } foreach (var p in catalogedPackages) { // This hash can match either the original hash (we couldn't poison the file, or redownloaded it) or // the poisoned hash (the obvious failure case of a poisoned file leaked). foreach (var matchingCatalogedFile in p.Files.Where(f => f.OriginalHash.SequenceEqual(poisonEntry.Hash) || (f.PoisonedHash?.SequenceEqual(poisonEntry.Hash) ?? false))) { poisonEntry.Type |= PoisonType.Hash; var match = new PoisonMatch { File = matchingCatalogedFile.Path, Package = p.Path, PackageId = p.Id, PackageVersion = p.Version, }; poisonEntry.Matches.Add(match); } } try { memStream.Seek(0, SeekOrigin.Begin); using (var asm = AssemblyDefinition.ReadAssembly(memStream)) { foreach (var a in asm.CustomAttributes) { foreach (var ca in a.ConstructorArguments) { if (ca.Type.Name == asm.MainModule.TypeSystem.String.Name) { if (ca.Value.ToString().Contains(PoisonMarker)) { poisonEntry.Type |= PoisonType.AssemblyAttribute; } } } } } } catch { // this is fine, it's just not an assembly. } return(poisonEntry.Type != PoisonType.None ? poisonEntry : null); }