diff --git a/util/DbSeederUtility/Program.cs b/util/DbSeederUtility/Program.cs index 59a61a220f..6e27170196 100644 --- a/util/DbSeederUtility/Program.cs +++ b/util/DbSeederUtility/Program.cs @@ -58,7 +58,9 @@ public class Program [Option('m', "mix-user-statuses", Description = "Use realistic status mix (85% confirmed, 5% each invited/accepted/revoked). Requires >= 10 users.")] bool mixStatuses = true, [Option('o', "org-structure", Description = "Org structure for collections: Traditional, Spotify, or Modern")] - string? structure = null + string? structure = null, + [Option('r', "region", Description = "Geographic region for names: NorthAmerica, Europe, AsiaPacific, LatinAmerica, MiddleEast, Africa, or Global")] + string? region = null ) { if (users < 1) @@ -77,6 +79,7 @@ public class Program } var structureModel = ParseOrgStructure(structure); + var geographicRegion = ParseGeographicRegion(region); var services = new ServiceCollection(); ServiceCollectionExtension.ConfigureServices(services); @@ -99,7 +102,8 @@ public class Program Ciphers = ciphers, Groups = groups, RealisticStatusMix = mixStatuses, - StructureModel = structureModel + StructureModel = structureModel, + Region = geographicRegion }); } @@ -118,4 +122,24 @@ public class Program _ => throw new ArgumentException($"Unknown structure '{structure}'. Use: Traditional, Spotify, or Modern") }; } + + private static GeographicRegion? ParseGeographicRegion(string? region) + { + if (string.IsNullOrEmpty(region)) + { + return null; + } + + return region.ToLowerInvariant() switch + { + "northamerica" => GeographicRegion.NorthAmerica, + "europe" => GeographicRegion.Europe, + "asiapacific" => GeographicRegion.AsiaPacific, + "latinamerica" => GeographicRegion.LatinAmerica, + "middleeast" => GeographicRegion.MiddleEast, + "africa" => GeographicRegion.Africa, + "global" => GeographicRegion.Global, + _ => throw new ArgumentException($"Unknown region '{region}'. Use: NorthAmerica, Europe, AsiaPacific, LatinAmerica, MiddleEast, Africa, or Global") + }; + } } diff --git a/util/Seeder/Data/BogusNameProvider.cs b/util/Seeder/Data/BogusNameProvider.cs new file mode 100644 index 0000000000..4a41b6b120 --- /dev/null +++ b/util/Seeder/Data/BogusNameProvider.cs @@ -0,0 +1,78 @@ +using Bit.Seeder.Data.Enums; +using Bogus; +using Bogus.DataSets; + +namespace Bit.Seeder.Data; + +/// +/// Provides locale-aware name generation using the Bogus library. +/// Maps GeographicRegion to appropriate Bogus locales for culturally-appropriate names. +/// +internal sealed class BogusNameProvider +{ + private readonly Faker _faker; + + public BogusNameProvider(GeographicRegion region, int? seed = null) + { + var locale = MapRegionToLocale(region, seed); + _faker = seed.HasValue + ? new Faker(locale) { Random = new Randomizer(seed.Value) } + : new Faker(locale); + } + + public string FirstName() => _faker.Name.FirstName(); + + public string FirstName(Name.Gender gender) => _faker.Name.FirstName(gender); + + public string LastName() => _faker.Name.LastName(); + + private static string MapRegionToLocale(GeographicRegion region, int? seed) => region switch + { + GeographicRegion.NorthAmerica => "en_US", + GeographicRegion.Europe => GetRandomEuropeanLocale(seed), + GeographicRegion.AsiaPacific => GetRandomAsianLocale(seed), + GeographicRegion.LatinAmerica => GetRandomLatinAmericanLocale(seed), + GeographicRegion.MiddleEast => GetRandomMiddleEastLocale(seed), + GeographicRegion.Africa => GetRandomAfricanLocale(seed), + GeographicRegion.Global => "en", + _ => "en" + }; + + private static string GetRandomEuropeanLocale(int? seed) + { + var locales = new[] { "en_GB", "de", "fr", "es", "it", "nl", "pl", "pt_PT", "sv" }; + return PickLocale(locales, seed); + } + + private static string GetRandomAsianLocale(int? seed) + { + var locales = new[] { "ja", "ko", "zh_CN", "zh_TW", "vi" }; + return PickLocale(locales, seed); + } + + private static string GetRandomLatinAmericanLocale(int? seed) + { + var locales = new[] { "es_MX", "pt_BR", "es" }; + return PickLocale(locales, seed); + } + + private static string GetRandomMiddleEastLocale(int? seed) + { + // Bogus has limited Middle East support; use available Arabic/Turkish locales + var locales = new[] { "ar", "tr", "fa" }; + return PickLocale(locales, seed); + } + + private static string GetRandomAfricanLocale(int? seed) + { + // Bogus has limited African support; use South African English and French (West Africa) + var locales = new[] { "en_ZA", "fr" }; + return PickLocale(locales, seed); + } + + private static string PickLocale(string[] locales, int? seed) + { + var random = seed.HasValue ? new Random(seed.Value) : Random.Shared; + return locales[random.Next(locales.Length)]; + } +} diff --git a/util/Seeder/Data/UsernameGenerator.cs b/util/Seeder/Data/CipherUsernameGenerator.cs similarity index 75% rename from util/Seeder/Data/UsernameGenerator.cs rename to util/Seeder/Data/CipherUsernameGenerator.cs index 42250bc7cd..21a726a8ff 100644 --- a/util/Seeder/Data/UsernameGenerator.cs +++ b/util/Seeder/Data/CipherUsernameGenerator.cs @@ -4,9 +4,13 @@ namespace Bit.Seeder.Data; /// /// Generates deterministic usernames for companies using configurable patterns. +/// Uses Bogus library for locale-aware name generation while maintaining determinism +/// through pre-generated arrays indexed by a seed. /// -internal sealed class UsernameGenerator +internal sealed class CipherUsernameGenerator { + private const int _namePoolSize = 1500; + private readonly Random _random; private readonly UsernamePattern _pattern; @@ -15,7 +19,7 @@ internal sealed class UsernameGenerator private readonly string[] _lastNames; - public UsernameGenerator( + public CipherUsernameGenerator( int seed, UsernamePatternType patternType = UsernamePatternType.FirstDotLast, GeographicRegion? region = null) @@ -23,12 +27,10 @@ internal sealed class UsernameGenerator _random = new Random(seed); _pattern = UsernamePatterns.GetPattern(patternType); - (_firstNames, _lastNames) = region switch - { - GeographicRegion.NorthAmerica => (Names.UsFirstNames, Names.UsLastNames), - GeographicRegion.Europe => (Names.EuropeanFirstNames, Names.EuropeanLastNames), - _ => (Names.AllFirstNames, Names.AllLastNames) - }; + // Pre-generate arrays from Bogus for deterministic index-based access + var provider = new BogusNameProvider(region ?? GeographicRegion.Global, seed); + _firstNames = Enumerable.Range(0, _namePoolSize).Select(_ => provider.FirstName()).ToArray(); + _lastNames = Enumerable.Range(0, _namePoolSize).Select(_ => provider.LastName()).ToArray(); } public string Generate(Company company) diff --git a/util/Seeder/Data/Names.cs b/util/Seeder/Data/Names.cs deleted file mode 100644 index dc493cd0e9..0000000000 --- a/util/Seeder/Data/Names.cs +++ /dev/null @@ -1,80 +0,0 @@ -namespace Bit.Seeder.Data; - -/// -/// First and last names organized by region for username generation. -/// Add new regions by creating arrays and including them in the All* properties. -/// -internal static class Names -{ - public static readonly string[] UsFirstNames = - [ - // Male - "James", "Robert", "John", "Michael", "David", "William", "Richard", "Joseph", "Thomas", "Christopher", - "Charles", "Daniel", "Matthew", "Anthony", "Mark", "Donald", "Steven", "Paul", "Andrew", "Joshua", - "Kenneth", "Kevin", "Brian", "George", "Timothy", "Ronald", "Edward", "Jason", "Jeffrey", "Ryan", - "Jacob", "Gary", "Nicholas", "Eric", "Jonathan", "Stephen", "Larry", "Justin", "Scott", "Brandon", - "Benjamin", "Samuel", "Raymond", "Gregory", "Frank", "Alexander", "Patrick", "Jack", "Dennis", "Jerry", - // Female - "Mary", "Patricia", "Jennifer", "Linda", "Barbara", "Elizabeth", "Susan", "Jessica", "Sarah", "Karen", - "Lisa", "Nancy", "Betty", "Margaret", "Sandra", "Ashley", "Kimberly", "Emily", "Donna", "Michelle", - "Dorothy", "Carol", "Amanda", "Melissa", "Deborah", "Stephanie", "Rebecca", "Sharon", "Laura", "Cynthia", - "Kathleen", "Amy", "Angela", "Shirley", "Anna", "Brenda", "Pamela", "Emma", "Nicole", "Helen", - "Samantha", "Katherine", "Christine", "Debra", "Rachel", "Carolyn", "Janet", "Catherine", "Maria", "Heather" - ]; - - public static readonly string[] UsLastNames = - [ - "Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez", - "Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin", - "Lee", "Perez", "Thompson", "White", "Harris", "Sanchez", "Clark", "Ramirez", "Lewis", "Robinson", - "Walker", "Young", "Allen", "King", "Wright", "Scott", "Torres", "Nguyen", "Hill", "Flores", - "Green", "Adams", "Nelson", "Baker", "Hall", "Rivera", "Campbell", "Mitchell", "Carter", "Roberts", - "Gomez", "Phillips", "Evans", "Turner", "Diaz", "Parker", "Cruz", "Edwards", "Collins", "Reyes", - "Stewart", "Morris", "Morales", "Murphy", "Cook", "Rogers", "Gutierrez", "Ortiz", "Morgan", "Cooper", - "Peterson", "Bailey", "Reed", "Kelly", "Howard", "Ramos", "Kim", "Cox", "Ward", "Richardson", - "Watson", "Brooks", "Chavez", "Wood", "James", "Bennett", "Gray", "Mendoza", "Ruiz", "Hughes", - "Price", "Alvarez", "Castillo", "Sanders", "Patel", "Myers", "Long", "Ross", "Foster", "Jimenez" - ]; - - public static readonly string[] EuropeanFirstNames = - [ - // British - "Oliver", "George", "Harry", "Jack", "Charlie", "Thomas", "Oscar", "William", "James", "Henry", - "Olivia", "Amelia", "Isla", "Ava", "Emily", "Sophie", "Grace", "Mia", "Poppy", "Ella", - // German - "Maximilian", "Alexander", "Paul", "Leon", "Lukas", "Felix", "Noah", "Elias", "Ben", "Finn", - "Emma", "Hannah", "Mia", "Sofia", "Anna", "Emilia", "Lena", "Marie", "Lea", "Clara", - // French - "Gabriel", "Raphael", "Leo", "Louis", "Lucas", "Adam", "Hugo", "Jules", "Arthur", "Nathan", - "Louise", "Alice", "Chloe", "Ines", "Lea", "Manon", "Rose", "Anna", "Lina", "Mila", - // Spanish - "Hugo", "Martin", "Lucas", "Daniel", "Pablo", "Alejandro", "Adrian", "Alvaro", "David", "Mario", - "Lucia", "Sofia", "Maria", "Martina", "Paula", "Julia", "Daniela", "Valeria", "Alba", "Emma", - // Italian - "Leonardo", "Francesco", "Alessandro", "Lorenzo", "Mattia", "Andrea", "Gabriele", "Riccardo", "Tommaso", "Edoardo", - "Sofia", "Giulia", "Aurora", "Alice", "Ginevra", "Emma", "Giorgia", "Greta", "Beatrice", "Anna" - ]; - - public static readonly string[] EuropeanLastNames = - [ - // British - "Smith", "Jones", "Williams", "Brown", "Taylor", "Davies", "Wilson", "Evans", "Thomas", "Johnson", - "Roberts", "Walker", "Wright", "Robinson", "Thompson", "White", "Hughes", "Edwards", "Green", "Hall", - // German - "Mueller", "Schmidt", "Schneider", "Fischer", "Weber", "Meyer", "Wagner", "Becker", "Schulz", "Hoffmann", - "Schaefer", "Koch", "Bauer", "Richter", "Klein", "Wolf", "Schroeder", "Neumann", "Schwarz", "Zimmermann", - // French - "Martin", "Bernard", "Dubois", "Thomas", "Robert", "Richard", "Petit", "Durand", "Leroy", "Moreau", - "Simon", "Laurent", "Lefebvre", "Michel", "Garcia", "David", "Bertrand", "Roux", "Vincent", "Fournier", - // Spanish - "Garcia", "Rodriguez", "Martinez", "Lopez", "Sanchez", "Gonzalez", "Perez", "Martin", "Gomez", "Ruiz", - "Hernandez", "Jimenez", "Diaz", "Moreno", "Alvarez", "Munoz", "Romero", "Alonso", "Gutierrez", "Navarro", - // Italian - "Rossi", "Russo", "Ferrari", "Esposito", "Bianchi", "Romano", "Colombo", "Ricci", "Marino", "Greco", - "Bruno", "Gallo", "Conti", "DeLuca", "Costa", "Giordano", "Mancini", "Rizzo", "Lombardi", "Moretti" - ]; - - public static readonly string[] AllFirstNames = [.. UsFirstNames, .. EuropeanFirstNames]; - - public static readonly string[] AllLastNames = [.. UsLastNames, .. EuropeanLastNames]; -} diff --git a/util/Seeder/Options/OrganizationVaultOptions.cs b/util/Seeder/Options/OrganizationVaultOptions.cs index 0ebe79cf09..ff1be02f7c 100644 --- a/util/Seeder/Options/OrganizationVaultOptions.cs +++ b/util/Seeder/Options/OrganizationVaultOptions.cs @@ -54,4 +54,10 @@ public class OrganizationVaultOptions /// (25% VeryWeak, 30% Weak, 25% Fair, 15% Strong, 5% VeryStrong). /// public PasswordStrength PasswordStrength { get; init; } = PasswordStrength.Realistic; + + /// + /// Geographic region for culturally-appropriate name generation in cipher usernames. + /// Defaults to Global (mixed locales from all regions). + /// + public GeographicRegion? Region { get; init; } } diff --git a/util/Seeder/Recipes/OrganizationWithVaultRecipe.cs b/util/Seeder/Recipes/OrganizationWithVaultRecipe.cs index b183fc71d3..43ea0aa156 100644 --- a/util/Seeder/Recipes/OrganizationWithVaultRecipe.cs +++ b/util/Seeder/Recipes/OrganizationWithVaultRecipe.cs @@ -96,7 +96,7 @@ public class OrganizationWithVaultRecipe( var collectionIds = CreateCollections(organization.Id, orgKeys.Key, options.StructureModel, confirmedOrgUserIds); CreateGroups(organization.Id, options.Groups, confirmedOrgUserIds); - CreateCiphers(organization.Id, orgKeys.Key, collectionIds, options.Ciphers, options.UsernamePattern, options.PasswordStrength); + CreateCiphers(organization.Id, orgKeys.Key, collectionIds, options.Ciphers, options.UsernamePattern, options.PasswordStrength, options.Region); return organization.Id; } @@ -170,10 +170,11 @@ public class OrganizationWithVaultRecipe( List collectionIds, int cipherCount, UsernamePatternType usernamePattern, - PasswordStrength passwordStrength) + PasswordStrength passwordStrength, + GeographicRegion? region) { var companies = Companies.All; - var usernameGenerator = new UsernameGenerator(organizationId.GetHashCode(), usernamePattern); + var usernameGenerator = new CipherUsernameGenerator(organizationId.GetHashCode(), usernamePattern, region); var cipherList = Enumerable.Range(0, cipherCount) .Select(i => diff --git a/util/Seeder/Seeder.csproj b/util/Seeder/Seeder.csproj index fd6e26c1ee..b38c2cf1e1 100644 --- a/util/Seeder/Seeder.csproj +++ b/util/Seeder/Seeder.csproj @@ -19,6 +19,10 @@ + + + +