Example #1
0
        /// <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);
        }
Example #2
0
        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);
        }
Example #3
0
        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);
        }