File: SignCheck.cs
Web Access
Project: src\src\SignCheck\SignCheck\Microsoft.DotNet.SignCheck.csproj (Microsoft.DotNet.SignCheck)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using CommandLine;
using Microsoft.SignCheck.Logging;
using Microsoft.SignCheck.Verification;
 
namespace SignCheck
{
    internal class SignCheck
    {
        private static readonly char[] _wildcards = new char[] { '*', '?' };
 
        // Location where files can be downloaded
        private static readonly string _appData = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "SignCheck");
 
        internal List<string> _inputFiles;
 
        internal Exclusions Exclusions
        {
            get;
            set;
        }
 
        internal bool HasArgErrors
        {
            get;
            set;
        }
 
        internal bool LoggedResults
        {
            get;
            set;
        }
 
        internal string[] ResultDetails
        {
            get;
            set;
        }
 
        internal IEnumerable<string> InputFiles
        {
            get
            {
                if (_inputFiles == null)
                {
                    _inputFiles = GetInputFilesFromOptions();
                }
                return _inputFiles;
            }
        }
 
        public FileStatus FileStatus
        {
            get;
            set;
        }
 
        public Options Options
        {
            get;
            set;
        }
 
        public bool NoSignIssues
        {
            get;
            set;
        }
 
        public Log Log
        {
            get;
            set;
        }
 
        public int TotalFiles
        {
            get;
            set;
        }
 
        public int TotalUnsignedFiles
        {
            get;
            set;
        }
 
        public int TotalSignedFiles
        {
            get;
            set;
        }
 
        public int TotalSkippedFiles
        {
            get;
            set;
        }
 
        public int TotalExcludedFiles
        {
            get;
            set;
        }
 
        public int TotalSkippedExcludedFiles
        {
            get;
            set;
        }
 
        public SignCheck(string[] args)
        {
            Options = new Options();
            ParserResult<Options> parseResult = Parser.Default.ParseArguments<Options>(args).
                WithParsed(options => HandleOptions(options)).
                WithNotParsed<Options>(errors => HandleErrors(errors));
        }
 
        public SignCheck(Options options)
        {
            HandleOptions(options ?? new Options());
        }
 
        private void HandleOptions(Options options)
        {
            Options = options;
 
            Log = new Log(options.LogFile, options.ErrorLogFile, options.Verbosity);
 
            if (Options.FileStatus.Count() > 0)
            {
                FileStatus = FileStatus.NoFiles;
                foreach (string value in Options.FileStatus)
                {
                    FileStatus result;
                    if (Enum.TryParse<FileStatus>(value, out result))
                    {
                        FileStatus |= result;
                    }
                    else
                    {
                        Log.WriteError(LogVerbosity.Minimum, SignCheckResources.scErrorUnknownFileStatus, value);
                    }
                }
 
                if (FileStatus == FileStatus.NoFiles)
                {
                }
            }
            else
            {
                FileStatus = FileStatus.UnsignedFiles;
            }
 
            if (!String.IsNullOrEmpty(Options.ExclusionsFile))
            {
                ProcessExclusions(Options.ExclusionsFile);
            }
            else
            {
                Exclusions = new Exclusions();
            }
            // Add some well-known exclusions for WiX
            Exclusions.Add(new Exclusion("*netfxca*;*.msi;Wix custom action (NGEN"));
            Exclusions.Add(new Exclusion("*wixdepca*;*.msi;WiX custom action"));
            Exclusions.Add(new Exclusion("*wixuiwixca*;*.msi;WiX custom action"));
            Exclusions.Add(new Exclusion("*wixca*;*.msi;Wix custom action"));
            Exclusions.Add(new Exclusion("*wixstdba.dll*;*.exe;WiX standard bundle application"));
 
            if (!Directory.Exists(_appData))
            {
                Directory.CreateDirectory(_appData);
            }
        }
 
        private void HandleErrors(IEnumerable<Error> errors)
        {
            HasArgErrors = true;
        }
 
        private List<string> GetInputFilesFromOptions()
        {
            var inputFiles = new List<string>();
            var downloadFiles = new List<Uri>();
            if (Options.InputFiles == null)
            {
                return inputFiles;
            }
            foreach (string inputFile in Options.InputFiles)
            {
                Uri uriResult;
 
                if ((Uri.TryCreate(inputFile, UriKind.Absolute, out uriResult)) && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps))
                {
                    string downloadPath = Path.Combine(_appData, Path.GetFileName(uriResult.LocalPath));
                    inputFiles.Add(downloadPath);
                    downloadFiles.Add(uriResult);
                }
                else if (inputFile.IndexOfAny(_wildcards) > -1)
                {
                    SearchOption fileSearchOptions = Options.TraverseSubFolders ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
                    string fileSearchPath = Path.GetDirectoryName(inputFile);
                    string fileSearchPattern = Path.GetFileName(inputFile);
                    string[] matchedFiles = null;
 
                    if (String.IsNullOrEmpty(fileSearchPath))
                    {
                        // CASE 1: No path, pattern in filename, e.g. "--input-File *.txt" or "-i Foo?Bar.txt"
                        fileSearchPath = Directory.GetCurrentDirectory();
                        matchedFiles = Directory.GetFiles(fileSearchPath, fileSearchPattern, fileSearchOptions);
                    }
                    else
                    {
                        if (fileSearchPath.IndexOfAny(_wildcards) > -1)
                        {
                            // CASE 2: Path contains wildcards, e.g. "-i C:\Foo*\Bar.txt" or "-i C:\Foo*\Bar*.txt"
                            string[] wildcardDirectories = Utils.GetDirectories(fileSearchPath, null, fileSearchOptions);
 
                            var _matchedFiles = new List<string>();
 
                            foreach (string dir in wildcardDirectories)
                            {
                                _matchedFiles.AddRange(Directory.GetFiles(dir, fileSearchPattern, fileSearchOptions));
                            }
 
                            matchedFiles = _matchedFiles.ToArray();
                        }
                        else
                        {
                            // CASE 3: Path contains no search patterns, e.g. "-i C:\Foo\Bar\*.txt"
                            if (Directory.Exists(fileSearchPath))
                            {
                                matchedFiles = Directory.GetFiles(fileSearchPath, fileSearchPattern, fileSearchOptions);
                            }
                            else
                            {
                                Log.WriteError(String.Format(SignCheckResources.scDirDoesNotExist, fileSearchPath));
                            }
                        }
                    }
 
                    if (matchedFiles != null)
                    {
                        foreach (string file in matchedFiles)
                        {
                            inputFiles.Add(file);
                        }
                    }
                }
                else
                {
                    if (Directory.Exists(inputFile))
                    {
                        SearchOption searchOption = Options.Recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
 
                        foreach (string dirFile in Directory.GetFiles(inputFile, "*.*", searchOption))
                        {
                            inputFiles.Add(dirFile);
                        }
                    }
                    else if (File.Exists(Path.GetFullPath(inputFile)))
                    {
                        inputFiles.Add(inputFile);
                    }
                    else
                    {
                        Log.WriteError(String.Format(SignCheckResources.scInputFileDoesNotExist, inputFile));
                    }
                }
            }
 
            if (downloadFiles.Count > 0)
            {
                DownloadFilesAsync(downloadFiles).Wait();
            }
 
            // Exclude log files in case they are created in the folder being scanned.
            if (!String.IsNullOrEmpty(Options.ErrorLogFile))
            {
                inputFiles.Remove(Path.GetFullPath(Options.ErrorLogFile));
            }
 
            if (!String.IsNullOrEmpty(Options.LogFile))
            {
                inputFiles.Remove(Path.GetFullPath(Options.LogFile));
            }
            return inputFiles;
        }
 
        private void ProcessExclusions(string exclusionsFile)
        {
            Log.WriteMessage(LogVerbosity.Diagnostic, SignCheckResources.scProcessExclusions);
            Exclusions = new Exclusions(exclusionsFile);
        }
 
        private void ProcessResults(IEnumerable<SignatureVerificationResult> results, int indent)
        {
            foreach (SignatureVerificationResult result in results)
            {
                TotalFiles++;
 
                if (result.IsSigned && !result.IsExcluded)
                {
                    TotalSignedFiles++;
                }
                else if (!(result.IsExcluded || result.IsSkipped) && (!result.IsSigned && !result.IsDoNotSign))
                {
                    TotalUnsignedFiles++;
                }
 
                if (result.IsExcluded || (!result.IsSigned && result.IsDoNotSign))
                {
                    TotalExcludedFiles++;
                }
 
                if (result.IsSkipped)
                {
                    TotalSkippedFiles++;
                }
 
                if (result.IsSkipped && result.IsExcluded)
                {
                    TotalSkippedExcludedFiles++;
                }
 
                // Regardless of the file status reporting settings, a container file like an MSI or NuGet package
                // is always reported to keep the file hierarchy in the log readable.
                if (((result.IsSkipped) && ((FileStatus & FileStatus.SkippedFiles) != 0)) ||
                    ((result.IsSigned) && ((FileStatus & FileStatus.SignedFiles) != 0)) ||
                    ((result.IsExcluded) && ((FileStatus & FileStatus.ExcludedFiles) != 0)) ||
                    ((result.IsSigned) && (result.IsDoNotSign)) ||
                    ((result.NestedResults.Count() > 0) && (Options.Recursive)) ||
                    ((FileStatus & FileStatus.AllFiles) == FileStatus.AllFiles) ||
                    ((!result.IsSigned && !result.IsDoNotSign) && (!result.IsSkipped) && (!result.IsExcluded) && ((FileStatus & FileStatus.UnsignedFiles) != 0)))
                {
                    LoggedResults = true;
                    Log.WriteMessage(LogVerbosity.Minimum, String.Empty.PadLeft(indent) + result.ToString(result.IsExcluded ? DetailKeys.ResultKeysExcluded : ResultDetails));
                }
 
                if (((!result.IsSigned) && (!(result.IsSkipped || result.IsExcluded || result.IsDoNotSign))) || (result.IsSigned && result.IsDoNotSign))
                {
                    NoSignIssues = false;
                }
 
                if (result.NestedResults.Count > 0)
                {
                    ProcessResults(result.NestedResults, indent + 2);
                }
            }
        }
 
        public void GenerateExclusionsFile(StreamWriter writer, IEnumerable<SignatureVerificationResult> results)
        {
            foreach (SignatureVerificationResult result in results)
            {
                if ((!result.IsSigned) && (!result.IsSkipped))
                {
                    writer.WriteLine(result.ExclusionEntry);
                }
 
                if (result.NestedResults.Count > 0)
                {
                    GenerateExclusionsFile(writer, result.NestedResults);
                }
            }
        }
 
        internal int Run()
        {
            try
            {
                Log.WriteMessage("Starting execution of SignCheck.");
 
                SignatureVerificationOptions options = SignatureVerificationOptions.None;
                options |= Options.Recursive ? SignatureVerificationOptions.VerifyRecursive : SignatureVerificationOptions.None;
                options |= Options.EnableXmlSignatureVerification ? SignatureVerificationOptions.VerifyXmlSignatures : SignatureVerificationOptions.None;
                options |= Options.SkipTimestamp ? SignatureVerificationOptions.None : SignatureVerificationOptions.VerifyAuthentiCodeTimestamps;
                options |= Options.VerifyStrongName ? SignatureVerificationOptions.VerifyStrongNameSignature : SignatureVerificationOptions.None;
                options |= Options.EnableJarSignatureVerification ? SignatureVerificationOptions.VerifyJarSignatures : SignatureVerificationOptions.None;
                options |= !String.IsNullOrEmpty(Options.ExclusionsOutput) ? SignatureVerificationOptions.GenerateExclusion : SignatureVerificationOptions.None;
 
                var signatureVerificationManager = new SignatureVerificationManager(Exclusions, Log, options);
 
                ResultDetails = Options.Verbosity > LogVerbosity.Normal ? DetailKeys.ResultKeysVerbose : DetailKeys.ResultKeysNormal;
 
                if (InputFiles != null && InputFiles.Count() > 0)
                {
                    DateTime startTime = DateTime.Now;
                    IEnumerable<SignatureVerificationResult> results = signatureVerificationManager.VerifyFiles(InputFiles);
                    DateTime endTime = DateTime.Now;
 
                    NoSignIssues = true;
                    Log.WriteLine();
                    Log.WriteMessage(LogVerbosity.Minimum, SignCheckResources.scResults);
                    Log.WriteLine();
                    ProcessResults(results, 0);
 
                    // Generate an exclusions file for any unsigned files that were reported.
                    if (!String.IsNullOrEmpty(Options.ExclusionsOutput))
                    {
                        if (!Directory.Exists(Options.ExclusionsOutput))
                        {
                            Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(Options.ExclusionsOutput)));
                        }
                        using (var exclusionsWriter = new StreamWriter(Options.ExclusionsOutput, append: false))
                        {
                            GenerateExclusionsFile(exclusionsWriter, results);
                        }
                    }
 
                    if (LoggedResults)
                    {
                        Log.WriteLine();
                    }
 
                    if (NoSignIssues)
                    {
                        Log.WriteMessage(LogVerbosity.Minimum, SignCheckResources.scNoSignIssues);
                    }
                    else
                    {
                        Log.WriteError(LogVerbosity.Minimum, SignCheckResources.scSignIssuesFound);
                    }
 
                    TimeSpan totalTime = endTime - startTime;
                    Log.WriteMessage(LogVerbosity.Minimum, String.Format(SignCheckResources.scTime, totalTime));
                    Log.WriteMessage(LogVerbosity.Minimum, String.Format(SignCheckResources.scStats,
                        TotalFiles, TotalSignedFiles, TotalUnsignedFiles, TotalSkippedFiles, TotalExcludedFiles, TotalSkippedExcludedFiles));
                }
                else
                {
                    Log.WriteMessage(LogVerbosity.Minimum, SignCheckResources.scNoFilesProcessed);
                }
            }
 
            catch (Exception e)
            {
                Log.WriteError(e.ToString());
            }
            finally
            {
                if (Log != null)
                {
                    Log.Close();
                }
            }
 
            return Log.HasLoggedErrors ? -1 : 0;
        }
 
        private async Task DownloadFileAsync(Uri uri)
        {
            try
            {
                ServicePointManager.CheckCertificateRevocationList = true;
 
                using (var wc = new WebClient())
                {
                    string downloadPath = Path.Combine(_appData, Path.GetFileName(uri.LocalPath));
 
                    if (File.Exists(downloadPath))
                    {
                        Log.WriteMessage(LogVerbosity.Detailed, SignCheckResources.scDeleteExistingFile, downloadPath);
                        File.Delete(downloadPath);
                    }
 
                    Log.WriteMessage(LogVerbosity.Detailed, SignCheckResources.scDownloading, uri.AbsoluteUri, downloadPath);
                    await wc.DownloadFileTaskAsync(uri, downloadPath);
                }
            }
            catch (Exception e)
            {
                Log.WriteError(e.Message);
            }
        }
 
        private async Task DownloadFilesAsync(IEnumerable<Uri> uris)
        {
            await Task.WhenAll(uris.Select(u => DownloadFileAsync(u)));
        }
 
        [STAThread]
        static int Main(string[] args)
        {
            // Exit code 3 for help output
            int retVal = 3;
            var sc = new SignCheck(args);
            if ((sc.Options != null) && (!sc.HasArgErrors))
            {
                retVal = sc.Run();
            }
            return retVal;
        }
    }
}