using Alkami.Ops.Certificates.Data; using Alkami.Ops.Certificates.SecretServer; using Alkami.Ops.Certificates.Utilities; using Alkami.Ops.Common.Cryptography; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using System.Management.Automation; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace Alkami.Ops.Certificates { internal class SecretServerImporter : IDisposable { private readonly string _tempPath; private readonly string _globalPassword; private readonly SecretServerClient _secretServerClient; private const string CertificateTemplateTypeName = "Automated_Certificate"; private readonly string ExportCertificatesScript; private static long certCount = 0; /// /// Constructor. /// /// The base URL of the secret server site. /// Secret Server username with API access. /// Secret server user password. public SecretServerImporter(string secretSite, string secretUser, string secretPassword) { _tempPath = Path.Combine(Path.GetTempPath(), "CertificateExport"); if (Directory.Exists(_tempPath)) { Extensions.ClearDirectory(_tempPath); } else { Directory.CreateDirectory(_tempPath); } _globalPassword = GeneratePassword(); _secretServerClient = new SecretServerClient(secretSite, secretUser, secretPassword); // Read in powershell scripts. ExportCertificatesScript = Resources.ExportRemoteCertificates; } /// /// Cleanup /// public void Dispose() { if (Directory.Exists(_tempPath)) { Directory.Delete(_tempPath, true); } _secretServerClient?.Dispose(); } /// /// Creates machine-friendly per-pod secrets, so a server can download 4 secrets instead of 200. /// /// /// public void CreatePodSecrets(string friendlySecretBaseFolderPath, string machineSecretBaseFolderPath, string[] importableUsers) { var baseFolder = _secretServerClient.GetOrAddFolderAsync(friendlySecretBaseFolderPath).GetAwaiter().GetResult(); // The secret server will only return -all- folders under a subfolder. Can't just query for folders at a time. var folders = _secretServerClient.GetFoldersByParentFolder(baseFolder.ID); // Parse apart the paths of the folders to strip off the base path. friendlySecretBaseFolderPath = friendlySecretBaseFolderPath.Replace('/', '\\'); var folderPaths = folders.Select(folder => folder.FolderPath) .Select(path => path.Substring(path.IndexOf(friendlySecretBaseFolderPath) + friendlySecretBaseFolderPath.Length)); // Parse apart the environment types, and pods that exist (+ common) // Levels: // 1) EnvironmentType // 2) Environment (or Common) // 3) Web / App / All var splitPaths = folderPaths.Select(path => path.Split(new char[] { '\\' }, StringSplitOptions.RemoveEmptyEntries)).ToArray(); var environmentTypes = splitPaths .Where(split => split.Length >= 1) .Select(split => split[0]) .Distinct(); foreach (var environmentType in environmentTypes) { var environments = splitPaths .Where(split => split.Length >= 2) .Where(split => string.Equals(split[0], environmentType, StringComparison.OrdinalIgnoreCase)) .Select(split => split[1]) .Distinct(); foreach (var environment in environments) { CreatePodSecrets(friendlySecretBaseFolderPath, machineSecretBaseFolderPath, environmentType, environment, importableUsers); } } } /// /// Creates machine-friendly per-pod secrets, so a server can download 4 secrets instead of 200. /// /// Secret server path of the friendly secrets folder. /// Secret server path of the machine per-pod secrets folder. /// The type of environment. /// The name of the environment. (Or "Common") private void CreatePodSecrets(string friendlySecretBaseFolderPath, string machineSecretBaseFolderPath, string environmentType, string environment, string[] importableUsers) { using (PowerShell powerShellSession = PowerShell.Create()) { var environmentPath = Path.Combine(friendlySecretBaseFolderPath, environmentType, environment); string[] serverTypes = new string[] { "All", "Web", "App" }; // Get the secret template type. var secretTemplateType = _secretServerClient.GetSecretTemplateByName(CertificateTemplateTypeName); foreach (var serverType in serverTypes) { // Clear out the temp path directory. Only one of these processes should run at a time. // TODO: Rewrite the function to be pathed by environment type and environment so multiple of them can be run in parallel. if (Directory.Exists(_tempPath) && Directory.EnumerateFiles(_tempPath).Any()) { Extensions.ClearDirectory(_tempPath); } var secretsPath = Path.Combine(environmentPath, serverType); var folder = _secretServerClient.GetOrAddFolderAsync(secretsPath).GetAwaiter().GetResult(); var secrets = _secretServerClient.GetSecretsByFolder(folder); // Load the full secrets. var fullSecrets = secrets.Select(secret => _secretServerClient.GetSecretByID(secret.ID).GetAwaiter().GetResult()); // Download each secret to a separate folder by ID. foreach (var secret in fullSecrets) { // Download the zip. Console.WriteLine($"Attempting to download {secret.ID}"); var zipName = $"{secret.ID}.zip"; var zipDownloadLocation = Path.Combine(_tempPath, zipName); if (!_secretServerClient.DownloadFile(zipDownloadLocation, secret).GetAwaiter().GetResult()) { Console.WriteLine($"Failed to download secret ID {secret.ID}"); continue; } // Unzip the file to a per-secret folder. var outputFolder = Path.Combine(_tempPath, secret.ID.ToString()); if (!Directory.Exists(outputFolder)) { Directory.CreateDirectory(outputFolder); } // Unzip it. ZipFile.ExtractToDirectory(zipDownloadLocation, outputFolder); var pfxFiles = Directory.GetFiles(outputFolder, "*.pfx", SearchOption.AllDirectories); foreach (var file in pfxFiles) { // re-export secret password with global password var cert = new X509Certificate2(file, secret["Import Password"], X509KeyStorageFlags.Exportable); byte[] exportedBytes = cert.Export(X509ContentType.Pkcs12, _globalPassword); File.WriteAllBytes(file, exportedBytes); } } // Copy all of the certificates together, preserving folder paths. var podSecretOutputFolder = Path.Combine(_tempPath, "PodSecretOutputFolder"); if (!Directory.Exists(podSecretOutputFolder)) { Directory.CreateDirectory(podSecretOutputFolder); } var secretFolders = Directory.GetDirectories(_tempPath); foreach (var sourceFolder in secretFolders) { // Source directories, a folder for each secret server secret. var sourceDirectories = Directory.GetDirectories(sourceFolder, "*", SearchOption.AllDirectories); // Create folders in the destination folder foreach (string dirPath in sourceDirectories) { var dstPath = dirPath.Replace(sourceFolder, podSecretOutputFolder); if (!Directory.Exists(dstPath)) { Directory.CreateDirectory(dstPath); } } // Copy over the secrets. foreach (string newPath in Directory.GetFiles(sourceFolder, "*.*", SearchOption.AllDirectories)) { // If the file already exists in the combined secret, and it isn't the same file, come up with a new non-duplicate name and copy the secret over. var podSecretDestinationPath = newPath.Replace(sourceFolder, podSecretOutputFolder); int counter = 1; bool doCopy = true; do { // Copy over the file if it doesn't exist. if (!File.Exists(podSecretDestinationPath)) { break; } // If the files are equal, the cert already exists in the combined folder, so break out. if (Extensions.FilesAreEqual(new FileInfo(newPath), new FileInfo(podSecretDestinationPath))) { doCopy = false; break; } // Otherwise we're going to try to come up with a unique filename. // This loop is going to keep going until we either find the exact same cert, or we arrive on a unique file name. string nonConflictingPath = Path.Combine( Path.GetDirectoryName(newPath), Path.GetFileNameWithoutExtension(newPath), counter.ToString(), Path.GetExtension(newPath)); podSecretDestinationPath = nonConflictingPath; counter++; } while (true); if (doCopy) { File.Copy(newPath, podSecretDestinationPath, true); } } } // Zip up the combined certs. var podSecretName = serverType; var podSecretZipPath = Path.Combine(_tempPath, $"{podSecretName}.zip"); ZipFile.CreateFromDirectory(podSecretOutputFolder, podSecretZipPath); // Create secret with combined zip. var secretPath = Path.Combine(machineSecretBaseFolderPath, environmentType, environment); var destinationSecretFolder = _secretServerClient.GetOrAddFolderAsync(secretPath).GetAwaiter().GetResult(); // See if a secret already exists, and if it does, delete it. var existingSecret = _secretServerClient.FindSecretByName(serverType, destinationSecretFolder.ID); if (existingSecret != null) { _secretServerClient.DeleteSecret(existingSecret.ID).GetAwaiter().GetResult(); } // Create the new secret stub by template ID. var secretStub = _secretServerClient.CreateSecretStubByTemplateIDAsync(secretTemplateType.ID, podSecretName + "-" + destinationSecretFolder.ID, destinationSecretFolder.ID).GetAwaiter().GetResult(); var currentZipHash = ""; using (var stream = File.OpenRead(podSecretZipPath)) { currentZipHash = Extensions.GetMd5HashString(stream); } // Fill out secret details. secretStub["Import Password"] = _globalPassword; secretStub["Certificate Name"] = podSecretName; secretStub["Thumbprint"] = "N/A"; secretStub["ExpirationDate"] = ""; secretStub["File Hash"] = currentZipHash; // Create the secret. var newSecret = _secretServerClient.CreateSecret(secretStub).GetAwaiter().GetResult(); // Upload the zip. _secretServerClient.UploadFile(newSecret, podSecretZipPath).GetAwaiter().GetResult(); } } } /// /// Imports certificates from all of the given servers into the secret server. /// /// List of server hostnames. /// Secret server folder path to drop the secrets in. public void Import(IEnumerable servers, string baseSecretFolder) { // Make sure the servers are unique just in case. servers = servers.Select(s => s.ToLower()).Distinct(); var serverData = new Dictionary(); var environmentData = new Dictionary(); var certificateData = new Dictionary(); if (Directory.Exists(_tempPath)) { // Remove directories and files under _tempPath. foreach (var directory in Directory.GetDirectories(_tempPath)) { Directory.Delete(directory, true); } foreach (var file in Directory.GetFiles(_tempPath)) { File.Delete(file); } } // Gather server/environment data from the servers. using (var timer = new QuickWatch("GatherEnvironmentData")) { GatherEnvironmentData(servers, ref environmentData, ref serverData); } // Download certificates from each server. using (var timer = new QuickWatch("DownloadCertificatesFromServers")) { DownloadCertificatesFromServers(servers, _tempPath); } // Build the tree of certificate information, to weed out unique certificates that need uploading. using (var timer = new QuickWatch("BuildCertificateData")) { BuildCertificateData(_tempPath, ref serverData, ref certificateData); } // Upload the secrets to the secret server. using (var timer = new QuickWatch("UploadCertificatesToSecretServer")) { UploadCertificatesToSecretServer(baseSecretFolder, ref environmentData, ref certificateData); } } /// /// Gathers environment data from each of the servers, and tracks the data in _environmentData and _serverData /// /// private void GatherEnvironmentData(IEnumerable servers, ref Dictionary environmentData, ref Dictionary serverData) { // Read server/environment data from all of the servers. foreach (var server in servers) { // Grab the server name/type/envType/host from the server's machine config. var serverInfo = Extensions.GetServerInfo(server); // Look up the environment. var environmentNameLower = serverInfo.EnvironmentName.ToLower(); EnvironmentInfo environment = null; if (!environmentData.TryGetValue(environmentNameLower, out environment)) { environment = new EnvironmentInfo(serverInfo.EnvironmentName, serverInfo.EnvironmentType); environmentData[environmentNameLower] = environment; } // Recreate the server to include the environment. serverInfo = new ServerInfo(serverInfo, environment); serverData[server] = serverInfo; // Record the server in the environment. environment.Servers.Add(serverInfo); } } /// /// Generates a strong password of [length] characters. /// /// The length of the password /// private string GeneratePassword(int length = 32) { RNGCryptoServiceProvider cryptRNG = new RNGCryptoServiceProvider(); byte[] tokenBuffer = new byte[length]; cryptRNG.GetBytes(tokenBuffer); return Convert.ToBase64String(tokenBuffer); } /// /// Aliases store string names to the Alkami certificate import folder names. /// /// String name of the store. /// private string GetStoreFolderName(string storename) { switch (storename.ToLower()) { case "my": return "Personal"; case "ca": return "IA"; case "root": return "Root"; case "trustedpeople": return "TrustedPeople"; default: throw new Exception("Unknown Store Name Type"); } } /// /// Downloads certificates from servers and unzips them to the output directory. /// /// /// private void DownloadCertificatesFromServers(IEnumerable servers, string outputDirectory) { // Export certs from every server. var serverString = string.Join(",", servers); using (PowerShell instance = PowerShell.Create()) { instance.AddScript(ExportCertificatesScript); instance.AddParameter("serverString", serverString); instance.AddParameter("exportPassword", _globalPassword); instance.AddParameter("importPath", outputDirectory); var psOutput = instance.Invoke(); if (instance.Streams.Error.Any()) { Console.WriteLine("There were Errors executing powershell:"); foreach (var error in instance.Streams.Error) { Console.WriteLine(error); } } } } /// /// Loads all certificates into memory, builds certificate chains, and determines certificate cert/server/environment relationships. /// /// The directory of certificates, per server folder. private void BuildCertificateData(string serverCertificateDirectory, ref Dictionary serverData, ref Dictionary certificateData) { var certificateFiles = Directory.GetFiles(serverCertificateDirectory, "*", SearchOption.AllDirectories).Select(p => Path.GetFullPath(p)).ToArray(); var certificateFileHasPrivateKey = new Dictionary(); var fileToCertificate = new ConcurrentDictionary(); // Load all of the certificates into memory so we can make sense of them. var options = new ParallelOptions(); options.MaxDegreeOfParallelism = 32; foreach (var file in certificateFiles) { var extension = Path.GetExtension(file); string newPassword = null; X509Certificate2 cert = null; if (extension == ".cer") { cert = new X509Certificate2(file); } else if (extension == ".pfx") { cert = new X509Certificate2(file, _globalPassword, X509KeyStorageFlags.Exportable); // Generate a unique password for this cert going forward. newPassword = GeneratePassword(); } else { throw new Exception($"Unhandled certificate extension {extension}"); } // Store if the particular cert has a private key. certificateFileHasPrivateKey[file] = cert.HasPrivateKey; var certInfo = new CertificateInfo(cert, file, newPassword); if (!fileToCertificate.TryAdd(file, certInfo)) { throw new Exception($"Was unable to track certificate {file}. Investigate."); } }; // Identify unique certificates from the list of certs from every server which includes duplicates. // Prefer to select unique certs that include private keys. var uniqueCertificates = new Dictionary(); var fileToCertificateEnum = fileToCertificate.Where(cert => cert.Value.Password != null).Concat(fileToCertificate); foreach (var certInfoKV in fileToCertificateEnum) { var certInfo = certInfoKV.Value; if (!uniqueCertificates.ContainsKey(certInfo.Thumbprint)) { uniqueCertificates.Add(certInfo.Thumbprint, certInfo); } } // Run back through each non-unique certificate, and assign servers/environments to each unique cert. // This is tracked by the folder that the certificate was loaded from. File paths in the form of: // "basePath\serverName\certificateStore\certificateName.pfx" foreach (var file in certificateFiles) { // Select the server/store out of the folder path. var fileSplit = file.Split(new char[] { '\\' }); var serverName = fileSplit[fileSplit.Length - 3].ToLower(); var storeName = fileSplit[fileSplit.Length - 2].ToLower(); // Look up the unique certificate by the thumbprint of the non-unique certificate. var nonUniqueCertInfo = fileToCertificate[file]; var uniqueCertInfo = uniqueCertificates[nonUniqueCertInfo.Thumbprint]; // Add the server/environment that the cert is in to the unique cert info object. var serverInfo = serverData[serverName]; var environmentName = serverInfo.EnvironmentName.ToLower(); uniqueCertInfo.Servers.TryAdd(serverName, serverInfo); uniqueCertInfo.Environments.TryAdd(environmentName, serverInfo.Environment); // Record the store that the certificate belongs in, if it does not already exist. if (!uniqueCertInfo.Stores.Any(certStore => string.Equals(certStore.StoreName, storeName, StringComparison.OrdinalIgnoreCase))) { bool storeHasPrivateKey = certificateFileHasPrivateKey[file]; StoreInfo store = new StoreInfo(storeName, storeHasPrivateKey); uniqueCertInfo.Stores.Add(store); } } // Copy all of the unique certificates up to _certificateData, which will be the store of certs for other functions. foreach (var certInfoKV in uniqueCertificates) { certificateData.Add(certInfoKV.Key, certInfoKV.Value); } // For each unique certificate, build out the certificate chain and figure out which certs require which other certs. // Later we will use this data to create secrets for the "leaf certificates" that are not intermediates of other certificates. foreach (var certificateInfo in uniqueCertificates) { var certificate = certificateInfo.Value.Certificate; X509Chain chain = new X509Chain(); chain.Build(certificate); List chainCerts = chain.ChainElements.ToList(); // Exit if there's no chain to walk. if (chainCerts.Count <= 1) { continue; } // Build the tree of parent/child certificate associations. for (int i = 1; i < chainCerts.Count; i++) { var parentCert = chainCerts[i]; var childCert = chainCerts[i - 1]; // Apparently certificates in the chain can exist and validate WITHOUT being loaded into the stores of the remote machines. // They are most likely getting validated through the domain controller. if (!certificateData.ContainsKey(parentCert.Thumbprint)) { string filename = null; // No filename because it wasn't exported from a cert store. var info = new CertificateInfo(parentCert, filename); certificateData[parentCert.Thumbprint] = info; } // Attach the parent/child info to certificates for tracking. var parentInfo = certificateData[parentCert.Thumbprint]; var childInfo = certificateData[childCert.Thumbprint]; childInfo.Parent = parentInfo; parentInfo.Children.Add(childInfo); } } } /// /// Uploads tracked certificates in _certificateData to the secret server at the base path. /// /// The base folder path in the secret server to upload certs to. private void UploadCertificatesToSecretServer(string baseSecretFolder, ref Dictionary environmentData, ref Dictionary certificateData) { // Grab the list of certificates to create secrets for. var certsToExport = certificateData.Select(x => x.Value).ToList(); var secretImportZipPath = Path.Combine(_tempPath, "SecretImportTemp"); if (!Directory.Exists(secretImportZipPath)) { Directory.CreateDirectory(secretImportZipPath); } // Create folders on the secret server for every environment, so we can multithread the certificate uploads without fear of stomping. var majorPodRegex = @"[^\.](\d+)"; foreach (var environment in environmentData) { var type = environment.Value.EnvironmentType; type = char.ToUpper(type[0]) + type.Substring(1); var path = Path.Combine(baseSecretFolder, type); // Define the server types. var serverTypeFolders = new string[] { "Web", "App", "All" }; // Create the common folder for the environment type. foreach (var serverType in serverTypeFolders) { var serverTypePath = Path.Combine(path, "Common", serverType); var folder = _secretServerClient.GetOrAddFolderAsync(serverTypePath).GetAwaiter().GetResult(); } // Only production cares about separate cert folders per environment. if (type == "Production") { var matches = Regex.Matches(environment.Value.Name, majorPodRegex); string majorPod = matches[0].Captures[0].Value.Trim(); path = Path.Combine(path, majorPod); } // Server type folders inside the pod. foreach (var serverType in serverTypeFolders) { var serverTypePath = Path.Combine(path, serverType); var folder = _secretServerClient.GetOrAddFolderAsync(serverTypePath).GetAwaiter().GetResult(); } } // Fetch all the secrets from the base folder, so we can check for secrets we've already loaded. bool includeSubfolders = true; var baseFolder = _secretServerClient.GetOrAddFolderAsync(baseSecretFolder).GetAwaiter().GetResult(); var allSecrets = _secretServerClient.GetSecretsByFolder(baseFolder, includeSubfolders); try { var secretTemplateType = _secretServerClient.GetSecretTemplateByName(CertificateTemplateTypeName); // Create separate .zip individual files for each certificate being uploaded. var options = new ParallelOptions(); options.MaxDegreeOfParallelism = 32; Parallel.ForEach(certsToExport, options, (cert) => { // Create a folder for the cert. var certFolderPath = Path.Combine(secretImportZipPath, cert.Thumbprint); if (!Directory.Exists(certFolderPath)) { Directory.CreateDirectory(certFolderPath); } // Copy all of the certs for the chain var currentCert = cert; Console.WriteLine($"Working on {cert.Name}"); do { // Don't export this cert because it didn't exist in the stores of the remote machines. if (cert.FileName == null) { currentCert = currentCert.Parent; continue; } // For each store the cert belongs in. foreach (var store in cert.Stores) { // Export certificates to the appropriate place and store folder. var outputDirectory = Path.Combine(certFolderPath, store.StoreName); if (!Directory.Exists(outputDirectory)) { Directory.CreateDirectory(outputDirectory); } var password = store.HasPrivateKey ? cert.Password : null; CertificateHelper.ExportCertificate(cert.Certificate, outputDirectory, password); } // Move along to the next cert in the chain. currentCert = currentCert.Parent; } while (currentCert != null); // Zip up the cert for upload to secret. var outputZipPath = Path.Combine(secretImportZipPath, $"{cert.Thumbprint}.zip"); ZipFile.CreateFromDirectory(certFolderPath, outputZipPath); var currentZipHash = ""; using (var stream = File.OpenRead(outputZipPath)) { currentZipHash = Extensions.GetMd5HashString(stream); } // Figure out what environment subfolder the certificate is going in on the secret server. var serverNames = cert.Servers.Select(serverInfoKV => serverInfoKV.Key.ToLower()); var appServerNames = new string[] { "vma", "app", "fab", "mic" }; var webServerNames = new string[] { "vmw", "web" }; bool onApps = appServerNames.Any(type => serverNames.Any(serverName => serverName.Contains(type))); bool onWebs = webServerNames.Any(type => serverNames.Any(serverName => serverName.Contains(type))); bool onBothWebsAndApps = onApps && onWebs; // Determine the types of environments that this cert exists in. // We duplicate certs for each environment. It's too reckless to have a "every environment type" common folder. var environmentTypes = cert.Environments .Select(e => e.Value.EnvironmentType.ToLower()) .Distinct() .Select(et => char.ToUpper(et.ElementAt(0)) + et.Substring(1)); // Upper case environment type names. foreach (var environmentType in environmentTypes) { // Build secret server folder path to create the secret. // Determine if the cert is on multiple pods within an environment type. var environments = cert.Environments.Where(e => e.Value.EnvironmentType.ToLower() == environmentType.ToLower()); bool onMultiplePods = environments.Count() >= 2; var secretServerFolderPath = Path.Combine(baseSecretFolder, environmentType); if (onMultiplePods) { secretServerFolderPath = Path.Combine(secretServerFolderPath, "Common"); } else if (environmentType == "Production") { var matches = Regex.Matches(environments.First().Value.Name, majorPodRegex); string majorPod = matches[0].Captures[0].Value.Trim(); secretServerFolderPath = Path.Combine(secretServerFolderPath, majorPod); } string serverFolder = string.Empty; if (onBothWebsAndApps) { serverFolder = "All"; } else if (onWebs) { serverFolder = "Web"; } else { serverFolder = "App"; } secretServerFolderPath = Path.Combine(secretServerFolderPath, serverFolder); var fullSecretPath = Path.Combine(secretServerFolderPath, cert.UniqueName); var folder = _secretServerClient.GetOrAddFolderAsync(secretServerFolderPath).GetAwaiter().GetResult(); // Figure out if the secret already exists for the cert. var secretName = cert.UniqueName; if (allSecrets.Any(existingSecret => existingSecret.FolderID == folder.ID && string.Equals(existingSecret.Name, secretName, StringComparison.OrdinalIgnoreCase))) { continue; } // Create the secret. var secretStub = _secretServerClient.CreateSecretStubByTemplateIDAsync(secretTemplateType.ID, secretName + "-" + folder.ID, folder.ID).GetAwaiter().GetResult(); var expiration = cert.Certificate.NotAfter.ToString(); // Fill out details about the cert. secretStub["Import Password"] = cert.Password; secretStub["Certificate Name"] = secretName; secretStub["Thumbprint"] = cert.Thumbprint; secretStub["ExpirationDate"] = expiration; secretStub["File Hash"] = currentZipHash; var foundCert = _secretServerClient.FindSecretByName(secretName); if (foundCert != null) { Console.WriteLine($"Secret exists {secretName}"); } else { try { // Create the secret. var secret = _secretServerClient.CreateSecret(secretStub).GetAwaiter().GetResult(); // Upload the cert .zip. _secretServerClient.UploadFile(secret, outputZipPath).GetAwaiter().GetResult(); } catch (Exception e) { Console.WriteLine("{0} Exception caught.", e.Message); } } } }); } finally { // Clean up the cert directory. if (Directory.Exists(secretImportZipPath)) { Directory.Delete(secretImportZipPath, true); } } } } }