From 186a7487bc5ae55c0674b7bf13b14a1b0c32dac5 Mon Sep 17 00:00:00 2001 From: Erick Yondon <8766776+erdembayar@users.noreply.github.com> Date: Wed, 20 Dec 2023 14:36:04 -0800 Subject: [PATCH] Add new interface ITyposquattingServiceHelper --- .../Services/ITyposquattingServiceHelper.cs | 15 ++ .../App_Start/DefaultDependenciesModule.cs | 31 ++- src/NuGetGallery/NuGetGallery.csproj | 3 +- .../Services/NullTyposquattingService.cs | 23 ++ .../TyposquattingDistanceCalculation.cs | 213 ---------------- .../Services/TyposquattingService.cs | 44 +--- .../TyposquattingStringNormalization.cs | 86 ------- .../NuGetGallery.Core.Facts.csproj | 3 +- .../TestTyposquattingServiceHelper.cs | 54 +++++ .../Services/TyposquattingServiceFacts.cs | 228 +----------------- 10 files changed, 141 insertions(+), 559 deletions(-) create mode 100644 src/NuGetGallery.Core/Services/ITyposquattingServiceHelper.cs create mode 100644 src/NuGetGallery/Services/NullTyposquattingService.cs delete mode 100644 src/NuGetGallery/Services/TyposquattingDistanceCalculation.cs delete mode 100644 src/NuGetGallery/Services/TyposquattingStringNormalization.cs create mode 100644 tests/NuGetGallery.Core.Facts/Utilities/TestTyposquattingServiceHelper.cs diff --git a/src/NuGetGallery.Core/Services/ITyposquattingServiceHelper.cs b/src/NuGetGallery.Core/Services/ITyposquattingServiceHelper.cs new file mode 100644 index 0000000000..2165cadcbf --- /dev/null +++ b/src/NuGetGallery.Core/Services/ITyposquattingServiceHelper.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace NuGetGallery +{ + /// + /// This interface for providing additional methods for ITyposquattingService. + /// + public interface ITyposquattingServiceHelper + { + string NormalizeString(string packageId); + bool IsDistanceLessThanOrEqualToThreshold(string normalizedUploadedPackageId, string normalizedPackageId, int threshold); + int GetThreshold(string packageId); + } +} \ No newline at end of file diff --git a/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs b/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs index f001a03f86..c09f3e9aaf 100644 --- a/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs +++ b/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs @@ -54,7 +54,6 @@ using NuGetGallery.Infrastructure.Mail; using NuGetGallery.Infrastructure.Search; using NuGetGallery.Infrastructure.Search.Correlation; -using NuGetGallery.Login; using NuGetGallery.Security; using NuGetGallery.Services; using Role = NuGet.Services.Entities.Role; @@ -407,6 +406,8 @@ protected override void Load(ContainerBuilder builder) .AsSelf() .As() .InstancePerLifetimeScope(); + + RegisterTyposquattingServiceHelper(builder, loggerFactory); builder.RegisterType() .AsSelf() @@ -1587,5 +1588,33 @@ private static void RegisterCookieComplianceService(ConfigurationService configu CookieComplianceService.Initialize(service ?? new NullCookieComplianceService(), logger); } + + private static void RegisterTyposquattingServiceHelper(ContainerBuilder builder, ILoggerFactory loggerFactory) + { + var logger = loggerFactory.CreateLogger(nameof(ITyposquattingServiceHelper)); + + builder.Register(c => + { + var typosquattingService = GetAddInServices(sp => + { + sp.ComposeExportedValue(logger); + }).FirstOrDefault(); + + if (typosquattingService == null) + { + typosquattingService = new NullTyposquattingServiceHelper(); + logger.LogWarning("No typosquatting service helper was found, using NullTyposquattingServiceHelper instead."); + } + else + { + logger.LogWarning("ITyposquattingServiceHelper found."); + } + + return typosquattingService; + }) + .AsSelf() + .As() + .SingleInstance(); + } } } \ No newline at end of file diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj index acdb141ad1..b2207fdae8 100644 --- a/src/NuGetGallery/NuGetGallery.csproj +++ b/src/NuGetGallery/NuGetGallery.csproj @@ -350,6 +350,7 @@ + @@ -649,8 +650,6 @@ - - diff --git a/src/NuGetGallery/Services/NullTyposquattingService.cs b/src/NuGetGallery/Services/NullTyposquattingService.cs new file mode 100644 index 0000000000..37d88aa52d --- /dev/null +++ b/src/NuGetGallery/Services/NullTyposquattingService.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace NuGetGallery.Services +{ + public class NullTyposquattingServiceHelper : ITyposquattingServiceHelper + { + public int GetThreshold(string packageId) + { + return 0; + } + + public bool IsDistanceLessThanOrEqualToThreshold(string normalizedUploadedPackageId, string normalizedPackageId, int threshold) + { + return normalizedUploadedPackageId == normalizedPackageId; + } + + public string NormalizeString(string packageId) + { + return packageId.ToLowerInvariant(); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Services/TyposquattingDistanceCalculation.cs b/src/NuGetGallery/Services/TyposquattingDistanceCalculation.cs deleted file mode 100644 index 4626acb31e..0000000000 --- a/src/NuGetGallery/Services/TyposquattingDistanceCalculation.cs +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; - -namespace NuGetGallery -{ - public static class TyposquattingDistanceCalculation - { - private const char PlaceholderForAlignment = '*'; // This const place holder variable is used for strings alignment - - private static readonly HashSet SpecialCharacters = new HashSet { '.', '_', '-' }; - private static readonly string SpecialCharactersToString = "[" + new string(SpecialCharacters.ToArray()) + "]"; - - private class BasicEditDistanceInfo - { - public int Distance { get; set; } - public PathInfo[,] Path { get; set; } - } - - private enum PathInfo - { - Match, - Delete, - Substitute, - Insert, - } - - public static bool IsDistanceLessThanOrEqualToThreshold(string str1, string str2, int threshold) - { - if (str1 == null) - { - throw new ArgumentNullException(nameof(str1)); - } - if (str2 == null) - { - throw new ArgumentNullException(nameof(str2)); - } - - var newStr1 = RegexEx.ReplaceWithTimeout(str1, SpecialCharactersToString, string.Empty, RegexOptions.None); - var newStr2 = RegexEx.ReplaceWithTimeout(str2, SpecialCharactersToString, string.Empty, RegexOptions.None); - if (Math.Abs(newStr1.Length - newStr2.Length) > threshold) - { - return false; - } - - return GetDistance(str1, str2, threshold) <= threshold; - } - - private static int GetDistance(string str1, string str2, int threshold) - { - var basicEditDistanceInfo = GetBasicEditDistanceWithPath(str1, str2); - if (basicEditDistanceInfo.Distance <= threshold) - { - return basicEditDistanceInfo.Distance; - } - var alignedStrings = TraceBackAndAlignStrings(basicEditDistanceInfo.Path, str1, str2); - var refreshedEditDistance = RefreshEditDistance(alignedStrings[0], alignedStrings[1], basicEditDistanceInfo.Distance); - - return refreshedEditDistance; - } - - /// - /// The following function is used to calculate the classical edit distance and construct the path in dynamic programming way. - /// - private static BasicEditDistanceInfo GetBasicEditDistanceWithPath(string str1, string str2) - { - var distances = new int[str1.Length + 1, str2.Length + 1]; - var path = new PathInfo[str1.Length + 1, str2.Length + 1]; - distances[0, 0] = 0; - path[0, 0] = PathInfo.Match; - for (var i = 1; i <= str1.Length; i++) - { - distances[i, 0] = i; - path[i, 0] = PathInfo.Delete; - } - - for (var j = 1; j <= str2.Length; j++) - { - distances[0, j] = j; - path[0, j] = PathInfo.Insert; - } - - for (var i = 1; i <= str1.Length; i++) - { - for (var j = 1; j <= str2.Length; j++) - { - if (str1[i - 1] == str2[j - 1]) - { - distances[i, j] = distances[i - 1, j - 1]; - path[i, j] = PathInfo.Match; - } - else - { - distances[i, j] = distances[i - 1, j - 1] + 1; - path[i, j] = PathInfo.Substitute; - - if (distances[i - 1, j] + 1 < distances[i, j]) - { - distances[i, j] = distances[i - 1, j] + 1; - path[i, j] = PathInfo.Delete; - } - - if (distances[i, j - 1] + 1 < distances[i, j]) - { - distances[i, j] = distances[i, j - 1] + 1; - path[i, j] = PathInfo.Insert; - } - } - } - } - - return new BasicEditDistanceInfo - { - Distance = distances[str1.Length, str2.Length], - Path = path - }; - } - - /// - /// The following function is used to traceback based on the construction path and align two strings. - /// Example: For two strings: "asp.net" "aspnet". After traceback and alignment, we will have aligned strings as "asp.net" "asp*net" ('*' is the placeholder). - /// The returned strings contain the two inputted strings after alignment. - /// - private static string[] TraceBackAndAlignStrings(PathInfo[,] path, string str1, string str2) - { - var newStr1 = new StringBuilder(str1); - var newStr2 = new StringBuilder(str2); - var alignedStrs = new string[2]; - - var i = str1.Length; - var j = str2.Length; - while (i > 0 && j > 0) - { - switch (path[i, j]) - { - case PathInfo.Match: - i--; - j--; - break; - case PathInfo.Substitute: - i--; - j--; - break; - case PathInfo.Delete: - newStr2.Insert(j, PlaceholderForAlignment); - i--; - break; - case PathInfo.Insert: - newStr1.Insert(i, PlaceholderForAlignment); - j--; - break; - default: - throw new ArgumentException("Invalidate operation for edit distance trace back: " + path[i, j]); - } - } - - for (var k = 0; k < i; k++) - { - newStr2.Insert(k, PlaceholderForAlignment); - } - - for (var k = 0; k < j; k++) - { - newStr1.Insert(k, PlaceholderForAlignment); - } - - alignedStrs[0] = newStr1.ToString(); - alignedStrs[1] = newStr2.ToString(); - - return alignedStrs; - } - - /// - /// The following function is used to refresh the edit distance based on predefined rules. (Insert/Delete special characters will not account for distance) - /// Example: For two aligned strings: "asp.net" "asp*net" ('*' is the placeholder), we will scan the two strings again and the mapping from '.' to '*' will not account for the distance. - /// So the final distance will be 0 for these two strings "asp.net" "aspnet". - /// - private static int RefreshEditDistance(string alignedStr1, string alignedStr2, int basicEditDistance) - { - if (alignedStr1.Length != alignedStr2.Length) - { - throw new ArgumentException("The lengths of two aligned strings are not same!"); - } - - var sameSubstitution = 0; - for (var i = 0; i < alignedStr2.Length; i++) - { - if (alignedStr1[i] != alignedStr2[i]) - { - if (alignedStr1[i] == PlaceholderForAlignment && SpecialCharacters.Contains(alignedStr2[i])) - { - sameSubstitution += 1; - } - else if (alignedStr2[i] == PlaceholderForAlignment && SpecialCharacters.Contains(alignedStr1[i])) - { - sameSubstitution += 1; - } - else - { - continue; - } - } - } - - return basicEditDistance - sameSubstitution; - } - } -} \ No newline at end of file diff --git a/src/NuGetGallery/Services/TyposquattingService.cs b/src/NuGetGallery/Services/TyposquattingService.cs index bb69964bb5..4af9589ce4 100644 --- a/src/NuGetGallery/Services/TyposquattingService.cs +++ b/src/NuGetGallery/Services/TyposquattingService.cs @@ -13,26 +13,21 @@ namespace NuGetGallery { public class TyposquattingService : ITyposquattingService { - private static readonly IReadOnlyList ThresholdsList = new List - { - new ThresholdInfo (lowerBound: 0, upperBound: 30, threshold: 0), - new ThresholdInfo (lowerBound: 30, upperBound: 50, threshold: 1), - new ThresholdInfo (lowerBound: 50, upperBound: 129, threshold: 2) - }; - private readonly IContentObjectService _contentObjectService; private readonly IFeatureFlagService _featureFlagService; private readonly IPackageService _packageService; private readonly IReservedNamespaceService _reservedNamespaceService; private readonly ITelemetryService _telemetryService; private readonly ITyposquattingCheckListCacheService _typosquattingCheckListCacheService; + private readonly ITyposquattingServiceHelper _typosquattingServiceHelper; public TyposquattingService(IContentObjectService contentObjectService, IFeatureFlagService featureFlagService, IPackageService packageService, IReservedNamespaceService reservedNamespaceService, ITelemetryService telemetryService, - ITyposquattingCheckListCacheService typosquattingCheckListCacheService) + ITyposquattingCheckListCacheService typosquattingCheckListCacheService, + ITyposquattingServiceHelper typosquattingServiceHelper) { _contentObjectService = contentObjectService ?? throw new ArgumentNullException(nameof(contentObjectService)); _featureFlagService = featureFlagService ?? throw new ArgumentNullException(nameof(featureFlagService)); @@ -40,6 +35,7 @@ public TyposquattingService(IContentObjectService contentObjectService, _reservedNamespaceService = reservedNamespaceService ?? throw new ArgumentNullException(nameof(reservedNamespaceService)); _telemetryService = telemetryService ?? throw new ArgumentNullException(nameof(telemetryService)); _typosquattingCheckListCacheService = typosquattingCheckListCacheService ?? throw new ArgumentNullException(nameof(typosquattingCheckListCacheService)); + _typosquattingServiceHelper = typosquattingServiceHelper; } public bool IsUploadedPackageIdTyposquatting(string uploadedPackageId, User uploadedPackageOwner, out List typosquattingCheckCollisionIds) @@ -70,13 +66,13 @@ public bool IsUploadedPackageIdTyposquatting(string uploadedPackageId, User uplo _telemetryService.TrackMetricForTyposquattingChecklistRetrievalTime(uploadedPackageId, checklistRetrievalStopwatch.Elapsed); var algorithmProcessingStopwatch = Stopwatch.StartNew(); - var threshold = GetThreshold(uploadedPackageId); - var normalizedUploadedPackageId = TyposquattingStringNormalization.NormalizeString(uploadedPackageId); + int threshold = _typosquattingServiceHelper.GetThreshold(uploadedPackageId); + string normalizedUploadedPackageId = _typosquattingServiceHelper.NormalizeString(uploadedPackageId); var collisionIds = new ConcurrentBag(); Parallel.ForEach(packageIdsCheckList, (packageId, loopState) => { - string normalizedPackageId = TyposquattingStringNormalization.NormalizeString(packageId); - if (TyposquattingDistanceCalculation.IsDistanceLessThanOrEqualToThreshold(normalizedUploadedPackageId, normalizedPackageId, threshold)) + string normalizedPackageId = _typosquattingServiceHelper.NormalizeString(packageId); + if (_typosquattingServiceHelper.IsDistanceLessThanOrEqualToThreshold(normalizedUploadedPackageId, normalizedPackageId, threshold)) { collisionIds.Add(packageId); } @@ -138,29 +134,5 @@ public bool IsUploadedPackageIdTyposquatting(string uploadedPackageId, User uplo return wasUploadBlocked; } - private static int GetThreshold(string packageId) - { - foreach (var thresholdInfo in ThresholdsList) - { - if (packageId.Length >= thresholdInfo.LowerBound && packageId.Length < thresholdInfo.UpperBound) - { - return thresholdInfo.Threshold; - } - } - - throw new ArgumentException(String.Format("There is no predefined typo-squatting threshold for this package Id: {0}", packageId)); - } - } - public class ThresholdInfo - { - public int LowerBound { get; } - public int UpperBound { get; } - public int Threshold { get; } - public ThresholdInfo(int lowerBound, int upperBound, int threshold) - { - LowerBound = lowerBound; - UpperBound = upperBound; - Threshold = threshold; - } } } \ No newline at end of file diff --git a/src/NuGetGallery/Services/TyposquattingStringNormalization.cs b/src/NuGetGallery/Services/TyposquattingStringNormalization.cs deleted file mode 100644 index 82b97fd2cf..0000000000 --- a/src/NuGetGallery/Services/TyposquattingStringNormalization.cs +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Text; -using System.Globalization; -using System.Collections.Generic; - -namespace NuGetGallery -{ - public static class TyposquattingStringNormalization - { - /// - /// The following dictionary is built through picking up similar characters manually from wiki unicode page. - /// https://en.wikipedia.org/wiki/List_of_Unicode_characters - /// - private static readonly IReadOnlyDictionary SimilarCharacterDictionary = new Dictionary() - { - { "a", "AΑАαаÀÁÂÃÄÅàáâãäåĀāĂ㥹ǍǎǞǟǠǡǺǻȀȁȂȃȦȧȺΆάἀἁἂἃἄἅἆἇἈἉἊἋἌΆἍἎἏӐӑӒӓὰάᾀᾁᾂᾃᾄᾅᾆᾇᾈᾊᾋᾌᾍᾎᾏᾰᾱᾲᾳᾴᾶᾷᾸᾹᾺᾼДд"}, - { "b", "BΒВЪЬƀƁƂƃƄƅɃḂḃϦЂБвъьѢѣҌҍႦႪხҔҕӃӄ"}, - { "c", "CСсϹϲÇçĆćĈĉĊċČčƇƈȻȼҪҫ𐒨"}, - { "d", "DƊԁÐĎďĐđƉƋƌǷḊḋԀԂԃ"}, - { "e", "EΕЕеÈÉÊËèéêëĒēĔĕĖėĘęĚěȄȅȆȇȨȩɆɇΈЀЁЄѐёҼҽҾҿӖӗἘἙἚἛἜἝῈΈ"}, - { "f", "FϜƑƒḞḟϝҒғӺӻ"}, - { "g", "GǤԌĜĝĞğĠġĢģƓǥǦǧǴǵԍ"}, - { "h", "HΗНһհҺĤĥħǶȞȟΉἨἩἪἫἬἭἮἯᾘᾙᾚᾛᾜᾝᾞᾟῊΉῌЋнћҢңҤҥӇӈӉӊԊԋԦԧԨԩႬႹ𐒅𐒌𐒎𐒣"}, - { "i", "IΙІӀ¡ìíîïǐȉȋΐίιϊіїὶίῐῑῒΐῖῗΊΪȊȈἰἱἲἳἴἵἶἷἸἹἺἻἼἽἾἿῘῙῚΊЇӏÌÍÎÏĨĩĪīĬĭĮįİǏ"}, - { "j", "JЈͿϳĴĵǰȷ"}, - { "k", "KΚКKĶķĸƘƙǨǩκϏЌкќҚқҜҝҞҟҠҡԞԟ"}, - { "l", "LĹĺĻļĽľĿŀŁłſƖƪȴẛ"}, - { "m", "MΜМṀṁϺϻмӍӎ𐒄"}, - { "n", "NΝпÑñŃńŅņŇňʼnƝǸǹᾐᾑᾒᾓᾔᾕᾖᾗῂῃῄῆῇԤԥԮԯ𐒐"}, - { "o", "OΟОՕჿоοÒÓÔÕÖðòóôõöøŌōŎŏŐőƠơǑǒǪǫǬǭȌȍȎȏȪȫȬȭȮȯȰȱΌδόϘϙὀὁὂὃὄὅὈὉὊὋὌὍὸόῸΌӦӧჾ𐒆𐒠0"}, - { "p", "PΡРрρÞþƤƥƿṖṗϷϸῤῥῬҎҏႲႼ"}, - { "q", "QգԛȡɊɋԚႭႳ"}, - { "r", "RгŔŕŖŗŘřƦȐȑȒȓɌɼѓ"}, - { "s", "SЅѕՏႽჽŚśŜŝŞşŠšȘșȿṠṡ𐒖𐒡"}, - { "t", "TΤТͲͳŢţŤťŦŧƬƭƮȚțȾṪṫτтҬҭէ"}, - { "u", "UՍႮÙÚÛÜùúûüŨũŪūŬŭŮůŰűŲųƯưǓǔǕǖǗǘǙǚǛǜȔȕȖȗμυϋύὐὑὒὓὔὕὖὗὺύῠῡῢΰῦῧ𐒩"}, - { "v", "VνѴѵƔƲѶѷ"}, - { "w", "WωшԜԝŴŵƜẀẁẂẃẄẅώШЩщѡѿὠὡὢὣὤὥὦὧὼώᾠᾡᾢᾣᾤᾥᾦᾧῲῳῴῶῷ"}, - { "x", "XХΧх×χҲҳӼӽӾӿჯ"}, - { "y", "YΥҮƳуУÝýÿŶŷŸƴȲȳɎɏỲỳΎΫγϒϓϔЎЧўүҶҷҸҹӋӌӮӯӰӱӲӳӴӵὙὛὝὟῨῩῪΎႯႸ𐒋𐒦"}, - { "z", "ZΖჍŹźŻżŽžƵƶȤȥ"}, - { "3", "ƷЗʒӡჳǮǯȜȝзэӞӟӠ"}, - { "8", "Ȣȣ"}, - { "_", ".-" } - }; - - private static readonly IReadOnlyDictionary NormalizedMappingDictionary = GetNormalizedMappingDictionary(SimilarCharacterDictionary); - - public static string NormalizeString(string str) - { - var normalizedString = new StringBuilder(); - var textElementEnumerator = StringInfo.GetTextElementEnumerator(str); - while (textElementEnumerator.MoveNext()) - { - var textElement = textElementEnumerator.GetTextElement(); - if (NormalizedMappingDictionary.TryGetValue(textElement, out var normalizedTextElement)) - { - normalizedString.Append(normalizedTextElement); - } - else - { - normalizedString.Append(textElement); - } - } - - return normalizedString.ToString(); - } - - private static Dictionary GetNormalizedMappingDictionary(IReadOnlyDictionary similarCharacterDictionary) - { - var normalizedMappingDictionary = new Dictionary(); - foreach (var item in similarCharacterDictionary) - { - var textElementEnumerator = StringInfo.GetTextElementEnumerator(item.Value); - while (textElementEnumerator.MoveNext()) - { - normalizedMappingDictionary[textElementEnumerator.GetTextElement()] = item.Key; - } - } - - return normalizedMappingDictionary; - } - } -} \ No newline at end of file diff --git a/tests/NuGetGallery.Core.Facts/NuGetGallery.Core.Facts.csproj b/tests/NuGetGallery.Core.Facts/NuGetGallery.Core.Facts.csproj index ebe89cf044..9124be526b 100644 --- a/tests/NuGetGallery.Core.Facts/NuGetGallery.Core.Facts.csproj +++ b/tests/NuGetGallery.Core.Facts/NuGetGallery.Core.Facts.csproj @@ -1,4 +1,4 @@ - + Debug @@ -130,6 +130,7 @@ + diff --git a/tests/NuGetGallery.Core.Facts/Utilities/TestTyposquattingServiceHelper.cs b/tests/NuGetGallery.Core.Facts/Utilities/TestTyposquattingServiceHelper.cs new file mode 100644 index 0000000000..8b1e32c3c8 --- /dev/null +++ b/tests/NuGetGallery.Core.Facts/Utilities/TestTyposquattingServiceHelper.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace NuGetGallery.TestUtils +{ + // TODO : below would be removed when this is addressed https://github.com/NuGet/Engineering/issues/5176 + public class TestTyposquattingServiceHelper : ITyposquattingServiceHelper + { + private static readonly IReadOnlyList ThresholdsList = new List + { + new ThresholdInfo (lowerBound: 0, upperBound: 30, threshold: 0), + new ThresholdInfo (lowerBound: 30, upperBound: 50, threshold: 1), + new ThresholdInfo (lowerBound: 50, upperBound: 129, threshold: 2) + }; + + public int GetThreshold(string packageId) + { + foreach (var thresholdInfo in ThresholdsList) + { + if (packageId.Length >= thresholdInfo.LowerBound && packageId.Length < thresholdInfo.UpperBound) + { + return thresholdInfo.Threshold; + } + } + throw new ArgumentException(String.Format("There is no predefined typo-squatting threshold for this package Id: {0}", packageId)); + } + + public bool IsDistanceLessThanOrEqualToThreshold(string normalizedUploadedPackageId, string normalizedPackageId, int threshold) + { + return normalizedUploadedPackageId == normalizedPackageId; + } + + public string NormalizeString(string packageId) + { + return packageId.ToLowerInvariant(); + } + + private class ThresholdInfo + { + public int LowerBound { get; } + public int UpperBound { get; } + public int Threshold { get; } + public ThresholdInfo(int lowerBound, int upperBound, int threshold) + { + LowerBound = lowerBound; + UpperBound = upperBound; + Threshold = threshold; + } + } + } +} diff --git a/tests/NuGetGallery.Facts/Services/TyposquattingServiceFacts.cs b/tests/NuGetGallery.Facts/Services/TyposquattingServiceFacts.cs index 8b017e934f..c9964fc060 100644 --- a/tests/NuGetGallery.Facts/Services/TyposquattingServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/TyposquattingServiceFacts.cs @@ -3,11 +3,11 @@ using System; using System.Linq; -using System.Globalization; using System.Collections.Generic; using Moq; using Xunit; using NuGet.Services.Entities; +using NuGetGallery.TestUtils; namespace NuGetGallery { @@ -100,7 +100,8 @@ private static ITyposquattingService CreateService( packageService.Object, reservedNamespaceService.Object, telemetryService.Object, - typosquattingCheckListCacheService.Object); + typosquattingCheckListCacheService.Object, + new TestTyposquattingServiceHelper()); } [Fact] @@ -110,7 +111,7 @@ public void CheckNotTyposquattingByDifferentOwnersTest() var uploadedPackageId = "new_package_for_testing"; var newService = CreateService(); - + // Act var typosquattingCheckResult = newService.IsUploadedPackageIdTyposquatting(uploadedPackageId, _uploadedPackageOwner, out List typosquattingCheckCollisionIds); @@ -137,93 +138,6 @@ public void CheckNotTyposquattingBySameOwnersTest() Assert.Equal(0, typosquattingCheckCollisionIds.Count); } - [Fact] - public void CheckIsTyposquattingByDifferentOwnersTest() - { - // Arrange - var uploadedPackageId = "Mícrosoft.NetFramew0rk.v1"; - var newService = CreateService(); - - // Act - var typosquattingCheckResult = newService.IsUploadedPackageIdTyposquatting(uploadedPackageId, _uploadedPackageOwner, out List typosquattingCheckCollisionIds); - - // Assert - Assert.True(typosquattingCheckResult); - Assert.Equal(1, typosquattingCheckCollisionIds.Count); - Assert.Equal("microsoft_netframework_v1", typosquattingCheckCollisionIds[0]); - } - - [Fact] - public void CheckIsTyposquattingMultiCollisionsWithoutSameUser() - { - // Arrange - var uploadedPackageId = "microsoft_netframework.v1"; - var pacakgeRegistrationsList = PacakgeRegistrationsList.Concat(new PackageRegistration[] - { - new PackageRegistration { - Id = "microsoft-netframework-v1", - DownloadCount = new Random().Next(0, 10000), - IsVerified = true, - Owners = new List { new User() { Username = string.Format("owner{0}", _packageIds.Count() + 2), Key = _packageIds.Count() + 2} } - } - }); - var mockPackageService = new Mock(); - mockPackageService - .Setup(x => x.GetAllPackageRegistrations()) - .Returns(pacakgeRegistrationsList); - var mockTyposquattingCheckListCacheService = new Mock(); - mockTyposquattingCheckListCacheService - .Setup(x => x.GetTyposquattingCheckList(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(pacakgeRegistrationsList.Select(pr => pr.Id).ToList()); - - var newService = CreateService(packageService: mockPackageService, typosquattingCheckListCacheService: mockTyposquattingCheckListCacheService); - - // Act - var typosquattingCheckResult = newService.IsUploadedPackageIdTyposquatting(uploadedPackageId, _uploadedPackageOwner, out List typosquattingCheckCollisionIds); - - // Assert - Assert.True(typosquattingCheckResult); - Assert.Equal(2, typosquattingCheckCollisionIds.Count); - } - - [Fact] - public void CheckNotTyposquattingMultiCollisionsWithSameUsers() - { - // Arrange - var uploadedPackageId = "microsoft_netframework.v1"; - _uploadedPackageOwner.Username = "owner1"; - _uploadedPackageOwner.Key = 1; - var pacakgeRegistrationsList = PacakgeRegistrationsList.Concat(new PackageRegistration[] - { - new PackageRegistration() - { - Id = "microsoft-netframework-v1", - DownloadCount = new Random().Next(0, 10000), - IsVerified = true, - Owners = new List { new User() { Username = string.Format("owner{0}", _packageIds.Count() + 2), Key = _packageIds.Count() + 2 } } - } - }); - - var mockPackageService = new Mock(); - mockPackageService - .Setup(x => x.GetAllPackageRegistrations()) - .Returns(pacakgeRegistrationsList); - var mockTyposquattingCheckListCacheService = new Mock(); - mockTyposquattingCheckListCacheService - .Setup(x => x.GetTyposquattingCheckList(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(pacakgeRegistrationsList.Select(pr => pr.Id).ToList()); - - var newService = CreateService(packageService: mockPackageService, typosquattingCheckListCacheService: mockTyposquattingCheckListCacheService); - - // Act - var typosquattingCheckResult = newService.IsUploadedPackageIdTyposquatting(uploadedPackageId, _uploadedPackageOwner, out List typosquattingCheckCollisionIds); - - // Assert - Assert.False(typosquattingCheckResult); - Assert.Equal(1, typosquattingCheckCollisionIds.Count); - Assert.Equal("microsoft-netframework-v1", typosquattingCheckCollisionIds[0]); - } - [Fact] public void CheckNotTyposquattingWithinReservedNameSpace() { @@ -233,7 +147,7 @@ public void CheckNotTyposquattingWithinReservedNameSpace() var mockReservedNamespaceService = new Mock(); mockReservedNamespaceService .Setup(x => x.GetReservedNamespacesForId(It.IsAny())) - .Returns(new List { new ReservedNamespace()}); + .Returns(new List { new ReservedNamespace() }); var newService = CreateService(reservedNamespaceService: mockReservedNamespaceService); @@ -309,7 +223,7 @@ public void CheckTyposquattingEmptyChecklist() .Returns(new List()); var newService = CreateService(packageService: mockPackageService, typosquattingCheckListCacheService: mockTyposquattingCheckListCacheService); - + // Act var typosquattingCheckResult = newService.IsUploadedPackageIdTyposquatting(uploadedPackageId, _uploadedPackageOwner, out List typosquattingCheckCollisionIds); @@ -361,7 +275,7 @@ public void CheckNotTyposquattingBlockUserNotEnabled() .Returns(20000); var newService = CreateService(contentObjectService: mockContentObjectService); - + // Act var typosquattingCheckResult = newService.IsUploadedPackageIdTyposquatting(uploadedPackageId, _uploadedPackageOwner, out List typosquattingCheckCollisionIds); @@ -385,7 +299,7 @@ public void CheckIsTyposquattingBlockUserNotEnabled() .Returns(false); var newService = CreateService(featureFlagService: featureFlagService); - + // Act var typosquattingCheckResult = newService.IsUploadedPackageIdTyposquatting(uploadedPackageId, _uploadedPackageOwner, out List typosquattingCheckCollisionIds); @@ -430,132 +344,6 @@ public void CheckTelemetryServiceLogOriginalUploadedPackageId() It.IsAny(), It.IsAny()), Times.Once); - - mockTelemetryService.Verify( - x => x.TrackMetricForTyposquattingOwnersCheckTime(uploadedPackageId, It.IsAny()), - Times.Once); - } - - [Theory] - [InlineData("Microsoft_NetFramework_v1", "Microsoft.NetFramework.v1", 0)] - [InlineData("Microsoft_NetFramework_v1", "microsoft-netframework-v1", 0)] - [InlineData("Microsoft_NetFramework_v1", "MicrosoftNetFrameworkV1", 0)] - [InlineData("Microsoft_NetFramework_v1", "Mícr0s0ft_NetFrάmѐw0rk_v1", 0)] - [InlineData("Dotnet.Script.Core.RoslynDependencies", "dotnet-script-core-rõslyndependencies", 1)] - [InlineData("Dotnet.Script.Core.RoslynDependencies", "DotnetScriptCoreRoslyndependncies", 1)] - [InlineData("MichaelBrandonMorris.Extensions.CollectionExtensions", "Michaelbrandonmorris.Extension.CollectionExtension", 2)] - [InlineData("MichaelBrandonMorris.Extensions.CollectionExtensions", "MichaelBrandonMoris_Extensions_CollectionExtension", 2)] - public void CheckTyposquattingDistance(string str1, string str2, int threshold) - { - // Arrange - str1 = TyposquattingStringNormalization.NormalizeString(str1); - str2 = TyposquattingStringNormalization.NormalizeString(str2); - - // Act - var checkResult = TyposquattingDistanceCalculation.IsDistanceLessThanOrEqualToThreshold(str1, str2, threshold); - - // Assert - Assert.True(checkResult); - } - - [Theory] - [InlineData("Lappa.ORM", "JCTools.I18N", 0)] - [InlineData("Cake.Intellisense.Core", "Cake.IntellisenseGenerator", 0)] - [InlineData("Hangfire.Net40", "Hangfire.SqlServer.Net40", 0)] - [InlineData("LogoFX.Client.Tests.Integration.SpecFlow.Core", "LogoFX.Client.Testing.EndToEnd.SpecFlow", 1)] - [InlineData("cordova-plugin-ms-adal.TypeScript.DefinitelyTyped", "eonasdan-bootstrap-datetimepicker.TypeScript.DefinitelyTyped", 2)] - public void CheckNotTyposquattingDistance(string str1, string str2, int threshold) - { - // Arrange - str1 = TyposquattingStringNormalization.NormalizeString(str1); - str2 = TyposquattingStringNormalization.NormalizeString(str2); - - // Act - var checkResult = TyposquattingDistanceCalculation.IsDistanceLessThanOrEqualToThreshold(str1, str2, threshold); - - // Assert - Assert.False(checkResult); - } - - [Theory] - [InlineData("ă", "a")] - [InlineData("aă", "aa")] - [InlineData("aăăa", "aaaa")] - [InlineData("𐒎", "h")] - [InlineData("h𐒎", "hh")] - [InlineData("h𐒎𐒎h", "hhhh")] - [InlineData("aă𐒎a", "aaha")] - [InlineData("a𐒎ăa", "ahaa")] - [InlineData("aă𐒎ăa", "aahaa")] - [InlineData("a𐒎ă𐒎a", "ahaha")] - [InlineData("aă𐒎ă𐒎a", "aahaha")] - [InlineData("aă𐒎𐒎ăă𐒎a", "aahhaaha")] - [InlineData("aă𐒎𐒎a𐒎ăă𐒎ă𐒎a", "aahhahaahaha")] - [InlineData("Microsoft_NetFramework_v1", "microsoft_netframework_v1")] - [InlineData("Microsoft.netframework-v1", "microsoft_netframework_v1")] - [InlineData("mícr0s0ft.nёtFrǎmȇwὀrk.v1", "microsoft_netframework_v1")] - public void CheckNormalization(string str1, string str2) - { - // Arrange and Act - str1 = TyposquattingStringNormalization.NormalizeString(str1); - - // Assert - Assert.Equal(str1, str2); - } - - [Fact] - public void CheckNormalizationDictionary() - { - // Arrange - var similarCharacterDictionary = new Dictionary() - { - { "a", "AΑАαаÀÁÂÃÄÅàáâãäåĀāĂ㥹ǍǎǞǟǠǡǺǻȀȁȂȃȦȧȺΆάἀἁἂἃἄἅἆἇἈἉἊἋἌΆἍἎἏӐӑӒӓὰάᾀᾁᾂᾃᾄᾅᾆᾇᾈᾊᾋᾌᾍᾎᾏᾰᾱᾲᾳᾴᾶᾷᾸᾹᾺᾼДд"}, - { "b", "BΒВЪЬƀƁƂƃƄƅɃḂḃϦЂБвъьѢѣҌҍႦႪხҔҕӃӄ"}, - { "c", "CСсϹϲÇçĆćĈĉĊċČčƇƈȻȼҪҫ𐒨"}, - { "d", "DƊԁÐĎďĐđƉƋƌǷḊḋԀԂԃ"}, - { "e", "EΕЕеÈÉÊËèéêëĒēĔĕĖėĘęĚěȄȅȆȇȨȩɆɇΈЀЁЄѐёҼҽҾҿӖӗἘἙἚἛἜἝῈΈ"}, - { "f", "FϜƑƒḞḟϝҒғӺӻ"}, - { "g", "GǤԌĜĝĞğĠġĢģƓǥǦǧǴǵԍ"}, - { "h", "HΗНһհҺĤĥħǶȞȟΉἨἩἪἫἬἭἮἯᾘᾙᾚᾛᾜᾝᾞᾟῊΉῌЋнћҢңҤҥӇӈӉӊԊԋԦԧԨԩႬႹ𐒅𐒌𐒎𐒣"}, - { "i", "IΙІӀ¡ìíîïǐȉȋΐίιϊіїὶίῐῑῒΐῖῗΊΪȊȈἰἱἲἳἴἵἶἷἸἹἺἻἼἽἾἿῘῙῚΊЇӏÌÍÎÏĨĩĪīĬĭĮįİǏ"}, - { "j", "JЈͿϳĴĵǰȷ"}, - { "k", "KΚКKĶķĸƘƙǨǩκϏЌкќҚқҜҝҞҟҠҡԞԟ"}, - { "l", "LĹĺĻļĽľĿŀŁłſƖƪȴẛ"}, - { "m", "MΜМṀṁϺϻмӍӎ𐒄"}, - { "n", "NΝпÑñŃńŅņŇňʼnƝǸǹᾐᾑᾒᾓᾔᾕᾖᾗῂῃῄῆῇԤԥԮԯ𐒐"}, - { "o", "OΟОՕჿоοÒÓÔÕÖðòóôõöøŌōŎŏŐőƠơǑǒǪǫǬǭȌȍȎȏȪȫȬȭȮȯȰȱΌδόϘϙὀὁὂὃὄὅὈὉὊὋὌὍὸόῸΌӦӧჾ𐒆𐒠0"}, - { "p", "PΡРрρÞþƤƥƿṖṗϷϸῤῥῬҎҏႲႼ"}, - { "q", "QգԛȡɊɋԚႭႳ"}, - { "r", "RгŔŕŖŗŘřƦȐȑȒȓɌɼѓ"}, - { "s", "SЅѕՏႽჽŚśŜŝŞşŠšȘșȿṠṡ𐒖𐒡"}, - { "t", "TΤТͲͳŢţŤťŦŧƬƭƮȚțȾṪṫτтҬҭէ"}, - { "u", "UՍႮÙÚÛÜùúûüŨũŪūŬŭŮůŰűŲųƯưǓǔǕǖǗǘǙǚǛǜȔȕȖȗμυϋύὐὑὒὓὔὕὖὗὺύῠῡῢΰῦῧ𐒩"}, - { "v", "VνѴѵƔƲѶѷ"}, - { "w", "WωшԜԝŴŵƜẀẁẂẃẄẅώШЩщѡѿὠὡὢὣὤὥὦὧὼώᾠᾡᾢᾣᾤᾥᾦᾧῲῳῴῶῷ"}, - { "x", "XХΧх×χҲҳӼӽӾӿჯ"}, - { "y", "YΥҮƳуУÝýÿŶŷŸƴȲȳɎɏỲỳΎΫγϒϓϔЎЧўүҶҷҸҹӋӌӮӯӰӱӲӳӴӵὙὛὝὟῨῩῪΎႯႸ𐒋𐒦"}, - { "z", "ZΖჍŹźŻżŽžƵƶȤȥ"}, - { "3", "ƷЗʒӡჳǮǯȜȝзэӞӟӠ"}, - { "8", "Ȣȣ"}, - { "_", ".-" } - }; - - var testPackageName = "testpackage"; - foreach (var item in similarCharacterDictionary) - { - var textElementEnumerator = StringInfo.GetTextElementEnumerator(item.Value); - while (textElementEnumerator.MoveNext()) - { - var typoString = testPackageName + textElementEnumerator.GetTextElement(); - var baseString = testPackageName + item.Key; - - // Act - var normalizedString = TyposquattingStringNormalization.NormalizeString(typoString); - - // Assert - Assert.Equal(baseString, normalizedString); - } - } } } } \ No newline at end of file