From 5d7ccf0cc8accb415cc5dcb50cc667fdc70861f5 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Thu, 5 Jun 2025 15:48:55 +0200 Subject: [PATCH 01/46] add component type --- .../TypedComponent/ComponentType.cs | 3 + .../uv/UvLockComponentDetector.cs | 69 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ComponentType.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ComponentType.cs index 31b245d59..e6737b48b 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ComponentType.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ComponentType.cs @@ -62,4 +62,7 @@ public enum ComponentType : byte [EnumMember] DotNet = 19, + + [EnumMember] + Uv = 20, } diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs new file mode 100644 index 000000000..3978aab0f --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.Internal; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Microsoft.Extensions.Logging; +using Tomlyn; +using Tomlyn.Model; + +namespace Microsoft.ComponentDetection.Detectors.Uv +{ + public class UvLockComponentDetector : FileComponentDetector + { + public override string Id => "UvLock"; + public override IList SearchPatterns { get; } = new List { "uv.lock" }; + public override IEnumerable SupportedComponentTypes => new[] { ComponentType.Uv }; + public override int Version => 1; + public override IEnumerable Categories => new[] { "Python" }; + + protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary detectorArgs, CancellationToken cancellationToken = default) + { + var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder; + var file = processRequest.ComponentStream; + try + { + using var reader = new StreamReader(file.Stream); + var toml = await reader.ReadToEndAsync(cancellationToken); + var model = Toml.ToModel(toml); + if (model is TomlTable table) + { + // Optionally, log or use top-level metadata + var topLevelMetadata = new Dictionary(); + foreach (var key in table.Keys) + { + if (key != "package") + { + topLevelMetadata[key] = table[key]; + } + } + + if (table.TryGetValue("package", out var packagesObj) && packagesObj is TomlTableArray packages) + { + foreach (var pkgObj in packages) + { + var pkg = pkgObj as TomlTable; + var name = pkg?["name"]?.ToString(); + var version = pkg?["version"]?.ToString(); + if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(version)) + { + continue; + } + + var uvComponent = new UvComponent(name, version); + var detectedComponent = new DetectedComponent(uvComponent); + singleFileComponentRecorder.RegisterUsage(detectedComponent); + } + } + } + } + catch (Exception ex) + { + this.Logger.LogError(ex, "Failed to parse uv.lock file {File}", file.Location); + } + } + } +} From 5be938923373a8792e63477f05f2f7be074385b1 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Thu, 5 Jun 2025 15:51:57 +0200 Subject: [PATCH 02/46] metadata --- .../TypedComponent/UvComponent.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/Microsoft.ComponentDetection.Contracts/TypedComponent/UvComponent.cs diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/UvComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/UvComponent.cs new file mode 100644 index 000000000..cbf75b337 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/UvComponent.cs @@ -0,0 +1,34 @@ +namespace Microsoft.ComponentDetection.Contracts.TypedComponent; + +using System.Collections.Generic; + +public class UvComponent : TypedComponent +{ + public UvComponent() + { + this.Metadata = new Dictionary(); + } + + public UvComponent(string name, string version, Dictionary metadata = null) + { + this.Name = this.ValidateRequiredInput(name, nameof(this.Name), nameof(ComponentType.Pip)); + this.Version = this.ValidateRequiredInput(version, nameof(this.Version), nameof(ComponentType.Pip)); + this.Metadata = metadata ?? new Dictionary(); + } + + public string Name { get; set; } + + public string Version { get; set; } + + /// + /// Arbitrary metadata from the uv.lock TOML package entry (e.g., dependencies, source, wheels, sdist, etc.) + /// + public Dictionary Metadata { get; set; } + + public override ComponentType Type => ComponentType.Uv; + + protected override string ComputeId() + { + return $"{this.Name}:{this.Version}"; + } +} From 413d3e3609ffc4501d8d85a88e411145959fb8c2 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 6 Jun 2025 10:14:27 +0200 Subject: [PATCH 03/46] style --- .../TypedComponent/UvComponent.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/UvComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/UvComponent.cs index cbf75b337..6b32bdd51 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/UvComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/UvComponent.cs @@ -4,16 +4,13 @@ namespace Microsoft.ComponentDetection.Contracts.TypedComponent; public class UvComponent : TypedComponent { - public UvComponent() - { - this.Metadata = new Dictionary(); - } + public UvComponent() => this.Metadata = []; public UvComponent(string name, string version, Dictionary metadata = null) { this.Name = this.ValidateRequiredInput(name, nameof(this.Name), nameof(ComponentType.Pip)); this.Version = this.ValidateRequiredInput(version, nameof(this.Version), nameof(ComponentType.Pip)); - this.Metadata = metadata ?? new Dictionary(); + this.Metadata = metadata ?? []; } public string Name { get; set; } From 298b1d22bbcad65d37dceaf08cb13b74912fe38d Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 6 Jun 2025 10:14:43 +0200 Subject: [PATCH 04/46] start md --- docs/detectors/uv.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/detectors/uv.md diff --git a/docs/detectors/uv.md b/docs/detectors/uv.md new file mode 100644 index 000000000..2a1dba7ea --- /dev/null +++ b/docs/detectors/uv.md @@ -0,0 +1,6 @@ +# uv Detection +## Requirements +uv detection relies on a uv.lock file being present. + +## Detection strategy +uv detection is performed by parsing a uv.lock found under the scan directory. From 392e9708f0065a44cf8e830dcf08acb7adcf6682 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 6 Jun 2025 10:26:30 +0200 Subject: [PATCH 05/46] fix style --- .../uv/UvLockComponentDetector.cs | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs index 3978aab0f..6cacd99e5 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs @@ -1,34 +1,40 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.ComponentDetection.Contracts; -using Microsoft.ComponentDetection.Contracts.Internal; -using Microsoft.ComponentDetection.Contracts.TypedComponent; -using Microsoft.Extensions.Logging; -using Tomlyn; -using Tomlyn.Model; - namespace Microsoft.ComponentDetection.Detectors.Uv { + using System; + using System.Collections.Generic; + using System.IO; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.ComponentDetection.Contracts; + using Microsoft.ComponentDetection.Contracts.Internal; + using Microsoft.ComponentDetection.Contracts.TypedComponent; + using Microsoft.Extensions.Logging; + using Tomlyn; + using Tomlyn.Model; + public class UvLockComponentDetector : FileComponentDetector { public override string Id => "UvLock"; - public override IList SearchPatterns { get; } = new List { "uv.lock" }; - public override IEnumerable SupportedComponentTypes => new[] { ComponentType.Uv }; + + public override IList SearchPatterns { get; } = ["uv.lock"]; + + public override IEnumerable SupportedComponentTypes => [ComponentType.Uv]; + public override int Version => 1; - public override IEnumerable Categories => new[] { "Python" }; + + public override IEnumerable Categories => ["Python"]; protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary detectorArgs, CancellationToken cancellationToken = default) { var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder; var file = processRequest.ComponentStream; + try { using var reader = new StreamReader(file.Stream); var toml = await reader.ReadToEndAsync(cancellationToken); var model = Toml.ToModel(toml); + if (model is TomlTable table) { // Optionally, log or use top-level metadata @@ -43,9 +49,8 @@ protected override async Task OnFileFoundAsync(ProcessRequest processRequest, ID if (table.TryGetValue("package", out var packagesObj) && packagesObj is TomlTableArray packages) { - foreach (var pkgObj in packages) + foreach (var pkg in packages) { - var pkg = pkgObj as TomlTable; var name = pkg?["name"]?.ToString(); var version = pkg?["version"]?.ToString(); if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(version)) From 8cc618da74e4fb299da61fadb3c0ab949b24a4a7 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 6 Jun 2025 10:39:54 +0200 Subject: [PATCH 06/46] fix import style --- .../Extensions/ServiceCollectionExtensions.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs index 5099e5fa3..278891e2f 100644 --- a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs +++ b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs @@ -21,6 +21,7 @@ namespace Microsoft.ComponentDetection.Orchestrator.Extensions; using Microsoft.ComponentDetection.Detectors.Rust; using Microsoft.ComponentDetection.Detectors.Spdx; using Microsoft.ComponentDetection.Detectors.Swift; +using Microsoft.ComponentDetection.Detectors.Uv; using Microsoft.ComponentDetection.Detectors.Vcpkg; using Microsoft.ComponentDetection.Detectors.Yarn; using Microsoft.ComponentDetection.Detectors.Yarn.Parsers; @@ -154,6 +155,9 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s // Swift Package Manager services.AddSingleton(); + // Uv + services.AddSingleton(); + return services; } } From 772baf132cfb1e4a662624642996a60a043c3117 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 6 Jun 2025 11:59:48 +0200 Subject: [PATCH 07/46] add uv test project --- .../resources/uv/pyproject.toml | 18 + .../resources/uv/uv.lock | 497 ++++++++++++++++++ 2 files changed, 515 insertions(+) create mode 100644 test/Microsoft.ComponentDetection.VerificationTests/resources/uv/pyproject.toml create mode 100644 test/Microsoft.ComponentDetection.VerificationTests/resources/uv/uv.lock diff --git a/test/Microsoft.ComponentDetection.VerificationTests/resources/uv/pyproject.toml b/test/Microsoft.ComponentDetection.VerificationTests/resources/uv/pyproject.toml new file mode 100644 index 000000000..f79d996bb --- /dev/null +++ b/test/Microsoft.ComponentDetection.VerificationTests/resources/uv/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "component-detection" +version = "0.0.0" +description = "A test project" +authors = [] +requires-python = "==3.9.*" +dependencies = [ + "azure-identity==1.17.1", + "flask>2,<3", + "requests>=2.32.0", +] + +[dependency-groups] +dev = [ + "pytest-env>=1.1.5", + "pytest>=8.3.4", + "pytest-cov>=6.0.0" +] diff --git a/test/Microsoft.ComponentDetection.VerificationTests/resources/uv/uv.lock b/test/Microsoft.ComponentDetection.VerificationTests/resources/uv/uv.lock new file mode 100644 index 000000000..c377086ca --- /dev/null +++ b/test/Microsoft.ComponentDetection.VerificationTests/resources/uv/uv.lock @@ -0,0 +1,497 @@ +version = 1 +requires-python = "==3.9.*" + +[[package]] +name = "azure-core" +version = "1.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "six" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/29/ff7a519a315e41c85bab92a7478c6acd1cf0b14353139a08caee4c691f77/azure_core-1.34.0.tar.gz", hash = "sha256:bdb544989f246a0ad1c85d72eeb45f2f835afdcbc5b45e43f0dbde7461c81ece", size = 297999 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/9e/5c87b49f65bb16571599bc789857d0ded2f53014d3392bc88a5d1f3ad779/azure_core-1.34.0-py3-none-any.whl", hash = "sha256:0615d3b756beccdb6624d1c0ae97284f38b78fb59a2a9839bf927c66fbbdddd6", size = 207409 }, +] + +[[package]] +name = "azure-identity" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "cryptography" }, + { name = "msal" }, + { name = "msal-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/c9/f7e3926686a89670ce641b360bd2da9a2d7a12b3e532403462d99f81e9d5/azure-identity-1.17.1.tar.gz", hash = "sha256:32ecc67cc73f4bd0595e4f64b1ca65cd05186f4fe6f98ed2ae9f1aa32646efea", size = 246652 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/83/a777861351e7b99e7c84ff3b36bab35e87b6e5d36e50b6905e148c696515/azure_identity-1.17.1-py3-none-any.whl", hash = "sha256:db8d59c183b680e763722bfe8ebc45930e6c57df510620985939f7f3191e0382", size = 173229 }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 }, +] + +[[package]] +name = "certifi" +version = "2025.4.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220 }, + { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605 }, + { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910 }, + { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200 }, + { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565 }, + { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635 }, + { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218 }, + { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486 }, + { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911 }, + { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632 }, + { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820 }, + { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671 }, + { url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744 }, + { url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993 }, + { url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", size = 147382 }, + { url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", size = 149536 }, + { url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", size = 151349 }, + { url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", size = 146365 }, + { url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", size = 154499 }, + { url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", size = 157735 }, + { url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", size = 154786 }, + { url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", size = 150203 }, + { url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", size = 98436 }, + { url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", size = 105772 }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "component-detection" +version = "0.0.0" +source = { virtual = "." } +dependencies = [ + { name = "azure-identity" }, + { name = "flask" }, + { name = "requests" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-env" }, +] + +[package.metadata] +requires-dist = [ + { name = "azure-identity", specifier = "==1.17.1" }, + { name = "flask", specifier = ">2,<3" }, + { name = "requests", specifier = ">=2.32.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, + { name = "pytest-env", specifier = ">=1.1.5" }, +] + +[[package]] +name = "coverage" +version = "7.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/07/998afa4a0ecdf9b1981ae05415dad2d4e7716e1b1f00abbd91691ac09ac9/coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27", size = 812759 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/1e/388267ad9c6aa126438acc1ceafede3bb746afa9872e3ec5f0691b7d5efa/coverage-7.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:496948261eaac5ac9cf43f5d0a9f6eb7a6d4cb3bedb2c5d294138142f5c18f2a", size = 211566 }, + { url = "https://files.pythonhosted.org/packages/8f/a5/acc03e5cf0bba6357f5e7c676343de40fbf431bb1e115fbebf24b2f7f65e/coverage-7.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eacd2de0d30871eff893bab0b67840a96445edcb3c8fd915e6b11ac4b2f3fa6d", size = 211996 }, + { url = "https://files.pythonhosted.org/packages/5b/a2/0fc0a9f6b7c24fa4f1d7210d782c38cb0d5e692666c36eaeae9a441b6755/coverage-7.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b039ffddc99ad65d5078ef300e0c7eed08c270dc26570440e3ef18beb816c1ca", size = 240741 }, + { url = "https://files.pythonhosted.org/packages/e6/da/1c6ba2cf259710eed8916d4fd201dccc6be7380ad2b3b9f63ece3285d809/coverage-7.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e49824808d4375ede9dd84e9961a59c47f9113039f1a525e6be170aa4f5c34d", size = 238672 }, + { url = "https://files.pythonhosted.org/packages/ac/51/c8fae0dc3ca421e6e2509503696f910ff333258db672800c3bdef256265a/coverage-7.8.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b069938961dfad881dc2f8d02b47645cd2f455d3809ba92a8a687bf513839787", size = 239769 }, + { url = "https://files.pythonhosted.org/packages/59/8e/b97042ae92c59f40be0c989df090027377ba53f2d6cef73c9ca7685c26a6/coverage-7.8.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:de77c3ba8bb686d1c411e78ee1b97e6e0b963fb98b1637658dd9ad2c875cf9d7", size = 239555 }, + { url = "https://files.pythonhosted.org/packages/47/35/b8893e682d6e96b1db2af5997fc13ef62219426fb17259d6844c693c5e00/coverage-7.8.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1676628065a498943bd3f64f099bb573e08cf1bc6088bbe33cf4424e0876f4b3", size = 237768 }, + { url = "https://files.pythonhosted.org/packages/03/6c/023b0b9a764cb52d6243a4591dcb53c4caf4d7340445113a1f452bb80591/coverage-7.8.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8e1a26e7e50076e35f7afafde570ca2b4d7900a491174ca357d29dece5aacee7", size = 238757 }, + { url = "https://files.pythonhosted.org/packages/03/ed/3af7e4d721bd61a8df7de6de9e8a4271e67f3d9e086454558fd9f48eb4f6/coverage-7.8.2-cp39-cp39-win32.whl", hash = "sha256:6782a12bf76fa61ad9350d5a6ef5f3f020b57f5e6305cbc663803f2ebd0f270a", size = 214166 }, + { url = "https://files.pythonhosted.org/packages/9d/30/ee774b626773750dc6128354884652507df3c59d6aa8431526107e595227/coverage-7.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:1efa4166ba75ccefd647f2d78b64f53f14fb82622bc94c5a5cb0a622f50f1c9e", size = 215050 }, + { url = "https://files.pythonhosted.org/packages/69/2f/572b29496d8234e4a7773200dd835a0d32d9e171f2d974f3fe04a9dbc271/coverage-7.8.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:ec455eedf3ba0bbdf8f5a570012617eb305c63cb9f03428d39bf544cb2b94837", size = 203636 }, + { url = "https://files.pythonhosted.org/packages/a0/1a/0b9c32220ad694d66062f571cc5cedfa9997b64a591e8a500bb63de1bd40/coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32", size = 203623 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli" }, +] + +[[package]] +name = "cryptography" +version = "45.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/1f/9fa001e74a1993a9cadd2333bb889e50c66327b8594ac538ab8a04f915b7/cryptography-45.0.3.tar.gz", hash = "sha256:ec21313dd335c51d7877baf2972569f40a4291b76a0ce51391523ae358d05899", size = 744738 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/b2/2345dc595998caa6f68adf84e8f8b50d18e9fc4638d32b22ea8daedd4b7a/cryptography-45.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:7573d9eebaeceeb55285205dbbb8753ac1e962af3d9640791d12b36864065e71", size = 7056239 }, + { url = "https://files.pythonhosted.org/packages/71/3d/ac361649a0bfffc105e2298b720d8b862330a767dab27c06adc2ddbef96a/cryptography-45.0.3-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d377dde61c5d67eb4311eace661c3efda46c62113ff56bf05e2d679e02aebb5b", size = 4205541 }, + { url = "https://files.pythonhosted.org/packages/70/3e/c02a043750494d5c445f769e9c9f67e550d65060e0bfce52d91c1362693d/cryptography-45.0.3-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae1e637f527750811588e4582988932c222f8251f7b7ea93739acb624e1487f", size = 4433275 }, + { url = "https://files.pythonhosted.org/packages/40/7a/9af0bfd48784e80eef3eb6fd6fde96fe706b4fc156751ce1b2b965dada70/cryptography-45.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ca932e11218bcc9ef812aa497cdf669484870ecbcf2d99b765d6c27a86000942", size = 4209173 }, + { url = "https://files.pythonhosted.org/packages/31/5f/d6f8753c8708912df52e67969e80ef70b8e8897306cd9eb8b98201f8c184/cryptography-45.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af3f92b1dc25621f5fad065288a44ac790c5798e986a34d393ab27d2b27fcff9", size = 3898150 }, + { url = "https://files.pythonhosted.org/packages/8b/50/f256ab79c671fb066e47336706dc398c3b1e125f952e07d54ce82cf4011a/cryptography-45.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f8f8f0b73b885ddd7f3d8c2b2234a7d3ba49002b0223f58cfde1bedd9563c56", size = 4466473 }, + { url = "https://files.pythonhosted.org/packages/62/e7/312428336bb2df0848d0768ab5a062e11a32d18139447a76dfc19ada8eed/cryptography-45.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9cc80ce69032ffa528b5e16d217fa4d8d4bb7d6ba8659c1b4d74a1b0f4235fca", size = 4211890 }, + { url = "https://files.pythonhosted.org/packages/e7/53/8a130e22c1e432b3c14896ec5eb7ac01fb53c6737e1d705df7e0efb647c6/cryptography-45.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c824c9281cb628015bfc3c59335163d4ca0540d49de4582d6c2637312907e4b1", size = 4466300 }, + { url = "https://files.pythonhosted.org/packages/ba/75/6bb6579688ef805fd16a053005fce93944cdade465fc92ef32bbc5c40681/cryptography-45.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5833bb4355cb377ebd880457663a972cd044e7f49585aee39245c0d592904578", size = 4332483 }, + { url = "https://files.pythonhosted.org/packages/2f/11/2538f4e1ce05c6c4f81f43c1ef2bd6de7ae5e24ee284460ff6c77e42ca77/cryptography-45.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bb5bf55dcb69f7067d80354d0a348368da907345a2c448b0babc4215ccd3497", size = 4573714 }, + { url = "https://files.pythonhosted.org/packages/f5/bb/e86e9cf07f73a98d84a4084e8fd420b0e82330a901d9cac8149f994c3417/cryptography-45.0.3-cp311-abi3-win32.whl", hash = "sha256:3ad69eeb92a9de9421e1f6685e85a10fbcfb75c833b42cc9bc2ba9fb00da4710", size = 2934752 }, + { url = "https://files.pythonhosted.org/packages/c7/75/063bc9ddc3d1c73e959054f1fc091b79572e716ef74d6caaa56e945b4af9/cryptography-45.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:97787952246a77d77934d41b62fb1b6f3581d83f71b44796a4158d93b8f5c490", size = 3412465 }, + { url = "https://files.pythonhosted.org/packages/71/9b/04ead6015229a9396890d7654ee35ef630860fb42dc9ff9ec27f72157952/cryptography-45.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:c92519d242703b675ccefd0f0562eb45e74d438e001f8ab52d628e885751fb06", size = 7031892 }, + { url = "https://files.pythonhosted.org/packages/46/c7/c7d05d0e133a09fc677b8a87953815c522697bdf025e5cac13ba419e7240/cryptography-45.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5edcb90da1843df85292ef3a313513766a78fbbb83f584a5a58fb001a5a9d57", size = 4196181 }, + { url = "https://files.pythonhosted.org/packages/08/7a/6ad3aa796b18a683657cef930a986fac0045417e2dc428fd336cfc45ba52/cryptography-45.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38deed72285c7ed699864f964a3f4cf11ab3fb38e8d39cfcd96710cd2b5bb716", size = 4423370 }, + { url = "https://files.pythonhosted.org/packages/4f/58/ec1461bfcb393525f597ac6a10a63938d18775b7803324072974b41a926b/cryptography-45.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5555365a50efe1f486eed6ac7062c33b97ccef409f5970a0b6f205a7cfab59c8", size = 4197839 }, + { url = "https://files.pythonhosted.org/packages/d4/3d/5185b117c32ad4f40846f579369a80e710d6146c2baa8ce09d01612750db/cryptography-45.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e4253ed8f5948a3589b3caee7ad9a5bf218ffd16869c516535325fece163dcc", size = 3886324 }, + { url = "https://files.pythonhosted.org/packages/67/85/caba91a57d291a2ad46e74016d1f83ac294f08128b26e2a81e9b4f2d2555/cryptography-45.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cfd84777b4b6684955ce86156cfb5e08d75e80dc2585e10d69e47f014f0a5342", size = 4450447 }, + { url = "https://files.pythonhosted.org/packages/ae/d1/164e3c9d559133a38279215c712b8ba38e77735d3412f37711b9f8f6f7e0/cryptography-45.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:a2b56de3417fd5f48773ad8e91abaa700b678dc7fe1e0c757e1ae340779acf7b", size = 4200576 }, + { url = "https://files.pythonhosted.org/packages/71/7a/e002d5ce624ed46dfc32abe1deff32190f3ac47ede911789ee936f5a4255/cryptography-45.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:57a6500d459e8035e813bd8b51b671977fb149a8c95ed814989da682314d0782", size = 4450308 }, + { url = "https://files.pythonhosted.org/packages/87/ad/3fbff9c28cf09b0a71e98af57d74f3662dea4a174b12acc493de00ea3f28/cryptography-45.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f22af3c78abfbc7cbcdf2c55d23c3e022e1a462ee2481011d518c7fb9c9f3d65", size = 4325125 }, + { url = "https://files.pythonhosted.org/packages/f5/b4/51417d0cc01802304c1984d76e9592f15e4801abd44ef7ba657060520bf0/cryptography-45.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:232954730c362638544758a8160c4ee1b832dc011d2c41a306ad8f7cccc5bb0b", size = 4560038 }, + { url = "https://files.pythonhosted.org/packages/80/38/d572f6482d45789a7202fb87d052deb7a7b136bf17473ebff33536727a2c/cryptography-45.0.3-cp37-abi3-win32.whl", hash = "sha256:cb6ab89421bc90e0422aca911c69044c2912fc3debb19bb3c1bfe28ee3dff6ab", size = 2924070 }, + { url = "https://files.pythonhosted.org/packages/91/5a/61f39c0ff4443651cc64e626fa97ad3099249152039952be8f344d6b0c86/cryptography-45.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:d54ae41e6bd70ea23707843021c778f151ca258081586f0cfa31d936ae43d1b2", size = 3395005 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 }, +] + +[[package]] +name = "flask" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "importlib-metadata" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/b7/4ace17e37abd9c21715dea5ee11774a25e404c486a7893fa18e764326ead/flask-2.3.3.tar.gz", hash = "sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc", size = 672756 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/56/26f0be8adc2b4257df20c1c4260ddd0aa396cf8e75d90ab2f7ff99bc34f9/flask-2.3.3-py3-none-any.whl", hash = "sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b", size = 96112 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656 }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344 }, + { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389 }, + { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607 }, + { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728 }, + { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826 }, + { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843 }, + { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219 }, + { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946 }, + { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063 }, + { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506 }, +] + +[[package]] +name = "msal" +version = "1.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/90/81dcc50f0be11a8c4dcbae1a9f761a26e5f905231330a7cacc9f04ec4c61/msal-1.32.3.tar.gz", hash = "sha256:5eea038689c78a5a70ca8ecbe1245458b55a857bd096efb6989c69ba15985d35", size = 151449 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/bf/81516b9aac7fd867709984d08eb4db1d2e3fe1df795c8e442cde9b568962/msal-1.32.3-py3-none-any.whl", hash = "sha256:b2798db57760b1961b142f027ffb7c8169536bf77316e99a0df5c4aaebb11569", size = 115358 }, +] + +[[package]] +name = "msal-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583 }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pytest" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", size = 1515232 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/de/afa024cbe022b1b318a3d224125aa24939e99b4ff6f22e0ba639a2eaee47/pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e", size = 363797 }, +] + +[[package]] +name = "pytest-cov" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 }, +] + +[[package]] +name = "pytest-env" +version = "1.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "tomli" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/31/27f28431a16b83cab7a636dce59cf397517807d247caa38ee67d65e71ef8/pytest_env-1.1.5.tar.gz", hash = "sha256:91209840aa0e43385073ac464a554ad2947cc2fd663a9debf88d03b01e0cc1cf", size = 8911 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/b8/87cfb16045c9d4092cfcf526135d73b88101aac83bc1adcf82dfb5fd3833/pytest_env-1.1.5-py3-none-any.whl", hash = "sha256:ce90cf8772878515c24b31cd97c7fa1f4481cd68d588419fd45f10ecaee6bc30", size = 6141 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839 }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, +] + +[[package]] +name = "werkzeug" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498 }, +] + +[[package]] +name = "zipp" +version = "3.22.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/b6/7b3d16792fdf94f146bed92be90b4eb4563569eca91513c8609aebf0c167/zipp-3.22.0.tar.gz", hash = "sha256:dd2f28c3ce4bc67507bfd3781d21b7bb2be31103b51a4553ad7d90b84e57ace5", size = 25257 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/da/f64669af4cae46f17b90798a827519ce3737d31dbafad65d391e49643dc4/zipp-3.22.0-py3-none-any.whl", hash = "sha256:fe208f65f2aca48b81f9e6fd8cf7b8b32c26375266b009b413d45306b6148343", size = 9796 }, +] From 316324ddf4981214bf7ce11051a9d5cc75155f3b Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 6 Jun 2025 12:00:10 +0200 Subject: [PATCH 08/46] add UvLockComponentDetector deps --- .../uv/UvLockComponentDetector.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs index 6cacd99e5..bcd28771e 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs @@ -14,6 +14,16 @@ namespace Microsoft.ComponentDetection.Detectors.Uv public class UvLockComponentDetector : FileComponentDetector { + public UvLockComponentDetector( + IComponentStreamEnumerableFactory componentStreamEnumerableFactory, + IObservableDirectoryWalkerFactory walkerFactory, + ILogger logger) + { + this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory; + this.Scanner = walkerFactory; + this.Logger = logger; + } + public override string Id => "UvLock"; public override IList SearchPatterns { get; } = ["uv.lock"]; From 9cf0e812fd223e624d6d919dce5b0e82bea50d75 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 6 Jun 2025 12:09:47 +0200 Subject: [PATCH 09/46] just use PipComponent --- .../TypedComponent/UvComponent.cs | 31 ------------------- .../uv/UvLockComponentDetector.cs | 6 ++-- 2 files changed, 3 insertions(+), 34 deletions(-) delete mode 100644 src/Microsoft.ComponentDetection.Contracts/TypedComponent/UvComponent.cs diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/UvComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/UvComponent.cs deleted file mode 100644 index 6b32bdd51..000000000 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/UvComponent.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Microsoft.ComponentDetection.Contracts.TypedComponent; - -using System.Collections.Generic; - -public class UvComponent : TypedComponent -{ - public UvComponent() => this.Metadata = []; - - public UvComponent(string name, string version, Dictionary metadata = null) - { - this.Name = this.ValidateRequiredInput(name, nameof(this.Name), nameof(ComponentType.Pip)); - this.Version = this.ValidateRequiredInput(version, nameof(this.Version), nameof(ComponentType.Pip)); - this.Metadata = metadata ?? []; - } - - public string Name { get; set; } - - public string Version { get; set; } - - /// - /// Arbitrary metadata from the uv.lock TOML package entry (e.g., dependencies, source, wheels, sdist, etc.) - /// - public Dictionary Metadata { get; set; } - - public override ComponentType Type => ComponentType.Uv; - - protected override string ComputeId() - { - return $"{this.Name}:{this.Version}"; - } -} diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs index bcd28771e..9ad344e34 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs @@ -28,7 +28,7 @@ public UvLockComponentDetector( public override IList SearchPatterns { get; } = ["uv.lock"]; - public override IEnumerable SupportedComponentTypes => [ComponentType.Uv]; + public override IEnumerable SupportedComponentTypes => [ComponentType.Pip]; public override int Version => 1; @@ -68,8 +68,8 @@ protected override async Task OnFileFoundAsync(ProcessRequest processRequest, ID continue; } - var uvComponent = new UvComponent(name, version); - var detectedComponent = new DetectedComponent(uvComponent); + var pipComponent = new PipComponent(name, version); + var detectedComponent = new DetectedComponent(pipComponent); singleFileComponentRecorder.RegisterUsage(detectedComponent); } } From b2e686a03a53537b329a09126a442550ab8e5c58 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 6 Jun 2025 12:31:54 +0200 Subject: [PATCH 10/46] add unit tests --- .../UvLockDetectorTests.cs | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs new file mode 100644 index 000000000..555b316ad --- /dev/null +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs @@ -0,0 +1,114 @@ +namespace Microsoft.ComponentDetection.Detectors.Tests; + +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Microsoft.ComponentDetection.Detectors.Uv; +using Microsoft.ComponentDetection.TestsUtilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +[TestCategory("Governance/All")] +[TestCategory("Governance/ComponentDetection")] +public class UvLockDetectorTests : BaseDetectorTest +{ + [TestMethod] + public async Task TestUvLockDetectorWithNoFiles_ReturnsSuccessfullyAsync() + { + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().BeEmpty(); + } + + [TestMethod] + public async Task TestUvLockDetectorWithEmptyLockFile_FindsNothingAsync() + { + var emptyUvLock = string.Empty; // Empty TOML + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("uv.lock", emptyUvLock) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().BeEmpty(); + } + + [TestMethod] + public async Task TestUvLockDetectorWithNoPackages_FindsNothingAsync() + { + var uvLock = "# uv.lock file\n[metadata]\nversion = '1'\n"; + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("uv.lock", uvLock) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().BeEmpty(); + } + + [TestMethod] + public async Task TestUvLockDetectorWithMultiplePackages_FindsAllComponentsAsync() + { + var uvLock = @" +[[package]] +name = 'foo' +version = '1.2.3' +[[package]] +name = 'bar' +version = '4.5.6' +"; + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("uv.lock", uvLock) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + var detectedComponents = componentRecorder.GetDetectedComponents(); + detectedComponents.Should().HaveCount(2); + detectedComponents.Select(x => ((PipComponent)x.Component).Name).Should().BeEquivalentTo(["foo", "bar"]); + detectedComponents.Select(x => ((PipComponent)x.Component).Version).Should().BeEquivalentTo(["1.2.3", "4.5.6"]); + } + + [TestMethod] + public async Task TestUvLockDetectorWithMalformedPackage_IgnoresInvalidAsync() + { + var uvLock = @" +[[package]] +name = 'foo' +[[package]] +version = '4.5.6' +"; + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("uv.lock", uvLock) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + var detectedComponents = componentRecorder.GetDetectedComponents(); + detectedComponents.Should().BeEmpty(); + } + + [TestMethod] + public async Task TestUvLockDetectorWithMultipleLockFiles_FindsAllComponentsAsync() + { + var uvLock1 = @" +[[package]] +name = 'foo' +version = '1.2.3' +"; + var uvLock2 = @" +[[package]] +name = 'bar' +version = '4.5.6' +"; + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("uv.lock", uvLock1) + .WithFile("uv2.lock", uvLock2, ["uv.lock"]) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + var detectedComponents = componentRecorder.GetDetectedComponents(); + detectedComponents.Should().HaveCount(2); + detectedComponents.Select(x => ((PipComponent)x.Component).Name).Should().BeEquivalentTo(["foo", "bar"]); + } +} From cd3b461fb9dcc51fd1b6e3b251629db63d369efe Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 6 Jun 2025 12:55:48 +0200 Subject: [PATCH 11/46] add explicit component references --- .../uv/UvLockComponentDetector.cs | 54 ++++++++++++++----- .../UvLockDetectorTests.cs | 27 +++++++++- 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs index 9ad344e34..8c5d63a0e 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs @@ -3,6 +3,7 @@ namespace Microsoft.ComponentDetection.Detectors.Uv using System; using System.Collections.Generic; using System.IO; + using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.ComponentDetection.Contracts; @@ -47,16 +48,8 @@ protected override async Task OnFileFoundAsync(ProcessRequest processRequest, ID if (model is TomlTable table) { - // Optionally, log or use top-level metadata - var topLevelMetadata = new Dictionary(); - foreach (var key in table.Keys) - { - if (key != "package") - { - topLevelMetadata[key] = table[key]; - } - } - + // Parse all packages and their dependencies + var packagesList = new List<(PipComponent Component, List Dependencies)>(); if (table.TryGetValue("package", out var packagesObj) && packagesObj is TomlTableArray packages) { foreach (var pkg in packages) @@ -69,8 +62,45 @@ protected override async Task OnFileFoundAsync(ProcessRequest processRequest, ID } var pipComponent = new PipComponent(name, version); - var detectedComponent = new DetectedComponent(pipComponent); - singleFileComponentRecorder.RegisterUsage(detectedComponent); + var dependencies = new List(); + if (pkg.TryGetValue("dependencies", out var depsObj) && depsObj is TomlTable depsTable) + { + foreach (var depKey in depsTable.Keys) + { + var depName = depKey; + var depVersion = depsTable[depKey]?.ToString(); + if (!string.IsNullOrEmpty(depName) && !string.IsNullOrEmpty(depVersion)) + { + dependencies.Add($"{depName} {depVersion} - pip"); + } + } + } + + packagesList.Add((pipComponent, dependencies)); + } + } + + // Register all components and their dependencies in the graph + var componentIdSet = new HashSet(packagesList.Select(x => x.Component.Id)); + foreach (var (component, dependencies) in packagesList) + { + var detectedComponent = new DetectedComponent(component); + if (dependencies.Count == 0) + { + singleFileComponentRecorder.RegisterUsage(detectedComponent, isExplicitReferencedDependency: true); + } + else + { + singleFileComponentRecorder.RegisterUsage(detectedComponent, isExplicitReferencedDependency: true); + foreach (var depId in dependencies) + { + // Only add edges to components that exist in the lock file + if (componentIdSet.Contains(depId)) + { + singleFileComponentRecorder.RegisterUsage(new DetectedComponent(component), isExplicitReferencedDependency: true, parentComponentId: component.Id); + singleFileComponentRecorder.RegisterUsage(new DetectedComponent(new PipComponent(depId.Split(' ')[0], depId.Split(' ')[1])), isExplicitReferencedDependency: false, parentComponentId: component.Id); + } + } } } } diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs index 555b316ad..beaff3b51 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs @@ -49,7 +49,7 @@ public async Task TestUvLockDetectorWithNoPackages_FindsNothingAsync() } [TestMethod] - public async Task TestUvLockDetectorWithMultiplePackages_FindsAllComponentsAsync() + public async Task TestUvLockDetectorWithMultiplePackages_FindsAllComponentsAndGraphAsync() { var uvLock = @" [[package]] @@ -68,6 +68,18 @@ public async Task TestUvLockDetectorWithMultiplePackages_FindsAllComponentsAsync detectedComponents.Should().HaveCount(2); detectedComponents.Select(x => ((PipComponent)x.Component).Name).Should().BeEquivalentTo(["foo", "bar"]); detectedComponents.Select(x => ((PipComponent)x.Component).Version).Should().BeEquivalentTo(["1.2.3", "4.5.6"]); + + // Validate dependency graph structure: both are roots, no dependencies + var graphs = componentRecorder.GetDependencyGraphsByLocation(); + graphs.Should().ContainKey("uv.lock"); + var graph = graphs["uv.lock"]; + var fooId = new PipComponent("foo", "1.2.3").Id; + var barId = new PipComponent("bar", "4.5.6").Id; + graph.GetComponents().Should().BeEquivalentTo([fooId, barId]); + graph.GetDependenciesForComponent(fooId).Should().BeEmpty(); + graph.GetDependenciesForComponent(barId).Should().BeEmpty(); + graph.IsComponentExplicitlyReferenced(fooId).Should().BeTrue(); + graph.IsComponentExplicitlyReferenced(barId).Should().BeTrue(); } [TestMethod] @@ -89,7 +101,7 @@ public async Task TestUvLockDetectorWithMalformedPackage_IgnoresInvalidAsync() } [TestMethod] - public async Task TestUvLockDetectorWithMultipleLockFiles_FindsAllComponentsAsync() + public async Task TestUvLockDetectorWithMultipleLockFiles_FindsAllComponentsAndGraphAsync() { var uvLock1 = @" [[package]] @@ -110,5 +122,16 @@ public async Task TestUvLockDetectorWithMultipleLockFiles_FindsAllComponentsAsyn var detectedComponents = componentRecorder.GetDetectedComponents(); detectedComponents.Should().HaveCount(2); detectedComponents.Select(x => ((PipComponent)x.Component).Name).Should().BeEquivalentTo(["foo", "bar"]); + + // Validate both graphs + var graphs = componentRecorder.GetDependencyGraphsByLocation(); + graphs.Should().ContainKey("uv.lock"); + graphs.Should().ContainKey("uv2.lock"); + var fooId = new PipComponent("foo", "1.2.3").Id; + var barId = new PipComponent("bar", "4.5.6").Id; + graphs["uv.lock"].GetComponents().Should().Contain(fooId); + graphs["uv2.lock"].GetComponents().Should().Contain(barId); + graphs["uv.lock"].IsComponentExplicitlyReferenced(fooId).Should().BeTrue(); + graphs["uv2.lock"].IsComponentExplicitlyReferenced(barId).Should().BeTrue(); } } From d9984eb226dfe7a0ec8b789164512ab49e545180 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 6 Jun 2025 13:31:12 +0200 Subject: [PATCH 12/46] simplify name warning --- .../SimplePipComponentDetectorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/SimplePipComponentDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/SimplePipComponentDetectorTests.cs index fb0505129..3edeb681c 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/SimplePipComponentDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/SimplePipComponentDetectorTests.cs @@ -302,7 +302,7 @@ private void CheckGraphStructure(IDependencyGraph graph, Dictionary( + recorder.AssertAllExplicitlyReferencedComponents( childId, parentIds.Select(parentId => new Func(x => x.Id == parentId)).ToArray()); } From fff4dbdc895956d3b764add001e4569c8e61e282 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 6 Jun 2025 13:31:43 +0200 Subject: [PATCH 13/46] one test works --- .../uv/UvLockComponentDetector.cs | 41 ++++++++----- .../UvLockDetectorTests.cs | 58 +++++++++++-------- 2 files changed, 61 insertions(+), 38 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs index 8c5d63a0e..86da6e73b 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs @@ -46,8 +46,24 @@ protected override async Task OnFileFoundAsync(ProcessRequest processRequest, ID var toml = await reader.ReadToEndAsync(cancellationToken); var model = Toml.ToModel(toml); + var explicitNames = new HashSet(StringComparer.OrdinalIgnoreCase); if (model is TomlTable table) { + // Parse [package.metadata].requires-dist for explicit roots + if (table.TryGetValue("package.metadata", out var metadataObj) && metadataObj is TomlTable metadataTable) + { + if (metadataTable.TryGetValue("requires-dist", out var requiresDistObj) && requiresDistObj is TomlTableArray requiresDistArr) + { + foreach (var req in requiresDistArr) + { + if (req is TomlTable reqTable && reqTable.TryGetValue("name", out var nameObj) && nameObj is string nameStr && !string.IsNullOrEmpty(nameStr)) + { + explicitNames.Add(nameStr); + } + } + } + } + // Parse all packages and their dependencies var packagesList = new List<(PipComponent Component, List Dependencies)>(); if (table.TryGetValue("package", out var packagesObj) && packagesObj is TomlTableArray packages) @@ -80,26 +96,23 @@ protected override async Task OnFileFoundAsync(ProcessRequest processRequest, ID } } - // Register all components and their dependencies in the graph + // The test expects the key to be the file name, but the recorder uses the file path. We need to update the test to match the actual behavior. var componentIdSet = new HashSet(packagesList.Select(x => x.Component.Id)); + foreach (var (component, dependencies) in packagesList) { var detectedComponent = new DetectedComponent(component); - if (dependencies.Count == 0) - { - singleFileComponentRecorder.RegisterUsage(detectedComponent, isExplicitReferencedDependency: true); - } - else + + // Mark as explicit if there are no dependencies and no explicit roots are defined, or if in explicitNames + var isExplicit = explicitNames.Count == 0 || explicitNames.Contains(component.Name); + singleFileComponentRecorder.RegisterUsage(detectedComponent, isExplicitReferencedDependency: isExplicit); + + // Register dependencies as edges + foreach (var depId in dependencies) { - singleFileComponentRecorder.RegisterUsage(detectedComponent, isExplicitReferencedDependency: true); - foreach (var depId in dependencies) + if (componentIdSet.Contains(depId)) { - // Only add edges to components that exist in the lock file - if (componentIdSet.Contains(depId)) - { - singleFileComponentRecorder.RegisterUsage(new DetectedComponent(component), isExplicitReferencedDependency: true, parentComponentId: component.Id); - singleFileComponentRecorder.RegisterUsage(new DetectedComponent(new PipComponent(depId.Split(' ')[0], depId.Split(' ')[1])), isExplicitReferencedDependency: false, parentComponentId: component.Id); - } + singleFileComponentRecorder.RegisterUsage(new DetectedComponent(new PipComponent(depId.Split(' ')[0], depId.Split(' ')[1])), isExplicitReferencedDependency: false, parentComponentId: component.Id); } } } diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs index beaff3b51..81680adaa 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs @@ -71,8 +71,9 @@ public async Task TestUvLockDetectorWithMultiplePackages_FindsAllComponentsAndGr // Validate dependency graph structure: both are roots, no dependencies var graphs = componentRecorder.GetDependencyGraphsByLocation(); - graphs.Should().ContainKey("uv.lock"); - var graph = graphs["uv.lock"]; + var graphKey = graphs.Keys.FirstOrDefault(k => k.EndsWith("uv.lock")); + graphKey.Should().NotBeNull(); + var graph = graphs[graphKey]; var fooId = new PipComponent("foo", "1.2.3").Id; var barId = new PipComponent("bar", "4.5.6").Id; graph.GetComponents().Should().BeEquivalentTo([fooId, barId]); @@ -101,37 +102,46 @@ public async Task TestUvLockDetectorWithMalformedPackage_IgnoresInvalidAsync() } [TestMethod] - public async Task TestUvLockDetectorWithMultipleLockFiles_FindsAllComponentsAndGraphAsync() + public async Task TestUvLockDetectorWithExplicitRoots_MarksOnlyExplicitAsync() { - var uvLock1 = @" + var uvLock = @" [[package]] -name = 'foo' -version = '1.2.3' -"; - var uvLock2 = @" +name = 'azure-identity' +version = '1.17.1' [[package]] -name = 'bar' -version = '4.5.6' +name = 'flask' +version = '2.3.3' +[[package]] +name = 'requests' +version = '2.32.3' +[[package]] +name = 'six' +version = '1.17.0' +[package.metadata] +requires-dist = [ + { name = 'azure-identity', specifier = '==1.17.1' }, + { name = 'flask', specifier = '>2,<3' }, + { name = 'requests', specifier = '>=2.32.0' }, +] "; var (scanResult, componentRecorder) = await this.DetectorTestUtility - .WithFile("uv.lock", uvLock1) - .WithFile("uv2.lock", uvLock2, ["uv.lock"]) + .WithFile("uv.lock", uvLock) .ExecuteDetectorAsync(); scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); var detectedComponents = componentRecorder.GetDetectedComponents(); - detectedComponents.Should().HaveCount(2); - detectedComponents.Select(x => ((PipComponent)x.Component).Name).Should().BeEquivalentTo(["foo", "bar"]); - - // Validate both graphs + detectedComponents.Should().HaveCount(4); var graphs = componentRecorder.GetDependencyGraphsByLocation(); - graphs.Should().ContainKey("uv.lock"); - graphs.Should().ContainKey("uv2.lock"); - var fooId = new PipComponent("foo", "1.2.3").Id; - var barId = new PipComponent("bar", "4.5.6").Id; - graphs["uv.lock"].GetComponents().Should().Contain(fooId); - graphs["uv2.lock"].GetComponents().Should().Contain(barId); - graphs["uv.lock"].IsComponentExplicitlyReferenced(fooId).Should().BeTrue(); - graphs["uv2.lock"].IsComponentExplicitlyReferenced(barId).Should().BeTrue(); + var graphKey = graphs.Keys.FirstOrDefault(k => k.EndsWith("uv.lock")); + graphKey.Should().NotBeNull(); + var graph = graphs[graphKey]; + var azureId = new PipComponent("azure-identity", "1.17.1").Id; + var flaskId = new PipComponent("flask", "2.3.3").Id; + var requestsId = new PipComponent("requests", "2.32.3").Id; + var sixId = new PipComponent("six", "1.17.0").Id; + graph.IsComponentExplicitlyReferenced(azureId).Should().BeTrue(); + graph.IsComponentExplicitlyReferenced(flaskId).Should().BeTrue(); + graph.IsComponentExplicitlyReferenced(requestsId).Should().BeTrue(); + graph.IsComponentExplicitlyReferenced(sixId).Should().BeFalse(); } } From 5fdb6babcaa6d157da8e060b0b7e7cb14b9ff2e2 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 6 Jun 2025 14:50:58 +0200 Subject: [PATCH 14/46] add UvLock --- .../uv/UvDependency.cs | 9 +++ .../uv/UvLock.cs | 61 +++++++++++++++++++ .../uv/UvPackage.cs | 13 ++++ 3 files changed, 83 insertions(+) create mode 100644 src/Microsoft.ComponentDetection.Detectors/uv/UvDependency.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvDependency.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvDependency.cs new file mode 100644 index 000000000..b086ccd81 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvDependency.cs @@ -0,0 +1,9 @@ +namespace Microsoft.ComponentDetection.Detectors.Uv +{ + public class UvDependency + { + public string Name { get; set; } + + public string Specifier { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs new file mode 100644 index 000000000..8b10352ff --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs @@ -0,0 +1,61 @@ +namespace Microsoft.ComponentDetection.Detectors.Uv +{ + using System.Collections.Generic; + using System.IO; + using Tomlyn; + using Tomlyn.Model; + + public class UvLock + { + // a list of packages with their dependencies + public List Packages { get; set; } = []; + + // static method to parse the TOML stream into a UvLock model + public static UvLock Parse(Stream tomlStream) + { + using var reader = new StreamReader(tomlStream); + var tomlContent = reader.ReadToEnd(); + var model = Toml.ToModel(tomlContent); + + var uvLock = new UvLock(); + + // add packages from the TOML model + if (model is TomlTable table && table.TryGetValue("package", out var packagesObj) && packagesObj is TomlTableArray packages) + { + foreach (var pkg in packages) + { + if (pkg is TomlTable pkgTable && pkgTable.TryGetValue("name", out var nameObj) && nameObj is string name && pkgTable.TryGetValue("version", out var versionObj) && versionObj is string version) + { + var uvPackage = new UvPackage + { + Name = name, + Version = version, + Dependencies = [], + }; + + // Parse dependencies if present + if (pkgTable.TryGetValue("dependencies", out var depsObj) && depsObj is TomlTableArray depsArray) + { + foreach (var dep in depsArray) + { + if (dep is TomlTable depTable && depTable.TryGetValue("name", out var depNameObj) && depNameObj is string depName) + { + var depSpec = depTable.TryGetValue("specifier", out var specObj) && specObj is string spec ? spec : null; + uvPackage.Dependencies.Add(new UvDependency + { + Name = depName, + Specifier = depSpec, + }); + } + } + } + + uvLock.Packages.Add(uvPackage); + } + } + } + + return uvLock; + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs new file mode 100644 index 000000000..7cbf3ca84 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs @@ -0,0 +1,13 @@ +namespace Microsoft.ComponentDetection.Detectors.Uv +{ + using System.Collections.Generic; + + public class UvPackage + { + public string Name { get; set; } + + public string Version { get; set; } + + public List Dependencies { get; set; } = []; + } +} From 0b840e04fab59e1dce942db7998162da962587ad Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 6 Jun 2025 15:17:12 +0200 Subject: [PATCH 15/46] update detector to use UvLock parsing --- .../uv/UvLockComponentDetector.cs | 92 +++++-------------- 1 file changed, 22 insertions(+), 70 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs index 86da6e73b..7069b2bbc 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs @@ -2,7 +2,6 @@ namespace Microsoft.ComponentDetection.Detectors.Uv { using System; using System.Collections.Generic; - using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -10,8 +9,6 @@ namespace Microsoft.ComponentDetection.Detectors.Uv using Microsoft.ComponentDetection.Contracts.Internal; using Microsoft.ComponentDetection.Contracts.TypedComponent; using Microsoft.Extensions.Logging; - using Tomlyn; - using Tomlyn.Model; public class UvLockComponentDetector : FileComponentDetector { @@ -35,85 +32,38 @@ public UvLockComponentDetector( public override IEnumerable Categories => ["Python"]; - protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary detectorArgs, CancellationToken cancellationToken = default) + protected override Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary detectorArgs, CancellationToken cancellationToken = default) { var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder; var file = processRequest.ComponentStream; try { - using var reader = new StreamReader(file.Stream); - var toml = await reader.ReadToEndAsync(cancellationToken); - var model = Toml.ToModel(toml); + // Parse the file stream into a UvLock model + file.Stream.Position = 0; // Ensure stream is at the beginning + var uvLock = UvLock.Parse(file.Stream); + // Determine explicit roots from the TOML (optional, not implemented in UvLock yet) + // For now, mark all as explicit if no explicit roots are defined var explicitNames = new HashSet(StringComparer.OrdinalIgnoreCase); - if (model is TomlTable table) - { - // Parse [package.metadata].requires-dist for explicit roots - if (table.TryGetValue("package.metadata", out var metadataObj) && metadataObj is TomlTable metadataTable) - { - if (metadataTable.TryGetValue("requires-dist", out var requiresDistObj) && requiresDistObj is TomlTableArray requiresDistArr) - { - foreach (var req in requiresDistArr) - { - if (req is TomlTable reqTable && reqTable.TryGetValue("name", out var nameObj) && nameObj is string nameStr && !string.IsNullOrEmpty(nameStr)) - { - explicitNames.Add(nameStr); - } - } - } - } - - // Parse all packages and their dependencies - var packagesList = new List<(PipComponent Component, List Dependencies)>(); - if (table.TryGetValue("package", out var packagesObj) && packagesObj is TomlTableArray packages) - { - foreach (var pkg in packages) - { - var name = pkg?["name"]?.ToString(); - var version = pkg?["version"]?.ToString(); - if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(version)) - { - continue; - } - var pipComponent = new PipComponent(name, version); - var dependencies = new List(); - if (pkg.TryGetValue("dependencies", out var depsObj) && depsObj is TomlTable depsTable) - { - foreach (var depKey in depsTable.Keys) - { - var depName = depKey; - var depVersion = depsTable[depKey]?.ToString(); - if (!string.IsNullOrEmpty(depName) && !string.IsNullOrEmpty(depVersion)) - { - dependencies.Add($"{depName} {depVersion} - pip"); - } - } - } - - packagesList.Add((pipComponent, dependencies)); - } - } - - // The test expects the key to be the file name, but the recorder uses the file path. We need to update the test to match the actual behavior. - var componentIdSet = new HashSet(packagesList.Select(x => x.Component.Id)); + // Register all packages and their dependencies + var componentIdSet = new HashSet(uvLock.Packages.Select(x => new PipComponent(x.Name, x.Version).Id)); + foreach (var pkg in uvLock.Packages) + { + var pipComponent = new PipComponent(pkg.Name, pkg.Version); + var detectedComponent = new DetectedComponent(pipComponent); + var isExplicit = explicitNames.Count == 0 || explicitNames.Contains(pkg.Name); + singleFileComponentRecorder.RegisterUsage(detectedComponent, isExplicitReferencedDependency: isExplicit); - foreach (var (component, dependencies) in packagesList) + foreach (var dep in pkg.Dependencies) { - var detectedComponent = new DetectedComponent(component); - - // Mark as explicit if there are no dependencies and no explicit roots are defined, or if in explicitNames - var isExplicit = explicitNames.Count == 0 || explicitNames.Contains(component.Name); - singleFileComponentRecorder.RegisterUsage(detectedComponent, isExplicitReferencedDependency: isExplicit); - - // Register dependencies as edges - foreach (var depId in dependencies) + // Only register edge if dependency is in the lock file + var depPkg = uvLock.Packages.FirstOrDefault(p => p.Name.Equals(dep.Name, StringComparison.OrdinalIgnoreCase)); + if (depPkg != null) { - if (componentIdSet.Contains(depId)) - { - singleFileComponentRecorder.RegisterUsage(new DetectedComponent(new PipComponent(depId.Split(' ')[0], depId.Split(' ')[1])), isExplicitReferencedDependency: false, parentComponentId: component.Id); - } + var depComponentWithVersion = new PipComponent(depPkg.Name, depPkg.Version); + singleFileComponentRecorder.RegisterUsage(new DetectedComponent(depComponentWithVersion), isExplicitReferencedDependency: false, parentComponentId: pipComponent.Id); } } } @@ -122,6 +72,8 @@ protected override async Task OnFileFoundAsync(ProcessRequest processRequest, ID { this.Logger.LogError(ex, "Failed to parse uv.lock file {File}", file.Location); } + + return Task.CompletedTask; } } } From 329d936862be4fdd19806907a906262a20951a24 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 6 Jun 2025 15:27:21 +0200 Subject: [PATCH 16/46] add metadata --- .../uv/UvLock.cs | 81 +++++++++++++----- .../UvLockTests.cs | 82 +++++++++++++++++++ 2 files changed, 143 insertions(+), 20 deletions(-) create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs index 8b10352ff..5e1ac3521 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs @@ -10,6 +10,12 @@ public class UvLock // a list of packages with their dependencies public List Packages { get; set; } = []; + // Metadata dependencies (requires-dist) + public List MetadataRequiresDist { get; set; } = []; + + // Metadata dev dependencies (requires-dev) + public List MetadataRequiresDev { get; set; } = []; + // static method to parse the TOML stream into a UvLock model public static UvLock Parse(Stream tomlStream) { @@ -19,38 +25,73 @@ public static UvLock Parse(Stream tomlStream) var uvLock = new UvLock(); - // add packages from the TOML model - if (model is TomlTable table && table.TryGetValue("package", out var packagesObj) && packagesObj is TomlTableArray packages) + if (model is TomlTable table) { - foreach (var pkg in packages) + // add packages from the TOML model + if (table.TryGetValue("package", out var packagesObj) && packagesObj is TomlTableArray packages) { - if (pkg is TomlTable pkgTable && pkgTable.TryGetValue("name", out var nameObj) && nameObj is string name && pkgTable.TryGetValue("version", out var versionObj) && versionObj is string version) + foreach (var pkg in packages) { - var uvPackage = new UvPackage + if (pkg is TomlTable pkgTable && pkgTable.TryGetValue("name", out var nameObj) && nameObj is string name && pkgTable.TryGetValue("version", out var versionObj) && versionObj is string version) { - Name = name, - Version = version, - Dependencies = [], - }; + var uvPackage = new UvPackage + { + Name = name, + Version = version, + Dependencies = [], + }; - // Parse dependencies if present - if (pkgTable.TryGetValue("dependencies", out var depsObj) && depsObj is TomlTableArray depsArray) - { - foreach (var dep in depsArray) + // Parse dependencies if present + if (pkgTable.TryGetValue("dependencies", out var depsObj) && depsObj is TomlTableArray depsArray) { - if (dep is TomlTable depTable && depTable.TryGetValue("name", out var depNameObj) && depNameObj is string depName) + foreach (var dep in depsArray) { - var depSpec = depTable.TryGetValue("specifier", out var specObj) && specObj is string spec ? spec : null; - uvPackage.Dependencies.Add(new UvDependency + if (dep is TomlTable depTable && depTable.TryGetValue("name", out var depNameObj) && depNameObj is string depName) { - Name = depName, - Specifier = depSpec, - }); + var depSpec = depTable.TryGetValue("specifier", out var specObj) && specObj is string spec ? spec : null; + uvPackage.Dependencies.Add(new UvDependency + { + Name = depName, + Specifier = depSpec, + }); + } } } + + uvLock.Packages.Add(uvPackage); + } + } + } + + // Parse [package.metadata].requires-dist + if (table.TryGetValue("package.metadata", out var metadataObj) && metadataObj is TomlTable metadataTable) + { + if (metadataTable.TryGetValue("requires-dist", out var requiresDistObj) && requiresDistObj is TomlTableArray requiresDistArr) + { + foreach (var req in requiresDistArr) + { + if (req is TomlTable reqTable && reqTable.TryGetValue("name", out var nameObj) && nameObj is string name) + { + var spec = reqTable.TryGetValue("specifier", out var specObj) && specObj is string s ? s : null; + uvLock.MetadataRequiresDist.Add(new UvDependency { Name = name, Specifier = spec }); + } } + } + } - uvLock.Packages.Add(uvPackage); + // Parse [package.metadata.requires-dev].dev + if (table.TryGetValue("package.metadata.requires-dev", out var requiresDevObj) && requiresDevObj is TomlTable requiresDevTable) + { + if (requiresDevTable.TryGetValue("dev", out var devObj) && devObj is TomlTableArray devArr) + { + foreach (var req in devArr) + { + if (req is TomlTable reqTable && reqTable.TryGetValue("name", out var nameObj) && nameObj is string name) + { + var spec = reqTable.TryGetValue("specifier", out var specObj) && specObj is string s ? s : null; + uvLock.MetadataRequiresDev.Add(new UvDependency { Name = name, Specifier = spec }); + } + } } } } diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs new file mode 100644 index 000000000..96a53964a --- /dev/null +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs @@ -0,0 +1,82 @@ +namespace Microsoft.ComponentDetection.Detectors.Tests; + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using FluentAssertions; +using Microsoft.ComponentDetection.Detectors.Uv; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class UvLockTests +{ + [TestMethod] + public void Parse_ParsesMetadataRequiresDistAndDev() + { + var toml = @" +[package.metadata] +requires-dist = [ + { name = 'foo', specifier = '==1.0.0' }, + { name = 'bar' }, +] +[package.metadata.requires-dev] +dev = [ + { name = 'pytest', specifier = '>=8.3.4' }, + { name = 'pytest-cov', specifier = '>=6.0.0' }, + { name = 'pytest-env', specifier = '>=1.1.5' }, +] +"; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.MetadataRequiresDist.Should().BeEquivalentTo( + new List + { + new UvDependency { Name = "foo", Specifier = "==1.0.0" }, + new UvDependency { Name = "bar", Specifier = null }, + }, + options => options.ComparingByMembers()); + uvLock.MetadataRequiresDev.Should().BeEquivalentTo( + new List + { + new UvDependency { Name = "pytest", Specifier = ">=8.3.4" }, + new UvDependency { Name = "pytest-cov", Specifier = ">=6.0.0" }, + new UvDependency { Name = "pytest-env", Specifier = ">=1.1.5" }, + }, + options => options.ComparingByMembers()); + } + + [TestMethod] + public void Parse_EmptyMetadataLists() + { + var toml = @" +[package.metadata] +[package.metadata.requires-dev] +"; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.MetadataRequiresDist.Should().BeEmpty(); + uvLock.MetadataRequiresDev.Should().BeEmpty(); + } + + [TestMethod] + public void Parse_ParsesPackagesAndDependencies() + { + var toml = @" +[[package]] +name = 'foo' +version = '1.2.3' +dependencies = [ + { name = 'bar', specifier = '>=2.0.0' }, +] +[[package]] +name = 'bar' +version = '2.0.0' +"; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().HaveCount(2); + uvLock.Packages.First().Name.Should().Be("foo"); + uvLock.Packages.First().Dependencies.Should().ContainSingle(d => d.Name == "bar" && d.Specifier == ">=2.0.0"); + } +} From 50d397fd653ea760b76533a9705fde76a469bae9 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 6 Jun 2025 17:06:42 +0200 Subject: [PATCH 17/46] metadata is for each package --- .../uv/UvLock.cs | 75 +++++++++---------- .../uv/UvPackage.cs | 6 ++ .../UvLockTests.cs | 60 +++++++-------- 3 files changed, 69 insertions(+), 72 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs index 5e1ac3521..66d381a51 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs @@ -10,12 +10,6 @@ public class UvLock // a list of packages with their dependencies public List Packages { get; set; } = []; - // Metadata dependencies (requires-dist) - public List MetadataRequiresDist { get; set; } = []; - - // Metadata dev dependencies (requires-dev) - public List MetadataRequiresDev { get; set; } = []; - // static method to parse the TOML stream into a UvLock model public static UvLock Parse(Stream tomlStream) { @@ -27,7 +21,6 @@ public static UvLock Parse(Stream tomlStream) if (model is TomlTable table) { - // add packages from the TOML model if (table.TryGetValue("package", out var packagesObj) && packagesObj is TomlTableArray packages) { foreach (var pkg in packages) @@ -41,14 +34,16 @@ public static UvLock Parse(Stream tomlStream) Dependencies = [], }; - // Parse dependencies if present - if (pkgTable.TryGetValue("dependencies", out var depsObj) && depsObj is TomlTableArray depsArray) + if (pkgTable.TryGetValue("dependencies", out var depsObj) && depsObj is TomlArray depsArray) { foreach (var dep in depsArray) { - if (dep is TomlTable depTable && depTable.TryGetValue("name", out var depNameObj) && depNameObj is string depName) + if (dep is TomlTable depTable) { - var depSpec = depTable.TryGetValue("specifier", out var specObj) && specObj is string spec ? spec : null; + depTable.TryGetValue("name", out var depNameObj); + var depName = depNameObj as string; + depTable.TryGetValue("specifier", out var specObj); + var depSpec = specObj as string; uvPackage.Dependencies.Add(new UvDependency { Name = depName, @@ -58,39 +53,37 @@ public static UvLock Parse(Stream tomlStream) } } - uvLock.Packages.Add(uvPackage); - } - } - } - - // Parse [package.metadata].requires-dist - if (table.TryGetValue("package.metadata", out var metadataObj) && metadataObj is TomlTable metadataTable) - { - if (metadataTable.TryGetValue("requires-dist", out var requiresDistObj) && requiresDistObj is TomlTableArray requiresDistArr) - { - foreach (var req in requiresDistArr) - { - if (req is TomlTable reqTable && reqTable.TryGetValue("name", out var nameObj) && nameObj is string name) + if (pkg.TryGetValue("metadata", out var metadataObj) && metadataObj is TomlTable metadataTable) { - var spec = reqTable.TryGetValue("specifier", out var specObj) && specObj is string s ? s : null; - uvLock.MetadataRequiresDist.Add(new UvDependency { Name = name, Specifier = spec }); - } - } - } - } + if (metadataTable.TryGetValue("requires-dist", out var requiresDistObj) && requiresDistObj is TomlArray requiresDistArr) + { + foreach (var req in requiresDistArr) + { + if (req is TomlTable reqTable && reqTable.TryGetValue("name", out var requiresDistNameObj) && requiresDistNameObj is string requiresDistName) + { + var spec = reqTable.TryGetValue("specifier", out var specObj) && specObj is string s ? s : null; + uvPackage.MetadataRequiresDist.Add(new UvDependency { Name = requiresDistName, Specifier = spec }); + } + } + } - // Parse [package.metadata.requires-dev].dev - if (table.TryGetValue("package.metadata.requires-dev", out var requiresDevObj) && requiresDevObj is TomlTable requiresDevTable) - { - if (requiresDevTable.TryGetValue("dev", out var devObj) && devObj is TomlTableArray devArr) - { - foreach (var req in devArr) - { - if (req is TomlTable reqTable && reqTable.TryGetValue("name", out var nameObj) && nameObj is string name) - { - var spec = reqTable.TryGetValue("specifier", out var specObj) && specObj is string s ? s : null; - uvLock.MetadataRequiresDev.Add(new UvDependency { Name = name, Specifier = spec }); + if (metadataTable.TryGetValue("requires-dev", out var requiresDevObj) && requiresDevObj is TomlTable requiresDevTable) + { + if (requiresDevTable.TryGetValue("dev", out var devObj) && devObj is TomlArray devArr) + { + foreach (var req in devArr) + { + if (req is TomlTable reqTable && reqTable.TryGetValue("name", out var devNameObj) && devNameObj is string devDependencyName) + { + var spec = reqTable.TryGetValue("specifier", out var specObj) && specObj is string s ? s : null; + uvPackage.MetadataRequiresDev.Add(new UvDependency { Name = devDependencyName, Specifier = spec }); + } + } + } + } } + + uvLock.Packages.Add(uvPackage); } } } diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs index 7cbf3ca84..c7e138aa1 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs @@ -9,5 +9,11 @@ public class UvPackage public string Version { get; set; } public List Dependencies { get; set; } = []; + + // Metadata dependencies (requires-dist) + public List MetadataRequiresDist { get; set; } = []; + + // Metadata dev dependencies (requires-dev) + public List MetadataRequiresDev { get; set; } = []; } } diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs index 96a53964a..511a8a90b 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs @@ -1,6 +1,5 @@ namespace Microsoft.ComponentDetection.Detectors.Tests; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; @@ -14,51 +13,50 @@ public class UvLockTests [TestMethod] public void Parse_ParsesMetadataRequiresDistAndDev() { - var toml = @" + var toml = """ +[[package]] +name = "component-detection" +version = "0.0.0" + [package.metadata] requires-dist = [ - { name = 'foo', specifier = '==1.0.0' }, - { name = 'bar' }, + { name = "azure-identity", specifier = "==1.17.1" }, + { name = "flask", specifier = ">2,<3" }, + { name = "requests", specifier = ">=2.32.0" }, ] + [package.metadata.requires-dev] dev = [ - { name = 'pytest', specifier = '>=8.3.4' }, - { name = 'pytest-cov', specifier = '>=6.0.0' }, - { name = 'pytest-env', specifier = '>=1.1.5' }, + { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, + { name = "pytest-env", specifier = ">=1.1.5" }, ] -"; +"""; using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); var uvLock = UvLock.Parse(ms); - uvLock.MetadataRequiresDist.Should().BeEquivalentTo( - new List - { - new UvDependency { Name = "foo", Specifier = "==1.0.0" }, - new UvDependency { Name = "bar", Specifier = null }, - }, + uvLock.Packages.Should().ContainSingle(); + + var package = uvLock.Packages.First(); + package.Name.Should().Be("component-detection"); + package.Version.Should().Be("0.0.0"); + + package.MetadataRequiresDist.Should().BeEquivalentTo( + [ + new UvDependency { Name = "azure-identity", Specifier = "==1.17.1" }, + new UvDependency { Name = "flask", Specifier = ">2,<3" }, + new UvDependency { Name = "requests", Specifier = ">=2.32.0" }, + ], options => options.ComparingByMembers()); - uvLock.MetadataRequiresDev.Should().BeEquivalentTo( - new List - { + + package.MetadataRequiresDev.Should().BeEquivalentTo( + [ new UvDependency { Name = "pytest", Specifier = ">=8.3.4" }, new UvDependency { Name = "pytest-cov", Specifier = ">=6.0.0" }, new UvDependency { Name = "pytest-env", Specifier = ">=1.1.5" }, - }, + ], options => options.ComparingByMembers()); } - [TestMethod] - public void Parse_EmptyMetadataLists() - { - var toml = @" -[package.metadata] -[package.metadata.requires-dev] -"; - using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); - var uvLock = UvLock.Parse(ms); - uvLock.MetadataRequiresDist.Should().BeEmpty(); - uvLock.MetadataRequiresDev.Should().BeEmpty(); - } - [TestMethod] public void Parse_ParsesPackagesAndDependencies() { From 80e8c80b8ad46d9c5e6dad5db28822b01707cb26 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 6 Jun 2025 17:24:45 +0200 Subject: [PATCH 18/46] punt on explicit --- .../uv/UvLockComponentDetector.cs | 13 +++--- .../UvLockDetectorTests.cs | 44 ------------------- 2 files changed, 5 insertions(+), 52 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs index 7069b2bbc..20ece89bf 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs @@ -43,28 +43,25 @@ protected override Task OnFileFoundAsync(ProcessRequest processRequest, IDiction file.Stream.Position = 0; // Ensure stream is at the beginning var uvLock = UvLock.Parse(file.Stream); - // Determine explicit roots from the TOML (optional, not implemented in UvLock yet) - // For now, mark all as explicit if no explicit roots are defined - var explicitNames = new HashSet(StringComparer.OrdinalIgnoreCase); - - // Register all packages and their dependencies - var componentIdSet = new HashSet(uvLock.Packages.Select(x => new PipComponent(x.Name, x.Version).Id)); foreach (var pkg in uvLock.Packages) { var pipComponent = new PipComponent(pkg.Name, pkg.Version); var detectedComponent = new DetectedComponent(pipComponent); - var isExplicit = explicitNames.Count == 0 || explicitNames.Contains(pkg.Name); + var isExplicit = false; // TODO singleFileComponentRecorder.RegisterUsage(detectedComponent, isExplicitReferencedDependency: isExplicit); foreach (var dep in pkg.Dependencies) { - // Only register edge if dependency is in the lock file var depPkg = uvLock.Packages.FirstOrDefault(p => p.Name.Equals(dep.Name, StringComparison.OrdinalIgnoreCase)); if (depPkg != null) { var depComponentWithVersion = new PipComponent(depPkg.Name, depPkg.Version); singleFileComponentRecorder.RegisterUsage(new DetectedComponent(depComponentWithVersion), isExplicitReferencedDependency: false, parentComponentId: pipComponent.Id); } + else + { + this.Logger.LogWarning("Dependency {DependencyName} not found in uv.lock packages", dep.Name); + } } } } diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs index 81680adaa..e1dcbb507 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs @@ -100,48 +100,4 @@ public async Task TestUvLockDetectorWithMalformedPackage_IgnoresInvalidAsync() var detectedComponents = componentRecorder.GetDetectedComponents(); detectedComponents.Should().BeEmpty(); } - - [TestMethod] - public async Task TestUvLockDetectorWithExplicitRoots_MarksOnlyExplicitAsync() - { - var uvLock = @" -[[package]] -name = 'azure-identity' -version = '1.17.1' -[[package]] -name = 'flask' -version = '2.3.3' -[[package]] -name = 'requests' -version = '2.32.3' -[[package]] -name = 'six' -version = '1.17.0' -[package.metadata] -requires-dist = [ - { name = 'azure-identity', specifier = '==1.17.1' }, - { name = 'flask', specifier = '>2,<3' }, - { name = 'requests', specifier = '>=2.32.0' }, -] -"; - var (scanResult, componentRecorder) = await this.DetectorTestUtility - .WithFile("uv.lock", uvLock) - .ExecuteDetectorAsync(); - - scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); - var detectedComponents = componentRecorder.GetDetectedComponents(); - detectedComponents.Should().HaveCount(4); - var graphs = componentRecorder.GetDependencyGraphsByLocation(); - var graphKey = graphs.Keys.FirstOrDefault(k => k.EndsWith("uv.lock")); - graphKey.Should().NotBeNull(); - var graph = graphs[graphKey]; - var azureId = new PipComponent("azure-identity", "1.17.1").Id; - var flaskId = new PipComponent("flask", "2.3.3").Id; - var requestsId = new PipComponent("requests", "2.32.3").Id; - var sixId = new PipComponent("six", "1.17.0").Id; - graph.IsComponentExplicitlyReferenced(azureId).Should().BeTrue(); - graph.IsComponentExplicitlyReferenced(flaskId).Should().BeTrue(); - graph.IsComponentExplicitlyReferenced(requestsId).Should().BeTrue(); - graph.IsComponentExplicitlyReferenced(sixId).Should().BeFalse(); - } } From b5a9cb09cff0bef36869f9f9eece04eb6a0ddcd8 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 6 Jun 2025 17:34:08 +0200 Subject: [PATCH 19/46] not yet --- .../UvLockDetectorTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs index e1dcbb507..157e1b5af 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs @@ -79,8 +79,6 @@ public async Task TestUvLockDetectorWithMultiplePackages_FindsAllComponentsAndGr graph.GetComponents().Should().BeEquivalentTo([fooId, barId]); graph.GetDependenciesForComponent(fooId).Should().BeEmpty(); graph.GetDependenciesForComponent(barId).Should().BeEmpty(); - graph.IsComponentExplicitlyReferenced(fooId).Should().BeTrue(); - graph.IsComponentExplicitlyReferenced(barId).Should().BeTrue(); } [TestMethod] From 707cd114463ace375614145eb900626835ad2ff3 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Sat, 7 Jun 2025 11:15:25 +0200 Subject: [PATCH 20/46] more detector coverage --- .../UvLockDetectorTests.cs | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs index 157e1b5af..15203d8ff 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs @@ -1,5 +1,6 @@ namespace Microsoft.ComponentDetection.Detectors.Tests; +using System; using System.Linq; using System.Threading.Tasks; using FluentAssertions; @@ -7,7 +8,9 @@ namespace Microsoft.ComponentDetection.Detectors.Tests; using Microsoft.ComponentDetection.Contracts.TypedComponent; using Microsoft.ComponentDetection.Detectors.Uv; using Microsoft.ComponentDetection.TestsUtilities; +using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; [TestClass] [TestCategory("Governance/All")] @@ -81,6 +84,71 @@ public async Task TestUvLockDetectorWithMultiplePackages_FindsAllComponentsAndGr graph.GetDependenciesForComponent(barId).Should().BeEmpty(); } + [TestMethod] + public async Task TestUvLockDetectorWithDependencies_RegistersDependenciesAsync() + { + var uvLock = @" +[[package]] +name = 'foo' +version = '1.2.3' +dependencies = [{ name = 'bar' }] +[[package]] +name = 'bar' +version = '4.5.6' +"; + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("uv.lock", uvLock) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + var detectedComponents = componentRecorder.GetDetectedComponents(); + detectedComponents.Should().HaveCount(2); + + var fooId = new PipComponent("foo", "1.2.3").Id; + var barId = new PipComponent("bar", "4.5.6").Id; + + var graphs = componentRecorder.GetDependencyGraphsByLocation(); + var graphKey = graphs.Keys.FirstOrDefault(k => k.EndsWith("uv.lock")); + var graph = graphs[graphKey]; + + graph.GetComponents().Should().BeEquivalentTo([fooId, barId]); + graph.GetDependenciesForComponent(fooId).Should().BeEquivalentTo([barId]); + graph.GetDependenciesForComponent(barId).Should().BeEmpty(); + } + + [TestMethod] + public async Task TestUvLockDetectorWithMissingDependency_LogsWarningAsync() + { + var loggerMock = new Mock>(); + this.DetectorTestUtility.AddServiceMock(loggerMock); + + var uvLock = @" +[[package]] +name = 'foo' +version = '1.2.3' +dependencies = [{ name = 'baz' }] +[[package]] +name = 'bar' +version = '4.5.6' +"; + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("uv.lock", uvLock) + .AddServiceMock(loggerMock) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + + // Should log a warning for missing dependency 'baz' + loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Dependency baz not found")), + null, + It.IsAny>()), + Times.AtLeastOnce()); + } + [TestMethod] public async Task TestUvLockDetectorWithMalformedPackage_IgnoresInvalidAsync() { @@ -98,4 +166,29 @@ public async Task TestUvLockDetectorWithMalformedPackage_IgnoresInvalidAsync() var detectedComponents = componentRecorder.GetDetectedComponents(); detectedComponents.Should().BeEmpty(); } + + [TestMethod] + public async Task TestUvLockDetectorWithInvalidFile_LogsErrorAsync() + { + var loggerMock = new Mock>(); + this.DetectorTestUtility.AddServiceMock(loggerMock); + + var invalidUvLock = "not a valid toml file"; + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("uv.lock", invalidUvLock) + .AddServiceMock(loggerMock) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + + // Should log an error for parse failure + loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Failed to parse uv.lock file")), + It.IsAny(), + It.IsAny>()), + Times.AtLeastOnce()); + } } From e2398ff418fafba474faf87db181be49882c7607 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Sat, 7 Jun 2025 11:52:49 +0200 Subject: [PATCH 21/46] add UvLock coverage --- .../UvLockTests.cs | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs index 511a8a90b..265ecb336 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs @@ -1,5 +1,6 @@ namespace Microsoft.ComponentDetection.Detectors.Tests; +using System; using System.IO; using System.Linq; using System.Text; @@ -77,4 +78,89 @@ public void Parse_ParsesPackagesAndDependencies() uvLock.Packages.First().Name.Should().Be("foo"); uvLock.Packages.First().Dependencies.Should().ContainSingle(d => d.Name == "bar" && d.Specifier == ">=2.0.0"); } + + [TestMethod] + public void Parse_EmptyStream_ReturnsNoPackages() + { + using var ms = new MemoryStream([]); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().BeEmpty(); + } + + [TestMethod] + public void Parse_TomlNotATable_ThrowsException() + { + var toml = "42"; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + FluentActions.Invoking(() => UvLock.Parse(ms)) + .Should().Throw(); + } + + [TestMethod] + public void Parse_NoPackageKey_ReturnsNoPackages() + { + var toml = "[metadata]\nversion = '1'"; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().BeEmpty(); + } + + [TestMethod] + public void Parse_PackageKeyNotArray_ReturnsNoPackages() + { + var toml = "package = 42"; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().BeEmpty(); + } + + [TestMethod] + public void Parse_PackageMissingNameOrVersion_IgnoresPackage() + { + var toml = @" +[[package]] +version = '1.2.3' +[[package]] +name = 'foo' +"; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().BeEmpty(); + } + + [TestMethod] + public void Parse_PackageWithMalformedDependencies_IgnoresMalformed() + { + var toml = @" +[[package]] +name = 'foo' +version = '1.2.3' +dependencies = [42, { name = 'bar' }] +"; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().ContainSingle(); + var pkg = uvLock.Packages.First(); + pkg.Dependencies.Should().ContainSingle(d => d.Name == "bar"); + } + + [TestMethod] + public void Parse_PackageWithMalformedMetadata_IgnoresMalformed() + { + var toml = @" +[[package]] +name = 'foo' +version = '1.2.3' +[package.metadata] +requires-dist = [42, { name = 'bar' }] +[package.metadata.requires-dev] +dev = [42, { name = 'baz' }] +"; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().ContainSingle(); + var pkg = uvLock.Packages.First(); + pkg.MetadataRequiresDist.Should().ContainSingle(d => d.Name == "bar"); + pkg.MetadataRequiresDev.Should().ContainSingle(d => d.Name == "baz"); + } } From ed06cffaf2139262ac8b7b2d5eebf9e0c19461ea Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Mon, 9 Jun 2025 12:54:00 +0200 Subject: [PATCH 22/46] undo change --- .../SimplePipComponentDetectorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/SimplePipComponentDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/SimplePipComponentDetectorTests.cs index 3edeb681c..fb0505129 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/SimplePipComponentDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/SimplePipComponentDetectorTests.cs @@ -302,7 +302,7 @@ private void CheckGraphStructure(IDependencyGraph graph, Dictionary( childId, parentIds.Select(parentId => new Func(x => x.Id == parentId)).ToArray()); } From 60717dc5a76500c109f52078d5f351fa3a686287 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Mon, 9 Jun 2025 12:59:37 +0200 Subject: [PATCH 23/46] ComponentType Uv not needed --- .../TypedComponent/ComponentType.cs | 3 --- .../Extensions/ServiceCollectionExtensions.cs | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ComponentType.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ComponentType.cs index e6737b48b..31b245d59 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ComponentType.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ComponentType.cs @@ -62,7 +62,4 @@ public enum ComponentType : byte [EnumMember] DotNet = 19, - - [EnumMember] - Uv = 20, } diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs index 278891e2f..32f3f3776 100644 --- a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs +++ b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs @@ -155,7 +155,7 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s // Swift Package Manager services.AddSingleton(); - // Uv + // uv services.AddSingleton(); return services; From 3b4db0a4dc57bf383af4f87f05a2c5ffbc60d561 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Mon, 9 Jun 2025 13:28:35 +0200 Subject: [PATCH 24/46] improve test coverage of UvLock --- .../uv/UvLock.cs | 161 ++++++---- ....ComponentDetection.Detectors.Tests.csproj | 1 + .../UvLockTests.cs | 282 +++++++++++------- 3 files changed, 272 insertions(+), 172 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs index 66d381a51..aae79ab92 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs @@ -1,5 +1,6 @@ namespace Microsoft.ComponentDetection.Detectors.Uv { + using System; using System.Collections.Generic; using System.IO; using Tomlyn; @@ -16,80 +17,112 @@ public static UvLock Parse(Stream tomlStream) using var reader = new StreamReader(tomlStream); var tomlContent = reader.ReadToEnd(); var model = Toml.ToModel(tomlContent); + return new UvLock + { + Packages = ParsePackagesFromModel(model), + }; + } + + internal static List ParsePackagesFromModel(object model) + { + if (model is not TomlTable table) + { + throw new InvalidOperationException("TOML root is not a table"); + } + + if (!table.TryGetValue("package", out var packagesObj) || packagesObj is not TomlTableArray packages) + { + return []; + } + + var result = new List(); + foreach (var pkg in packages) + { + var parsed = ParsePackage(pkg); + if (parsed != null) + { + result.Add(parsed); + } + } + + return result; + } + + internal static UvPackage ParsePackage(object pkg) + { + if (pkg is not TomlTable pkgTable) + { + return null; + } + + if (!pkgTable.TryGetValue("name", out var nameObj) || nameObj is not string name) + { + return null; + } - var uvLock = new UvLock(); + if (!pkgTable.TryGetValue("version", out var versionObj) || versionObj is not string version) + { + return null; + } + + var uvPackage = new UvPackage + { + Name = name, + Version = version, + Dependencies = [], + MetadataRequiresDist = [], + MetadataRequiresDev = [], + }; + + if (pkgTable.TryGetValue("dependencies", out var depsObj) && depsObj is TomlArray depsArray) + { + uvPackage.Dependencies = ParseDependenciesArray(depsArray); + } - if (model is TomlTable table) + if (pkgTable.TryGetValue("metadata", out var metadataObj) && metadataObj is TomlTable metadataTable) { - if (table.TryGetValue("package", out var packagesObj) && packagesObj is TomlTableArray packages) + ParseMetadata(metadataTable, uvPackage); + } + + return uvPackage; + } + + internal static List ParseDependenciesArray(TomlArray depsArray) + { + var deps = new List(); + foreach (var dep in depsArray) + { + if (dep is TomlTable depTable) { - foreach (var pkg in packages) + if (depTable.TryGetValue("name", out var depNameObj) && depNameObj is string depName) { - if (pkg is TomlTable pkgTable && pkgTable.TryGetValue("name", out var nameObj) && nameObj is string name && pkgTable.TryGetValue("version", out var versionObj) && versionObj is string version) + var depSpec = depTable.TryGetValue("specifier", out var specObj) && specObj is string s ? s : null; + deps.Add(new UvDependency { - var uvPackage = new UvPackage - { - Name = name, - Version = version, - Dependencies = [], - }; - - if (pkgTable.TryGetValue("dependencies", out var depsObj) && depsObj is TomlArray depsArray) - { - foreach (var dep in depsArray) - { - if (dep is TomlTable depTable) - { - depTable.TryGetValue("name", out var depNameObj); - var depName = depNameObj as string; - depTable.TryGetValue("specifier", out var specObj); - var depSpec = specObj as string; - uvPackage.Dependencies.Add(new UvDependency - { - Name = depName, - Specifier = depSpec, - }); - } - } - } - - if (pkg.TryGetValue("metadata", out var metadataObj) && metadataObj is TomlTable metadataTable) - { - if (metadataTable.TryGetValue("requires-dist", out var requiresDistObj) && requiresDistObj is TomlArray requiresDistArr) - { - foreach (var req in requiresDistArr) - { - if (req is TomlTable reqTable && reqTable.TryGetValue("name", out var requiresDistNameObj) && requiresDistNameObj is string requiresDistName) - { - var spec = reqTable.TryGetValue("specifier", out var specObj) && specObj is string s ? s : null; - uvPackage.MetadataRequiresDist.Add(new UvDependency { Name = requiresDistName, Specifier = spec }); - } - } - } - - if (metadataTable.TryGetValue("requires-dev", out var requiresDevObj) && requiresDevObj is TomlTable requiresDevTable) - { - if (requiresDevTable.TryGetValue("dev", out var devObj) && devObj is TomlArray devArr) - { - foreach (var req in devArr) - { - if (req is TomlTable reqTable && reqTable.TryGetValue("name", out var devNameObj) && devNameObj is string devDependencyName) - { - var spec = reqTable.TryGetValue("specifier", out var specObj) && specObj is string s ? s : null; - uvPackage.MetadataRequiresDev.Add(new UvDependency { Name = devDependencyName, Specifier = spec }); - } - } - } - } - } - - uvLock.Packages.Add(uvPackage); - } + Name = depName, + Specifier = depSpec, + }); } } } - return uvLock; + return deps; + } + + internal static void ParseMetadata(TomlTable metadataTable, UvPackage uvPackage) + { + if (metadataTable.TryGetValue("requires-dist", out var requiresDistObj) && requiresDistObj is TomlArray requiresDistArr) + { + uvPackage.MetadataRequiresDist = ParseDependenciesArray(requiresDistArr); + } + + if (metadataTable.TryGetValue("requires-dev", out var requiresDevObj) && requiresDevObj is TomlTable requiresDevTable) + { + if (requiresDevTable.TryGetValue("dev", out var devObj) && devObj is TomlArray devArr) + { + uvPackage.MetadataRequiresDev = ParseDependenciesArray(devArr); + } + } } } } diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/Microsoft.ComponentDetection.Detectors.Tests.csproj b/test/Microsoft.ComponentDetection.Detectors.Tests/Microsoft.ComponentDetection.Detectors.Tests.csproj index 2b9b4a22a..03644cbfe 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/Microsoft.ComponentDetection.Detectors.Tests.csproj +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/Microsoft.ComponentDetection.Detectors.Tests.csproj @@ -14,6 +14,7 @@ + diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs index 265ecb336..c53c0207a 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs @@ -1,20 +1,21 @@ -namespace Microsoft.ComponentDetection.Detectors.Tests; - -using System; -using System.IO; -using System.Linq; -using System.Text; -using FluentAssertions; -using Microsoft.ComponentDetection.Detectors.Uv; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -[TestClass] -public class UvLockTests +namespace Microsoft.ComponentDetection.Detectors.Tests { - [TestMethod] - public void Parse_ParsesMetadataRequiresDistAndDev() + using System; + using System.IO; + using System.Linq; + using System.Text; + using FluentAssertions; + using Microsoft.ComponentDetection.Detectors.Uv; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Tomlyn.Model; + + [TestClass] + public class UvLockTests { - var toml = """ + [TestMethod] + public void Parse_ParsesMetadataRequiresDistAndDev() + { + var toml = """ [[package]] name = "component-detection" version = "0.0.0" @@ -33,35 +34,35 @@ public void Parse_ParsesMetadataRequiresDistAndDev() { name = "pytest-env", specifier = ">=1.1.5" }, ] """; - using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); - var uvLock = UvLock.Parse(ms); - uvLock.Packages.Should().ContainSingle(); - - var package = uvLock.Packages.First(); - package.Name.Should().Be("component-detection"); - package.Version.Should().Be("0.0.0"); - - package.MetadataRequiresDist.Should().BeEquivalentTo( - [ - new UvDependency { Name = "azure-identity", Specifier = "==1.17.1" }, - new UvDependency { Name = "flask", Specifier = ">2,<3" }, - new UvDependency { Name = "requests", Specifier = ">=2.32.0" }, - ], - options => options.ComparingByMembers()); - - package.MetadataRequiresDev.Should().BeEquivalentTo( - [ - new UvDependency { Name = "pytest", Specifier = ">=8.3.4" }, - new UvDependency { Name = "pytest-cov", Specifier = ">=6.0.0" }, - new UvDependency { Name = "pytest-env", Specifier = ">=1.1.5" }, - ], - options => options.ComparingByMembers()); - } + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().ContainSingle(); - [TestMethod] - public void Parse_ParsesPackagesAndDependencies() - { - var toml = @" + var package = uvLock.Packages.First(); + package.Name.Should().Be("component-detection"); + package.Version.Should().Be("0.0.0"); + + package.MetadataRequiresDist.Should().BeEquivalentTo( + [ + new UvDependency { Name = "azure-identity", Specifier = "==1.17.1" }, + new UvDependency { Name = "flask", Specifier = ">2,<3" }, + new UvDependency { Name = "requests", Specifier = ">=2.32.0" }, + ], + options => options.ComparingByMembers()); + + package.MetadataRequiresDev.Should().BeEquivalentTo( + [ + new UvDependency { Name = "pytest", Specifier = ">=8.3.4" }, + new UvDependency { Name = "pytest-cov", Specifier = ">=6.0.0" }, + new UvDependency { Name = "pytest-env", Specifier = ">=1.1.5" }, + ], + options => options.ComparingByMembers()); + } + + [TestMethod] + public void Parse_ParsesPackagesAndDependencies() + { + var toml = @" [[package]] name = 'foo' version = '1.2.3' @@ -72,82 +73,82 @@ public void Parse_ParsesPackagesAndDependencies() name = 'bar' version = '2.0.0' "; - using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); - var uvLock = UvLock.Parse(ms); - uvLock.Packages.Should().HaveCount(2); - uvLock.Packages.First().Name.Should().Be("foo"); - uvLock.Packages.First().Dependencies.Should().ContainSingle(d => d.Name == "bar" && d.Specifier == ">=2.0.0"); - } + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().HaveCount(2); + uvLock.Packages.First().Name.Should().Be("foo"); + uvLock.Packages.First().Dependencies.Should().ContainSingle(d => d.Name == "bar" && d.Specifier == ">=2.0.0"); + } - [TestMethod] - public void Parse_EmptyStream_ReturnsNoPackages() - { - using var ms = new MemoryStream([]); - var uvLock = UvLock.Parse(ms); - uvLock.Packages.Should().BeEmpty(); - } + [TestMethod] + public void Parse_EmptyStream_ReturnsNoPackages() + { + using var ms = new MemoryStream([]); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().BeEmpty(); + } - [TestMethod] - public void Parse_TomlNotATable_ThrowsException() - { - var toml = "42"; - using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); - FluentActions.Invoking(() => UvLock.Parse(ms)) - .Should().Throw(); - } + [TestMethod] + public void Parse_TomlNotATable_ThrowsException() + { + var toml = "42"; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + FluentActions.Invoking(() => UvLock.Parse(ms)) + .Should().Throw(); + } - [TestMethod] - public void Parse_NoPackageKey_ReturnsNoPackages() - { - var toml = "[metadata]\nversion = '1'"; - using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); - var uvLock = UvLock.Parse(ms); - uvLock.Packages.Should().BeEmpty(); - } + [TestMethod] + public void Parse_NoPackageKey_ReturnsNoPackages() + { + var toml = "[metadata]\nversion = '1'"; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().BeEmpty(); + } - [TestMethod] - public void Parse_PackageKeyNotArray_ReturnsNoPackages() - { - var toml = "package = 42"; - using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); - var uvLock = UvLock.Parse(ms); - uvLock.Packages.Should().BeEmpty(); - } + [TestMethod] + public void Parse_PackageKeyNotArray_ReturnsNoPackages() + { + var toml = "package = 42"; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().BeEmpty(); + } - [TestMethod] - public void Parse_PackageMissingNameOrVersion_IgnoresPackage() - { - var toml = @" + [TestMethod] + public void Parse_PackageMissingNameOrVersion_IgnoresPackage() + { + var toml = @" [[package]] version = '1.2.3' [[package]] name = 'foo' "; - using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); - var uvLock = UvLock.Parse(ms); - uvLock.Packages.Should().BeEmpty(); - } + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().BeEmpty(); + } - [TestMethod] - public void Parse_PackageWithMalformedDependencies_IgnoresMalformed() - { - var toml = @" + [TestMethod] + public void Parse_PackageWithMalformedDependencies_IgnoresMalformed() + { + var toml = @" [[package]] name = 'foo' version = '1.2.3' dependencies = [42, { name = 'bar' }] "; - using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); - var uvLock = UvLock.Parse(ms); - uvLock.Packages.Should().ContainSingle(); - var pkg = uvLock.Packages.First(); - pkg.Dependencies.Should().ContainSingle(d => d.Name == "bar"); - } + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().ContainSingle(); + var pkg = uvLock.Packages.First(); + pkg.Dependencies.Should().ContainSingle(d => d.Name == "bar"); + } - [TestMethod] - public void Parse_PackageWithMalformedMetadata_IgnoresMalformed() - { - var toml = @" + [TestMethod] + public void Parse_PackageWithMalformedMetadata_IgnoresMalformed() + { + var toml = @" [[package]] name = 'foo' version = '1.2.3' @@ -156,11 +157,76 @@ public void Parse_PackageWithMalformedMetadata_IgnoresMalformed() [package.metadata.requires-dev] dev = [42, { name = 'baz' }] "; - using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); - var uvLock = UvLock.Parse(ms); - uvLock.Packages.Should().ContainSingle(); - var pkg = uvLock.Packages.First(); - pkg.MetadataRequiresDist.Should().ContainSingle(d => d.Name == "bar"); - pkg.MetadataRequiresDev.Should().ContainSingle(d => d.Name == "baz"); + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().ContainSingle(); + var pkg = uvLock.Packages.First(); + pkg.MetadataRequiresDist.Should().ContainSingle(d => d.Name == "bar"); + pkg.MetadataRequiresDev.Should().ContainSingle(d => d.Name == "baz"); + } + + [TestMethod] + public void ParsePackagesFromModel_InvalidRoot_Throws() + { + Action act = () => UvLock.ParsePackagesFromModel(42); + act.Should().Throw(); + } + + [TestMethod] + public void ParsePackagesFromModel_NoPackages_ReturnsEmpty() + { + var table = new TomlTable(); + var result = UvLock.ParsePackagesFromModel(table); + result.Should().BeEmpty(); + } + + [TestMethod] + public void ParsePackage_ValidPackage_ParsesCorrectly() + { + var pkg = new TomlTable + { + ["name"] = "foo", + ["version"] = "1.0.0", + ["dependencies"] = new TomlArray { new TomlTable { ["name"] = "bar", ["specifier"] = ">=2.0.0" } }, + }; + var result = UvLock.ParsePackage(pkg); + result.Should().NotBeNull(); + result.Name.Should().Be("foo"); + result.Version.Should().Be("1.0.0"); + result.Dependencies.Should().ContainSingle(d => d.Name == "bar" && d.Specifier == ">=2.0.0"); + } + + [TestMethod] + public void ParsePackage_MissingNameOrVersion_ReturnsNull() + { + var pkg1 = new TomlTable { ["version"] = "1.0.0" }; + var pkg2 = new TomlTable { ["name"] = "foo" }; + UvLock.ParsePackage(pkg1).Should().BeNull(); + UvLock.ParsePackage(pkg2).Should().BeNull(); + } + + [TestMethod] + public void ParseDependenciesArray_ParsesValidDepsAndSkipsMalformed() + { + var arr = new TomlArray { 42, new TomlTable { ["name"] = "bar", ["specifier"] = "==1.2.3" }, new TomlTable { ["name"] = "baz" } }; + var result = UvLock.ParseDependenciesArray(arr); + result.Should().Contain(d => d.Name == "bar" && d.Specifier == "==1.2.3"); + result.Should().Contain(d => d.Name == "baz" && d.Specifier == null); + result.Should().HaveCount(2); + } + + [TestMethod] + public void ParseMetadata_ParsesRequiresDistAndDev() + { + var pkg = new UvPackage { Name = "foo", Version = "1.0.0" }; + var metadata = new TomlTable + { + ["requires-dist"] = new TomlArray { new TomlTable { ["name"] = "bar", ["specifier"] = ">=2.0.0" } }, + ["requires-dev"] = new TomlTable { ["dev"] = new TomlArray { new TomlTable { ["name"] = "baz" } } }, + }; + UvLock.ParseMetadata(metadata, pkg); + pkg.MetadataRequiresDist.Should().ContainSingle(d => d.Name == "bar" && d.Specifier == ">=2.0.0"); + pkg.MetadataRequiresDev.Should().ContainSingle(d => d.Name == "baz" && d.Specifier == null); + } } } From 74b9ecd65c88554f59609a3d586154e947544544 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Mon, 9 Jun 2025 13:46:42 +0200 Subject: [PATCH 25/46] enable nullable --- .../uv/UvDependency.cs | 5 +- .../uv/UvLock.cs | 83 ++++++++++--------- .../uv/UvPackage.cs | 5 +- 3 files changed, 50 insertions(+), 43 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvDependency.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvDependency.cs index b086ccd81..41a87fb5c 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvDependency.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvDependency.cs @@ -1,9 +1,10 @@ +#nullable enable namespace Microsoft.ComponentDetection.Detectors.Uv { public class UvDependency { - public string Name { get; set; } + public required string Name { get; init; } - public string Specifier { get; set; } + public string? Specifier { get; set; } } } diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs index aae79ab92..d7fd8d653 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs @@ -1,3 +1,4 @@ +#nullable enable namespace Microsoft.ComponentDetection.Detectors.Uv { using System; @@ -23,7 +24,7 @@ public static UvLock Parse(Stream tomlStream) }; } - internal static List ParsePackagesFromModel(object model) + internal static List ParsePackagesFromModel(object? model) { if (model is not TomlTable table) { @@ -39,7 +40,7 @@ internal static List ParsePackagesFromModel(object model) foreach (var pkg in packages) { var parsed = ParsePackage(pkg); - if (parsed != null) + if (parsed is not null) { result.Add(parsed); } @@ -48,69 +49,73 @@ internal static List ParsePackagesFromModel(object model) return result; } - internal static UvPackage ParsePackage(object pkg) + internal static UvPackage? ParsePackage(object? pkg) { if (pkg is not TomlTable pkgTable) { return null; } - if (!pkgTable.TryGetValue("name", out var nameObj) || nameObj is not string name) + if (pkgTable.TryGetValue("name", out var nameObj) && nameObj is string name && + pkgTable.TryGetValue("version", out var versionObj) && versionObj is string version) { - return null; - } - - if (!pkgTable.TryGetValue("version", out var versionObj) || versionObj is not string version) - { - return null; - } - - var uvPackage = new UvPackage - { - Name = name, - Version = version, - Dependencies = [], - MetadataRequiresDist = [], - MetadataRequiresDev = [], - }; + var uvPackage = new UvPackage + { + Name = name, + Version = version, + Dependencies = [], + MetadataRequiresDist = [], + MetadataRequiresDev = [], + }; + + if (pkgTable.TryGetValue("dependencies", out var depsObj) && depsObj is TomlArray depsArray) + { + uvPackage.Dependencies = ParseDependenciesArray(depsArray); + } - if (pkgTable.TryGetValue("dependencies", out var depsObj) && depsObj is TomlArray depsArray) - { - uvPackage.Dependencies = ParseDependenciesArray(depsArray); - } + if (pkgTable.TryGetValue("metadata", out var metadataObj) && metadataObj is TomlTable metadataTable) + { + ParseMetadata(metadataTable, uvPackage); + } - if (pkgTable.TryGetValue("metadata", out var metadataObj) && metadataObj is TomlTable metadataTable) - { - ParseMetadata(metadataTable, uvPackage); + return uvPackage; } - return uvPackage; + return null; } - internal static List ParseDependenciesArray(TomlArray depsArray) + internal static List ParseDependenciesArray(TomlArray? depsArray) { var deps = new List(); + if (depsArray is null) + { + return deps; + } + foreach (var dep in depsArray) { - if (dep is TomlTable depTable) + if (dep is TomlTable depTable && + depTable.TryGetValue("name", out var depNameObj) && depNameObj is string depName) { - if (depTable.TryGetValue("name", out var depNameObj) && depNameObj is string depName) + var depSpec = depTable.TryGetValue("specifier", out var specObj) && specObj is string s ? s : null; + deps.Add(new UvDependency { - var depSpec = depTable.TryGetValue("specifier", out var specObj) && specObj is string s ? s : null; - deps.Add(new UvDependency - { - Name = depName, - Specifier = depSpec, - }); - } + Name = depName, + Specifier = depSpec, + }); } } return deps; } - internal static void ParseMetadata(TomlTable metadataTable, UvPackage uvPackage) + internal static void ParseMetadata(TomlTable? metadataTable, UvPackage uvPackage) { + if (metadataTable is null) + { + return; + } + if (metadataTable.TryGetValue("requires-dist", out var requiresDistObj) && requiresDistObj is TomlArray requiresDistArr) { uvPackage.MetadataRequiresDist = ParseDependenciesArray(requiresDistArr); diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs index c7e138aa1..37b33a100 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs @@ -1,12 +1,13 @@ +#nullable enable namespace Microsoft.ComponentDetection.Detectors.Uv { using System.Collections.Generic; public class UvPackage { - public string Name { get; set; } + public required string Name { get; init; } - public string Version { get; set; } + public required string Version { get; init; } public List Dependencies { get; set; } = []; From e1a3248eb1e3d3b617d0f0a3949357c38e473065 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Mon, 9 Jun 2025 13:58:39 +0200 Subject: [PATCH 26/46] 100% line coverage --- .../UvLockTests.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs index c53c0207a..a5637b8c3 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs @@ -205,6 +205,13 @@ public void ParsePackage_MissingNameOrVersion_ReturnsNull() UvLock.ParsePackage(pkg2).Should().BeNull(); } + [TestMethod] + public void ParsePackage_NullOrNonTable_ReturnsNull() + { + UvLock.ParsePackage(null).Should().BeNull(); + UvLock.ParsePackage(42).Should().BeNull(); + } + [TestMethod] public void ParseDependenciesArray_ParsesValidDepsAndSkipsMalformed() { @@ -215,6 +222,14 @@ public void ParseDependenciesArray_ParsesValidDepsAndSkipsMalformed() result.Should().HaveCount(2); } + [TestMethod] + public void ParseDependenciesArray_NullOrNoValidDeps_ReturnsEmpty() + { + UvLock.ParseDependenciesArray(null).Should().BeEmpty(); + var arr = new TomlArray { 42, "foo", 3.14 }; + UvLock.ParseDependenciesArray(arr).Should().BeEmpty(); + } + [TestMethod] public void ParseMetadata_ParsesRequiresDistAndDev() { @@ -228,5 +243,16 @@ public void ParseMetadata_ParsesRequiresDistAndDev() pkg.MetadataRequiresDist.Should().ContainSingle(d => d.Name == "bar" && d.Specifier == ">=2.0.0"); pkg.MetadataRequiresDev.Should().ContainSingle(d => d.Name == "baz" && d.Specifier == null); } + + [TestMethod] + public void ParseMetadata_NullOrNoRelevantKeys_DoesNothing() + { + var pkg = new UvPackage { Name = "foo", Version = "1.0.0" }; + UvLock.ParseMetadata(null, pkg); // Should not throw + var emptyTable = new TomlTable(); + UvLock.ParseMetadata(emptyTable, pkg); // Should not throw or set anything + pkg.MetadataRequiresDist.Should().BeEmpty(); + pkg.MetadataRequiresDev.Should().BeEmpty(); + } } } From 46af1a0d6440acca5f36f52f9d90cb9627d2430a Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Mon, 9 Jun 2025 13:59:29 +0200 Subject: [PATCH 27/46] 100% branch coverage --- .../UvLockTests.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs index a5637b8c3..77cb51f8a 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs @@ -212,6 +212,18 @@ public void ParsePackage_NullOrNonTable_ReturnsNull() UvLock.ParsePackage(42).Should().BeNull(); } + [TestMethod] + public void ParsePackage_BranchCoverage_AllPaths() + { + // Path: pkg is TomlTable, but missing name + var pkgMissingName = new TomlTable { ["version"] = "1.0.0" }; + UvLock.ParsePackage(pkgMissingName).Should().BeNull(); + + // Path: pkg is TomlTable, but missing version + var pkgMissingVersion = new TomlTable { ["name"] = "foo" }; + UvLock.ParsePackage(pkgMissingVersion).Should().BeNull(); + } + [TestMethod] public void ParseDependenciesArray_ParsesValidDepsAndSkipsMalformed() { @@ -230,6 +242,14 @@ public void ParseDependenciesArray_NullOrNoValidDeps_ReturnsEmpty() UvLock.ParseDependenciesArray(arr).Should().BeEmpty(); } + [TestMethod] + public void ParseDependenciesArray_BranchCoverage_AllPaths() + { + // Path: dep is TomlTable but missing name + var arr = new TomlArray { new TomlTable { ["specifier"] = "==1.2.3" } }; + UvLock.ParseDependenciesArray(arr).Should().BeEmpty(); + } + [TestMethod] public void ParseMetadata_ParsesRequiresDistAndDev() { @@ -254,5 +274,31 @@ public void ParseMetadata_NullOrNoRelevantKeys_DoesNothing() pkg.MetadataRequiresDist.Should().BeEmpty(); pkg.MetadataRequiresDev.Should().BeEmpty(); } + + [TestMethod] + public void ParseMetadata_BranchCoverage_RequiresDistOnly() + { + var pkg = new UvPackage { Name = "foo", Version = "1.0.0" }; + var metadata = new TomlTable + { + ["requires-dist"] = new TomlArray { new TomlTable { ["name"] = "bar" } }, + }; + UvLock.ParseMetadata(metadata, pkg); + pkg.MetadataRequiresDist.Should().ContainSingle(d => d.Name == "bar"); + pkg.MetadataRequiresDev.Should().BeEmpty(); + } + + [TestMethod] + public void ParseMetadata_BranchCoverage_RequiresDevOnly() + { + var pkg = new UvPackage { Name = "foo", Version = "1.0.0" }; + var metadata = new TomlTable + { + ["requires-dev"] = new TomlTable { ["dev"] = new TomlArray { new TomlTable { ["name"] = "baz" } } }, + }; + UvLock.ParseMetadata(metadata, pkg); + pkg.MetadataRequiresDist.Should().BeEmpty(); + pkg.MetadataRequiresDev.Should().ContainSingle(d => d.Name == "baz"); + } } } From 611cea6f256e60dee28fbda5136c81732cea0e87 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Mon, 9 Jun 2025 14:08:02 +0200 Subject: [PATCH 28/46] 100% --- .../UvLockTests.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs index 77cb51f8a..c658af58a 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs @@ -300,5 +300,27 @@ public void ParseMetadata_BranchCoverage_RequiresDevOnly() pkg.MetadataRequiresDist.Should().BeEmpty(); pkg.MetadataRequiresDev.Should().ContainSingle(d => d.Name == "baz"); } + + [TestMethod] + public void ParseMetadata_RequiresDevTableWithoutDevArray_DoesNotThrowOrSet() + { + var pkg = new UvPackage { Name = "foo", Version = "1.0.0" }; + + // requires-dev exists but no "dev" key + var metadata = new TomlTable + { + ["requires-dev"] = new TomlTable { ["notdev"] = 42 }, + }; + UvLock.ParseMetadata(metadata, pkg); + pkg.MetadataRequiresDev.Should().BeEmpty(); + + // requires-dev exists, "dev" is not a TomlArray + metadata = new TomlTable + { + ["requires-dev"] = new TomlTable { ["dev"] = 42 }, + }; + UvLock.ParseMetadata(metadata, pkg); + pkg.MetadataRequiresDev.Should().BeEmpty(); + } } } From 98d6def8188ba0f65ba6372c6fd2c10acc4b98ad Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Mon, 9 Jun 2025 22:54:48 +0200 Subject: [PATCH 29/46] add IDefaultOffComponentDetector --- .../uv/UvLockComponentDetector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs index 20ece89bf..602077d8c 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs @@ -10,7 +10,7 @@ namespace Microsoft.ComponentDetection.Detectors.Uv using Microsoft.ComponentDetection.Contracts.TypedComponent; using Microsoft.Extensions.Logging; - public class UvLockComponentDetector : FileComponentDetector + public class UvLockComponentDetector : FileComponentDetector, IDefaultOffComponentDetector { public UvLockComponentDetector( IComponentStreamEnumerableFactory componentStreamEnumerableFactory, From 835f3b43e4fbd10138c638c88e8e5bff2b716aab Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Thu, 12 Jun 2025 13:50:39 +0200 Subject: [PATCH 30/46] add links --- docs/detectors/uv.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/detectors/uv.md b/docs/detectors/uv.md index 2a1dba7ea..e9835f4b7 100644 --- a/docs/detectors/uv.md +++ b/docs/detectors/uv.md @@ -1,6 +1,6 @@ # uv Detection ## Requirements -uv detection relies on a uv.lock file being present. +[uv](https://docs.astral.sh/uv/) detection relies on a [uv.lock](https://docs.astral.sh/uv/concepts/projects/layout/#the-lockfile) file being present. ## Detection strategy uv detection is performed by parsing a uv.lock found under the scan directory. From 424e4dfd1cd8b13dfd705d50d6e22a31d3c47474 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Thu, 12 Jun 2025 13:50:55 +0200 Subject: [PATCH 31/46] add UvLockDetectorExperiment --- .../Configs/UvLockDetectorExperiment.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/UvLockDetectorExperiment.cs diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/UvLockDetectorExperiment.cs b/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/UvLockDetectorExperiment.cs new file mode 100644 index 000000000..c6ae710a3 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/UvLockDetectorExperiment.cs @@ -0,0 +1,22 @@ +namespace Microsoft.ComponentDetection.Orchestrator.Experiments.Configs; + +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Detectors.Uv; + +/// +/// Experiment to validate UvLockComponentDetector against PipComponentDetector. +/// +public class UvLockDetectorExperiment : IExperimentConfiguration +{ + /// + public string Name => "UvLockDetectorExperiment"; + + /// + public bool IsInControlGroup(IComponentDetector componentDetector) => false; + + /// + public bool IsInExperimentGroup(IComponentDetector componentDetector) => componentDetector is UvLockComponentDetector; + + /// + public bool ShouldRecord(IComponentDetector componentDetector, int numComponents) => true; +} From 6a17664efbf3a680d281608d5839de2498c0bbf6 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Thu, 12 Jun 2025 14:59:42 +0200 Subject: [PATCH 32/46] fix DetectorRestrictionService --- .../Services/DetectorRestrictionService.cs | 36 +++++++++++++------ .../DetectorRestrictionServiceTests.cs | 30 ++++++++++++++++ 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorRestrictionService.cs b/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorRestrictionService.cs index 958a5668d..340808362 100644 --- a/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorRestrictionService.cs +++ b/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorRestrictionService.cs @@ -20,26 +20,33 @@ public IEnumerable ApplyRestrictions(DetectorRestrictions re { // Get a list of our default off detectors beforehand so that they can always be considered var defaultOffDetectors = detectors.Where(x => x is IDefaultOffComponentDetector).ToList(); - detectors = detectors.Where(x => !(x is IDefaultOffComponentDetector)).ToList(); + var nonDefaultOffDetectors = detectors.Where(x => !(x is IDefaultOffComponentDetector)).ToList(); // If someone specifies an "allow list", use it, otherwise assume everything is allowed if (restrictions.AllowedDetectorIds != null && restrictions.AllowedDetectorIds.Any()) { - var allowedIds = restrictions.AllowedDetectorIds; + var allowedIds = restrictions.AllowedDetectorIds.ToList(); // If we have retired detectors in the arg specified list and don't have the new detector, add the new detector if (allowedIds.Any(a => this.oldDetectorIds.Contains(a, StringComparer.OrdinalIgnoreCase)) && !allowedIds.Contains(this.newDetectorId, StringComparer.OrdinalIgnoreCase)) { allowedIds = allowedIds.Concat([ this.newDetectorId, - ]); + ]).ToList(); } - detectors = detectors.Where(d => allowedIds.Contains(d.Id, StringComparer.OrdinalIgnoreCase)).ToList(); + // Always include explicitly enabled default-off detectors if they are in the filter + var explicitlyEnabledDefaultOff = defaultOffDetectors + .Where(d => allowedIds.Contains(d.Id, StringComparer.OrdinalIgnoreCase) || + (restrictions.ExplicitlyEnabledDetectorIds != null && restrictions.ExplicitlyEnabledDetectorIds.Contains(d.Id))) + .ToList(); + + var filtered = nonDefaultOffDetectors.Where(d => allowedIds.Contains(d.Id, StringComparer.OrdinalIgnoreCase)).ToList(); + filtered.AddRange(explicitlyEnabledDefaultOff); foreach (var id in allowedIds) { - if (!detectors.Select(d => d.Id).Contains(id, StringComparer.OrdinalIgnoreCase)) + if (!filtered.Select(d => d.Id).Contains(id, StringComparer.OrdinalIgnoreCase)) { if (!this.oldDetectorIds.Contains(id, StringComparer.OrdinalIgnoreCase)) { @@ -51,6 +58,20 @@ public IEnumerable ApplyRestrictions(DetectorRestrictions re } } } + + detectors = filtered; + } + else + { + // If no filter, only add default-off detectors if explicitly enabled + if (restrictions.ExplicitlyEnabledDetectorIds != null && restrictions.ExplicitlyEnabledDetectorIds.Any()) + { + detectors = nonDefaultOffDetectors.Union(defaultOffDetectors.Where(x => restrictions.ExplicitlyEnabledDetectorIds.Contains(x.Id))).ToList(); + } + else + { + detectors = nonDefaultOffDetectors; + } } var allCategoryName = Enum.GetName(typeof(DetectorClass), DetectorClass.All); @@ -75,11 +96,6 @@ public IEnumerable ApplyRestrictions(DetectorRestrictions re } } - if (restrictions.ExplicitlyEnabledDetectorIds != null && restrictions.ExplicitlyEnabledDetectorIds.Any()) - { - detectors = detectors.Union(defaultOffDetectors.Where(x => restrictions.ExplicitlyEnabledDetectorIds.Contains(x.Id))).ToList(); - } - return detectors; } } diff --git a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/DetectorRestrictionServiceTests.cs b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/DetectorRestrictionServiceTests.cs index 4a5355508..a97588661 100644 --- a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/DetectorRestrictionServiceTests.cs +++ b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/DetectorRestrictionServiceTests.cs @@ -179,6 +179,36 @@ public void WithRestrictions_AlwaysIncludesDetectorsThatSpecifyAllCategory() .And.Contain(detectors[2]); } + [TestMethod] + public void WithRestrictions_ExplicitlyEnabledDefaultOffDetector_WithFilter_AllowsDetector() + { + var r = new DetectorRestrictions + { + AllowedDetectorIds = ["defaultOffDetector"], + ExplicitlyEnabledDetectorIds = ["defaultOffDetector"], + }; + var detectorMock = this.GenerateDetector("defaultOffDetector"); + var defaultOffDetectorMock = detectorMock.As(); + this.detectors = this.detectors.Union([defaultOffDetectorMock.Object]).ToArray(); + var restrictedDetectors = this.serviceUnderTest.ApplyRestrictions(r, this.detectors); + restrictedDetectors.Should().Contain(defaultOffDetectorMock.Object); + } + + [TestMethod] + public void WithRestrictions_ExplicitlyEnabledDefaultOffDetector_WithFilter_ThrowsIfNotEnabled() + { + var r = new DetectorRestrictions + { + AllowedDetectorIds = ["defaultOffDetector"], + ExplicitlyEnabledDetectorIds = [], + }; + var detectorMock = this.GenerateDetector("defaultOffDetector"); + var defaultOffDetectorMock = detectorMock.As(); + this.detectors = this.detectors.Union([defaultOffDetectorMock.Object]).ToArray(); + Action shouldThrow = () => this.serviceUnderTest.ApplyRestrictions(r, this.detectors); + shouldThrow.Should().Throw(); + } + private Mock GenerateDetector(string detectorName, string[] categories = null) { var mockDetector = new Mock(); From dc6aa1105d2e3f58335f53bd426fd29ecfb48471 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Thu, 12 Jun 2025 15:04:19 +0200 Subject: [PATCH 33/46] add experiment tests --- .../Configs/UvLockDetectorExperiment.cs | 3 +- .../Extensions/ServiceCollectionExtensions.cs | 1 + .../UvLockDetectorExperimentTests.cs | 34 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/UvLockDetectorExperimentTests.cs diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/UvLockDetectorExperiment.cs b/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/UvLockDetectorExperiment.cs index c6ae710a3..c71134702 100644 --- a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/UvLockDetectorExperiment.cs +++ b/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/UvLockDetectorExperiment.cs @@ -1,6 +1,7 @@ namespace Microsoft.ComponentDetection.Orchestrator.Experiments.Configs; using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Detectors.Pip; using Microsoft.ComponentDetection.Detectors.Uv; /// @@ -12,7 +13,7 @@ public class UvLockDetectorExperiment : IExperimentConfiguration public string Name => "UvLockDetectorExperiment"; /// - public bool IsInControlGroup(IComponentDetector componentDetector) => false; + public bool IsInControlGroup(IComponentDetector componentDetector) => componentDetector is PipComponentDetector; /// public bool IsInExperimentGroup(IComponentDetector componentDetector) => componentDetector is UvLockComponentDetector; diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs index 99d714ce6..8b61b57db 100644 --- a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs +++ b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs @@ -68,6 +68,7 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // Detectors // CocoaPods diff --git a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/UvLockDetectorExperimentTests.cs b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/UvLockDetectorExperimentTests.cs new file mode 100644 index 000000000..e47967d22 --- /dev/null +++ b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/UvLockDetectorExperimentTests.cs @@ -0,0 +1,34 @@ +namespace Microsoft.ComponentDetection.Orchestrator.Tests.Experiments; + +using FluentAssertions; +using Microsoft.ComponentDetection.Detectors.Pip; +using Microsoft.ComponentDetection.Detectors.Uv; +using Microsoft.ComponentDetection.Orchestrator.Experiments.Configs; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class UvLockDetectorExperimentTests +{ + private readonly UvLockDetectorExperiment experiment = new(); + + [TestMethod] + public void IsInControlGroup_ReturnsTrue_ForPipComponentDetector() + { + var pipDetector = new PipComponentDetector(null, null, null, null, null); + this.experiment.IsInControlGroup(pipDetector).Should().BeTrue(); + } + + [TestMethod] + public void IsInExperimentGroup_ReturnsTrue_ForUvLockComponentDetector() + { + var uvLockDetector = new UvLockComponentDetector(null, null, null); + this.experiment.IsInExperimentGroup(uvLockDetector).Should().BeTrue(); + } + + [TestMethod] + public void ShouldRecord_AlwaysReturnsTrue() + { + var pipDetector = new PipComponentDetector(null, null, null, null, null); + this.experiment.ShouldRecord(pipDetector, 0).Should().BeTrue(); + } +} From 2d18463a131cea24144e1b2bcdbf2576b9be5ccd Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Thu, 12 Jun 2025 15:07:36 +0200 Subject: [PATCH 34/46] not the behavior we want --- .../Services/DetectorRestrictionServiceTests.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/DetectorRestrictionServiceTests.cs b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/DetectorRestrictionServiceTests.cs index a97588661..990927466 100644 --- a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/DetectorRestrictionServiceTests.cs +++ b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/DetectorRestrictionServiceTests.cs @@ -194,21 +194,6 @@ public void WithRestrictions_ExplicitlyEnabledDefaultOffDetector_WithFilter_Allo restrictedDetectors.Should().Contain(defaultOffDetectorMock.Object); } - [TestMethod] - public void WithRestrictions_ExplicitlyEnabledDefaultOffDetector_WithFilter_ThrowsIfNotEnabled() - { - var r = new DetectorRestrictions - { - AllowedDetectorIds = ["defaultOffDetector"], - ExplicitlyEnabledDetectorIds = [], - }; - var detectorMock = this.GenerateDetector("defaultOffDetector"); - var defaultOffDetectorMock = detectorMock.As(); - this.detectors = this.detectors.Union([defaultOffDetectorMock.Object]).ToArray(); - Action shouldThrow = () => this.serviceUnderTest.ApplyRestrictions(r, this.detectors); - shouldThrow.Should().Throw(); - } - private Mock GenerateDetector(string detectorName, string[] categories = null) { var mockDetector = new Mock(); From 5e7fb3521f2e092338e76268a14fa114964c4cef Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Thu, 12 Jun 2025 15:36:55 +0200 Subject: [PATCH 35/46] parse package source --- .../uv/UvLock.cs | 11 +++ .../uv/UvPackage.cs | 3 + .../uv/UvSource.cs | 11 +++ .../UvLockTests.cs | 69 +++++++++++++++++++ 4 files changed, 94 insertions(+) create mode 100644 src/Microsoft.ComponentDetection.Detectors/uv/UvSource.cs diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs index d7fd8d653..c74fa149b 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs @@ -78,6 +78,17 @@ internal static List ParsePackagesFromModel(object? model) ParseMetadata(metadataTable, uvPackage); } + // Parse source + if (pkgTable.TryGetValue("source", out var sourceObj) && sourceObj is TomlTable sourceTable) + { + var source = new UvSource + { + Registry = sourceTable.TryGetValue("registry", out var regObj) && regObj is string reg ? reg : null, + Virtual = sourceTable.TryGetValue("virtual", out var virtObj) && virtObj is string virt ? virt : null, + }; + uvPackage.Source = source; + } + return uvPackage; } diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs index 37b33a100..1e7e4a33d 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs @@ -16,5 +16,8 @@ public class UvPackage // Metadata dev dependencies (requires-dev) public List MetadataRequiresDev { get; set; } = []; + + // Source property for uv.lock + public UvSource? Source { get; set; } } } diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvSource.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvSource.cs new file mode 100644 index 000000000..6c06b3625 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvSource.cs @@ -0,0 +1,11 @@ +#nullable enable + +namespace Microsoft.ComponentDetection.Detectors.Uv +{ + public class UvSource + { + public string? Registry { get; set; } + + public string? Virtual { get; set; } + } +} diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs index c658af58a..fea14df3d 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs @@ -322,5 +322,74 @@ public void ParseMetadata_RequiresDevTableWithoutDevArray_DoesNotThrowOrSet() UvLock.ParseMetadata(metadata, pkg); pkg.MetadataRequiresDev.Should().BeEmpty(); } + + [TestMethod] + public void ParsePackage_ParsesSourceRegistryAndVirtual() + { + var toml = """ +[[package]] +name = 'foo' +version = '1.0.0' +source = { registry = 'https://example.com/', virtual = '.' } +"""; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().ContainSingle(); + var pkg = uvLock.Packages.First(); + pkg.Source.Should().NotBeNull(); + pkg.Source!.Registry.Should().Be("https://example.com/"); + pkg.Source.Virtual.Should().Be("."); + } + + [TestMethod] + public void ParsePackage_ParsesSource_RegistryOnly() + { + var toml = """ +[[package]] +name = 'foo' +version = '1.0.0' +source = { registry = 'https://example.com/' } +"""; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().ContainSingle(); + var pkg = uvLock.Packages.First(); + pkg.Source.Should().NotBeNull(); + pkg.Source!.Registry.Should().Be("https://example.com/"); + pkg.Source.Virtual.Should().BeNull(); + } + + [TestMethod] + public void ParsePackage_ParsesSource_VirtualOnly() + { + var toml = """ +[[package]] +name = 'foo' +version = '1.0.0' +source = { virtual = '.' } +"""; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().ContainSingle(); + var pkg = uvLock.Packages.First(); + pkg.Source.Should().NotBeNull(); + pkg.Source!.Registry.Should().BeNull(); + pkg.Source.Virtual.Should().Be("."); + } + + [TestMethod] + public void ParsePackage_ParsesSource_Missing() + { + var toml = """ +[[package]] +name = 'foo' +version = '1.0.0' +"""; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().ContainSingle(); + var pkg = uvLock.Packages.First(); + pkg.Source.Should().BeNull(); + } } } From c7f1dc766a18e806971778c2a9c07d45ef27505e Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Thu, 12 Jun 2025 17:17:33 +0200 Subject: [PATCH 36/46] add explicitreference --- .../uv/UvLockComponentDetector.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs index 602077d8c..ef2d214a6 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs @@ -45,6 +45,24 @@ protected override Task OnFileFoundAsync(ProcessRequest processRequest, IDiction foreach (var pkg in uvLock.Packages) { + // Handle virtual source + if (pkg.Source?.Virtual != null) + { + if (pkg.Source.Virtual == ".") + { + // Add requires-dist as explicitly referenced component ids and dependencies + foreach (var dep in pkg.MetadataRequiresDist) + { + var depComponent = new PipComponent(dep.Name, dep.Specifier); + var detectedDep = new DetectedComponent(depComponent); + singleFileComponentRecorder.RegisterUsage(detectedDep, isExplicitReferencedDependency: true, isDevelopmentDependency: false); + } + } + + // Skip all virtual packages (including ".") for the graph + continue; + } + var pipComponent = new PipComponent(pkg.Name, pkg.Version); var detectedComponent = new DetectedComponent(pipComponent); var isExplicit = false; // TODO From 72152273ec4b646844aeddee79a81c927d69fcf0 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Thu, 12 Jun 2025 17:36:08 +0200 Subject: [PATCH 37/46] add test for explicit dep --- .../UvLockDetectorTests.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs index 15203d8ff..58cf62bd6 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs @@ -191,4 +191,31 @@ public async Task TestUvLockDetectorWithInvalidFile_LogsErrorAsync() It.IsAny>()), Times.AtLeastOnce()); } + + [TestMethod] + public async Task TestUvLockDetector_ExplicitDependencies_AreMarkedExplicit() + { + var uvLock = """ +[[package]] +name = 'foo' +version = '1.2.3' +source = { virtual = '.' } + +[package.metadata] +requires-dist = [ + { name = "bar", specifier = ">=3.9.1" }, +] +"""; + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("uv.lock", uvLock) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + var detected = componentRecorder.GetDetectedComponents().ToList(); + var graph = componentRecorder.GetDependencyGraphsByLocation().Values.First(); + + detected.Should().ContainSingle(); + var barId = detected.First(d => d.Component.Id.StartsWith("bar")).Component.Id; + graph.IsComponentExplicitlyReferenced(barId).Should().BeTrue(); + } } From 288c567c2cd7bf3c57766721a1278dda631c6e28 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Thu, 12 Jun 2025 18:28:35 +0200 Subject: [PATCH 38/46] fix dependency and devDependency --- .../uv/UvLockComponentDetector.cs | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs index ef2d214a6..bb3a86f88 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs @@ -32,6 +32,11 @@ public UvLockComponentDetector( public override IEnumerable Categories => ["Python"]; + internal static bool IsRootPackage(UvPackage pck) + { + return pck.Source?.Virtual != null; + } + protected override Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary detectorArgs, CancellationToken cancellationToken = default) { var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder; @@ -43,30 +48,33 @@ protected override Task OnFileFoundAsync(ProcessRequest processRequest, IDiction file.Stream.Position = 0; // Ensure stream is at the beginning var uvLock = UvLock.Parse(file.Stream); + var rootPackage = uvLock.Packages.FirstOrDefault(IsRootPackage); + + // Add requires-dist as explicitly referenced component ids and dependencies + foreach (var dep in rootPackage.MetadataRequiresDist) + { + var depComponent = new PipComponent(dep.Name, dep.Specifier); + var detectedDep = new DetectedComponent(depComponent); + singleFileComponentRecorder.RegisterUsage(detectedDep, isExplicitReferencedDependency: true, isDevelopmentDependency: false); + } + + var devPackages = new HashSet(); + foreach (var devDep in rootPackage.MetadataRequiresDev) + { + devPackages.Add(devDep.Name); + } + foreach (var pkg in uvLock.Packages) { - // Handle virtual source - if (pkg.Source?.Virtual != null) + if (IsRootPackage(pkg)) { - if (pkg.Source.Virtual == ".") - { - // Add requires-dist as explicitly referenced component ids and dependencies - foreach (var dep in pkg.MetadataRequiresDist) - { - var depComponent = new PipComponent(dep.Name, dep.Specifier); - var detectedDep = new DetectedComponent(depComponent); - singleFileComponentRecorder.RegisterUsage(detectedDep, isExplicitReferencedDependency: true, isDevelopmentDependency: false); - } - } - - // Skip all virtual packages (including ".") for the graph continue; } var pipComponent = new PipComponent(pkg.Name, pkg.Version); + var isDevelopmentDependency = devPackages.Contains(pkg.Name); var detectedComponent = new DetectedComponent(pipComponent); - var isExplicit = false; // TODO - singleFileComponentRecorder.RegisterUsage(detectedComponent, isExplicitReferencedDependency: isExplicit); + singleFileComponentRecorder.RegisterUsage(detectedComponent, isDevelopmentDependency: isDevelopmentDependency); foreach (var dep in pkg.Dependencies) { From e540df8fccd945ee68eb8a7eaa91f2dcece1cca2 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Thu, 12 Jun 2025 18:58:08 +0200 Subject: [PATCH 39/46] fix dev and non dev lists --- .../uv/UvLockComponentDetector.cs | 13 +++--- .../UvLockDetectorTests.cs | 44 +++++++++++++++++++ 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs index bb3a86f88..2125ca640 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs @@ -50,12 +50,10 @@ protected override Task OnFileFoundAsync(ProcessRequest processRequest, IDiction var rootPackage = uvLock.Packages.FirstOrDefault(IsRootPackage); - // Add requires-dist as explicitly referenced component ids and dependencies + var explicitPackages = new HashSet(); foreach (var dep in rootPackage.MetadataRequiresDist) { - var depComponent = new PipComponent(dep.Name, dep.Specifier); - var detectedDep = new DetectedComponent(depComponent); - singleFileComponentRecorder.RegisterUsage(detectedDep, isExplicitReferencedDependency: true, isDevelopmentDependency: false); + explicitPackages.Add(dep.Name); } var devPackages = new HashSet(); @@ -72,9 +70,10 @@ protected override Task OnFileFoundAsync(ProcessRequest processRequest, IDiction } var pipComponent = new PipComponent(pkg.Name, pkg.Version); - var isDevelopmentDependency = devPackages.Contains(pkg.Name); + var isExplicit = explicitPackages.Contains(pkg.Name); + var isDev = devPackages.Contains(pkg.Name); var detectedComponent = new DetectedComponent(pipComponent); - singleFileComponentRecorder.RegisterUsage(detectedComponent, isDevelopmentDependency: isDevelopmentDependency); + singleFileComponentRecorder.RegisterUsage(detectedComponent, isDevelopmentDependency: isDev, isExplicitReferencedDependency: isExplicit); foreach (var dep in pkg.Dependencies) { @@ -82,7 +81,7 @@ protected override Task OnFileFoundAsync(ProcessRequest processRequest, IDiction if (depPkg != null) { var depComponentWithVersion = new PipComponent(depPkg.Name, depPkg.Version); - singleFileComponentRecorder.RegisterUsage(new DetectedComponent(depComponentWithVersion), isExplicitReferencedDependency: false, parentComponentId: pipComponent.Id); + singleFileComponentRecorder.RegisterUsage(new DetectedComponent(depComponentWithVersion), parentComponentId: pipComponent.Id); } else { diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs index 58cf62bd6..bfa13c7f2 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs @@ -218,4 +218,48 @@ public async Task TestUvLockDetector_ExplicitDependencies_AreMarkedExplicit() var barId = detected.First(d => d.Component.Id.StartsWith("bar")).Component.Id; graph.IsComponentExplicitlyReferenced(barId).Should().BeTrue(); } + + [TestMethod] + public async Task TestUvLockDetector_DevelopmentAndNonDevelopmentDependencies() + { + var uvLock = @"[[package]] +name = 'foo' +version = '1.2.3' +[package.metadata] +requires-dist = [ + { name = 'bar', specifier = '>=2.0.0' }, + { name = 'baz', specifier = '>=3.0.0' } +] +[package.metadata.requires-dev] +dev = [ + { name = 'devonly', specifier = '>=4.0.0' } +] +[[package]] +name = 'bar' +version = '2.0.0' +[[package]] +name = 'baz' +version = '3.0.0' +[[package]] +name = 'devonly' +version = '4.0.0' +"; + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("uv.lock", uvLock) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + var detected = componentRecorder.GetDetectedComponents().ToList(); + var graph = componentRecorder.GetDependencyGraphsByLocation().Values.First(); + + var fooId = detected.First(d => d.Component.Id.StartsWith("foo ")).Component.Id; + var barId = detected.First(d => d.Component.Id.StartsWith("bar ")).Component.Id; + var bazId = detected.First(d => d.Component.Id.StartsWith("baz ")).Component.Id; + var devonlyId = detected.First(d => d.Component.Id.StartsWith("devonly ")).Component.Id; + + // bar and baz are non-dev dependencies, devonly is a dev dependency + graph.IsDevelopmentDependency(barId).Should().BeFalse(); + graph.IsDevelopmentDependency(bazId).Should().BeFalse(); + graph.IsDevelopmentDependency(devonlyId).Should().BeTrue(); + } } From d03ee6c413655b7805046b3455186c31b1fd72ab Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Tue, 17 Jun 2025 11:52:03 +0200 Subject: [PATCH 40/46] null checks --- .../uv/UvLockComponentDetector.cs | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs index 2125ca640..d12555cb7 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs @@ -51,15 +51,19 @@ protected override Task OnFileFoundAsync(ProcessRequest processRequest, IDiction var rootPackage = uvLock.Packages.FirstOrDefault(IsRootPackage); var explicitPackages = new HashSet(); - foreach (var dep in rootPackage.MetadataRequiresDist) - { - explicitPackages.Add(dep.Name); - } - var devPackages = new HashSet(); - foreach (var devDep in rootPackage.MetadataRequiresDev) + + if (rootPackage != null) { - devPackages.Add(devDep.Name); + foreach (var dep in rootPackage.MetadataRequiresDist) + { + explicitPackages.Add(dep.Name); + } + + foreach (var devDep in rootPackage.MetadataRequiresDev) + { + devPackages.Add(devDep.Name); + } } foreach (var pkg in uvLock.Packages) @@ -75,17 +79,20 @@ protected override Task OnFileFoundAsync(ProcessRequest processRequest, IDiction var detectedComponent = new DetectedComponent(pipComponent); singleFileComponentRecorder.RegisterUsage(detectedComponent, isDevelopmentDependency: isDev, isExplicitReferencedDependency: isExplicit); - foreach (var dep in pkg.Dependencies) + if (pkg.Dependencies != null) { - var depPkg = uvLock.Packages.FirstOrDefault(p => p.Name.Equals(dep.Name, StringComparison.OrdinalIgnoreCase)); - if (depPkg != null) - { - var depComponentWithVersion = new PipComponent(depPkg.Name, depPkg.Version); - singleFileComponentRecorder.RegisterUsage(new DetectedComponent(depComponentWithVersion), parentComponentId: pipComponent.Id); - } - else + foreach (var dep in pkg.Dependencies) { - this.Logger.LogWarning("Dependency {DependencyName} not found in uv.lock packages", dep.Name); + var depPkg = uvLock.Packages.FirstOrDefault(p => p.Name.Equals(dep.Name, StringComparison.OrdinalIgnoreCase)); + if (depPkg != null) + { + var depComponentWithVersion = new PipComponent(depPkg.Name, depPkg.Version); + singleFileComponentRecorder.RegisterUsage(new DetectedComponent(depComponentWithVersion), parentComponentId: pipComponent.Id); + } + else + { + this.Logger.LogWarning("Dependency {DependencyName} not found in uv.lock packages", dep.Name); + } } } } From f5097b74d210acdebf181678284590ba0ac0e22d Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Tue, 17 Jun 2025 12:14:01 +0200 Subject: [PATCH 41/46] add #nullable enable --- .../uv/UvLockComponentDetector.cs | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs index d12555cb7..d111e8487 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace Microsoft.ComponentDetection.Detectors.Uv { using System; @@ -49,7 +51,6 @@ protected override Task OnFileFoundAsync(ProcessRequest processRequest, IDiction var uvLock = UvLock.Parse(file.Stream); var rootPackage = uvLock.Packages.FirstOrDefault(IsRootPackage); - var explicitPackages = new HashSet(); var devPackages = new HashSet(); @@ -79,20 +80,17 @@ protected override Task OnFileFoundAsync(ProcessRequest processRequest, IDiction var detectedComponent = new DetectedComponent(pipComponent); singleFileComponentRecorder.RegisterUsage(detectedComponent, isDevelopmentDependency: isDev, isExplicitReferencedDependency: isExplicit); - if (pkg.Dependencies != null) + foreach (var dep in pkg.Dependencies) { - foreach (var dep in pkg.Dependencies) + var depPkg = uvLock.Packages.FirstOrDefault(p => p.Name.Equals(dep.Name, StringComparison.OrdinalIgnoreCase)); + if (depPkg != null) + { + var depComponentWithVersion = new PipComponent(depPkg.Name, depPkg.Version); + singleFileComponentRecorder.RegisterUsage(new DetectedComponent(depComponentWithVersion), parentComponentId: pipComponent.Id); + } + else { - var depPkg = uvLock.Packages.FirstOrDefault(p => p.Name.Equals(dep.Name, StringComparison.OrdinalIgnoreCase)); - if (depPkg != null) - { - var depComponentWithVersion = new PipComponent(depPkg.Name, depPkg.Version); - singleFileComponentRecorder.RegisterUsage(new DetectedComponent(depComponentWithVersion), parentComponentId: pipComponent.Id); - } - else - { - this.Logger.LogWarning("Dependency {DependencyName} not found in uv.lock packages", dep.Name); - } + this.Logger.LogWarning("Dependency {DependencyName} not found in uv.lock packages", dep.Name); } } } From 066e1d8d045dcf2ee9924e2b83fc433ae6a70c16 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Tue, 17 Jun 2025 12:14:15 +0200 Subject: [PATCH 42/46] fix tests --- .../UvLockDetectorTests.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs index bfa13c7f2..19cf0cc7a 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs @@ -205,6 +205,10 @@ public async Task TestUvLockDetector_ExplicitDependencies_AreMarkedExplicit() requires-dist = [ { name = "bar", specifier = ">=3.9.1" }, ] + +[[package]] +name = 'bar' +version = '2.0.0' """; var (scanResult, componentRecorder) = await this.DetectorTestUtility .WithFile("uv.lock", uvLock) @@ -225,6 +229,7 @@ public async Task TestUvLockDetector_DevelopmentAndNonDevelopmentDependencies() var uvLock = @"[[package]] name = 'foo' version = '1.2.3' +source = { virtual = '.' } [package.metadata] requires-dist = [ { name = 'bar', specifier = '>=2.0.0' }, @@ -252,10 +257,9 @@ public async Task TestUvLockDetector_DevelopmentAndNonDevelopmentDependencies() var detected = componentRecorder.GetDetectedComponents().ToList(); var graph = componentRecorder.GetDependencyGraphsByLocation().Values.First(); - var fooId = detected.First(d => d.Component.Id.StartsWith("foo ")).Component.Id; var barId = detected.First(d => d.Component.Id.StartsWith("bar ")).Component.Id; var bazId = detected.First(d => d.Component.Id.StartsWith("baz ")).Component.Id; - var devonlyId = detected.First(d => d.Component.Id.StartsWith("devonly ")).Component.Id; + var devonlyId = new PipComponent("devonly", "4.0.0").Id; // bar and baz are non-dev dependencies, devonly is a dev dependency graph.IsDevelopmentDependency(barId).Should().BeFalse(); From 58e453fd125455eebfa89b6df798fe26b0ab451c Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Tue, 17 Jun 2025 12:21:13 +0200 Subject: [PATCH 43/46] undo DetectorRestrictionService --- .../Services/DetectorRestrictionService.cs | 36 ++++++------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorRestrictionService.cs b/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorRestrictionService.cs index 340808362..958a5668d 100644 --- a/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorRestrictionService.cs +++ b/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorRestrictionService.cs @@ -20,33 +20,26 @@ public IEnumerable ApplyRestrictions(DetectorRestrictions re { // Get a list of our default off detectors beforehand so that they can always be considered var defaultOffDetectors = detectors.Where(x => x is IDefaultOffComponentDetector).ToList(); - var nonDefaultOffDetectors = detectors.Where(x => !(x is IDefaultOffComponentDetector)).ToList(); + detectors = detectors.Where(x => !(x is IDefaultOffComponentDetector)).ToList(); // If someone specifies an "allow list", use it, otherwise assume everything is allowed if (restrictions.AllowedDetectorIds != null && restrictions.AllowedDetectorIds.Any()) { - var allowedIds = restrictions.AllowedDetectorIds.ToList(); + var allowedIds = restrictions.AllowedDetectorIds; // If we have retired detectors in the arg specified list and don't have the new detector, add the new detector if (allowedIds.Any(a => this.oldDetectorIds.Contains(a, StringComparer.OrdinalIgnoreCase)) && !allowedIds.Contains(this.newDetectorId, StringComparer.OrdinalIgnoreCase)) { allowedIds = allowedIds.Concat([ this.newDetectorId, - ]).ToList(); + ]); } - // Always include explicitly enabled default-off detectors if they are in the filter - var explicitlyEnabledDefaultOff = defaultOffDetectors - .Where(d => allowedIds.Contains(d.Id, StringComparer.OrdinalIgnoreCase) || - (restrictions.ExplicitlyEnabledDetectorIds != null && restrictions.ExplicitlyEnabledDetectorIds.Contains(d.Id))) - .ToList(); - - var filtered = nonDefaultOffDetectors.Where(d => allowedIds.Contains(d.Id, StringComparer.OrdinalIgnoreCase)).ToList(); - filtered.AddRange(explicitlyEnabledDefaultOff); + detectors = detectors.Where(d => allowedIds.Contains(d.Id, StringComparer.OrdinalIgnoreCase)).ToList(); foreach (var id in allowedIds) { - if (!filtered.Select(d => d.Id).Contains(id, StringComparer.OrdinalIgnoreCase)) + if (!detectors.Select(d => d.Id).Contains(id, StringComparer.OrdinalIgnoreCase)) { if (!this.oldDetectorIds.Contains(id, StringComparer.OrdinalIgnoreCase)) { @@ -58,20 +51,6 @@ public IEnumerable ApplyRestrictions(DetectorRestrictions re } } } - - detectors = filtered; - } - else - { - // If no filter, only add default-off detectors if explicitly enabled - if (restrictions.ExplicitlyEnabledDetectorIds != null && restrictions.ExplicitlyEnabledDetectorIds.Any()) - { - detectors = nonDefaultOffDetectors.Union(defaultOffDetectors.Where(x => restrictions.ExplicitlyEnabledDetectorIds.Contains(x.Id))).ToList(); - } - else - { - detectors = nonDefaultOffDetectors; - } } var allCategoryName = Enum.GetName(typeof(DetectorClass), DetectorClass.All); @@ -96,6 +75,11 @@ public IEnumerable ApplyRestrictions(DetectorRestrictions re } } + if (restrictions.ExplicitlyEnabledDetectorIds != null && restrictions.ExplicitlyEnabledDetectorIds.Any()) + { + detectors = detectors.Union(defaultOffDetectors.Where(x => restrictions.ExplicitlyEnabledDetectorIds.Contains(x.Id))).ToList(); + } + return detectors; } } From dc218a5c6e01d1560390470109c5a3536c339472 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Tue, 17 Jun 2025 12:29:42 +0200 Subject: [PATCH 44/46] undo DetectorRestritionServiceTests changes --- .../Services/DetectorRestrictionServiceTests.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/DetectorRestrictionServiceTests.cs b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/DetectorRestrictionServiceTests.cs index 990927466..4a5355508 100644 --- a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/DetectorRestrictionServiceTests.cs +++ b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/DetectorRestrictionServiceTests.cs @@ -179,21 +179,6 @@ public void WithRestrictions_AlwaysIncludesDetectorsThatSpecifyAllCategory() .And.Contain(detectors[2]); } - [TestMethod] - public void WithRestrictions_ExplicitlyEnabledDefaultOffDetector_WithFilter_AllowsDetector() - { - var r = new DetectorRestrictions - { - AllowedDetectorIds = ["defaultOffDetector"], - ExplicitlyEnabledDetectorIds = ["defaultOffDetector"], - }; - var detectorMock = this.GenerateDetector("defaultOffDetector"); - var defaultOffDetectorMock = detectorMock.As(); - this.detectors = this.detectors.Union([defaultOffDetectorMock.Object]).ToArray(); - var restrictedDetectors = this.serviceUnderTest.ApplyRestrictions(r, this.detectors); - restrictedDetectors.Should().Contain(defaultOffDetectorMock.Object); - } - private Mock GenerateDetector(string detectorName, string[] categories = null) { var mockDetector = new Mock(); From 26a21c9c4f015c400e4fbe7c5a09fe89d504a429 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Thu, 19 Jun 2025 22:09:35 +0200 Subject: [PATCH 45/46] PipReportComponentDetector --- .../Experiments/Configs/UvLockDetectorExperiment.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/UvLockDetectorExperiment.cs b/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/UvLockDetectorExperiment.cs index c71134702..c5053e4d8 100644 --- a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/UvLockDetectorExperiment.cs +++ b/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/UvLockDetectorExperiment.cs @@ -13,7 +13,7 @@ public class UvLockDetectorExperiment : IExperimentConfiguration public string Name => "UvLockDetectorExperiment"; /// - public bool IsInControlGroup(IComponentDetector componentDetector) => componentDetector is PipComponentDetector; + public bool IsInControlGroup(IComponentDetector componentDetector) => componentDetector is PipReportComponentDetector; /// public bool IsInExperimentGroup(IComponentDetector componentDetector) => componentDetector is UvLockComponentDetector; From c2d9c07b22a8001ddc21e48d6ae535c19226725d Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 20 Jun 2025 07:50:03 +0200 Subject: [PATCH 46/46] update UvLockDetectorExperimentTests --- .../Experiments/UvLockDetectorExperimentTests.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/UvLockDetectorExperimentTests.cs b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/UvLockDetectorExperimentTests.cs index e47967d22..a3d78c9b9 100644 --- a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/UvLockDetectorExperimentTests.cs +++ b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/UvLockDetectorExperimentTests.cs @@ -12,10 +12,17 @@ public class UvLockDetectorExperimentTests private readonly UvLockDetectorExperiment experiment = new(); [TestMethod] - public void IsInControlGroup_ReturnsTrue_ForPipComponentDetector() + public void IsInControlGroup_ReturnsTrue_ForPipReportComponentDetector() + { + var pipReportDetector = new PipReportComponentDetector(null, null, null, null, null, null, null, null, null); + this.experiment.IsInControlGroup(pipReportDetector).Should().BeTrue(); + } + + [TestMethod] + public void IsInControlGroup_ReturnsFalse_ForPipComponentDetector() { var pipDetector = new PipComponentDetector(null, null, null, null, null); - this.experiment.IsInControlGroup(pipDetector).Should().BeTrue(); + this.experiment.IsInControlGroup(pipDetector).Should().BeFalse(); } [TestMethod]