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