From b7bac63e95a93bec8b23bae09081f581ed955ac2 Mon Sep 17 00:00:00 2001 From: Teodor Voinea Date: Fri, 19 Nov 2021 06:07:50 -0800 Subject: [PATCH] Initial commit --- .devcontainer/Dockerfile | 23 + .devcontainer/devcontainer.json | 65 + .devcontainer/library-scripts/azcli-debian.sh | 186 + .gitattributes | 69 + .github/CODEOWNERS | 7 + .github/ISSUE_TEMPLATE/bug_report.yml | 39 + .github/ISSUE_TEMPLATE/feature_request.yml | 28 + .../new_detector_template.md | 10 + .github/dependabot.yml | 17 + .github/labeler.yml | 22 + .github/release-drafter.yml | 43 + .github/workflows/build-release.yml | 81 + .github/workflows/build.yml | 33 + .github/workflows/codeql-analysis.yml | 86 + .../detector-version-bump-reminder.yml | 21 + .github/workflows/gen-docs.yml | 47 + .github/workflows/labeler.yml | 10 + .../workflows/publish-release-snapshot.yml | 60 + .github/workflows/test-linux.yml | 48 + .github/workflows/verify-snapshot.yml | 110 + .gitignore | 56 + .tours/add-a-telemetry-record.tour | 59 + .tours/getting-started.tour | 53 + .vscode/launch.json | 27 + .vscode/tasks.json | 42 + CODE_OF_CONDUCT.md | 9 + CONTRIBUTING.md | 66 + ComponentDetection.sln | 192 + Directory.Build.props | 40 + Directory.Build.targets | 11 + Directory.Packages.props | 42 + LICENSE.txt | 21 + README.md | 103 + SECURITY.md | 41 + analyzers.ruleset | 503 + docs/creating-a-new-detector.md | 142 + docs/detector-arguments.md | 71 + docs/enable-default-off.md | 7 + docs/feature-overview.md | 18 + docs/go-detection.md | 27 + docs/gradle-detection.md | 16 + .../component-type.png | Bin 0 -> 12581 bytes .../component-utilities.png | Bin 0 -> 25379 bytes .../detector-class.png | Bin 0 -> 12231 bytes .../go-detector-test-utility.png | Bin 0 -> 88414 bytes .../multi-root-dependency.png | Bin 0 -> 5633 bytes .../creating-a-new-detector/npm-component.png | Bin 0 -> 57191 bytes .../ruby-component-detector.png | Bin 0 -> 64849 bytes .../single-root-dependency.png | Bin 0 -> 2603 bytes .../creating-a-new-detector/test-projects.png | Bin 0 -> 31006 bytes .../creating-a-new-detector/vs-projects.png | Bin 0 -> 33645 bytes docs/linux-scanner.md | 41 + docs/maven-detection.md | 17 + docs/pip-detection.md | 33 + docs/renewing-secrets.md | 25 + global.json | 7 + src/Directory.Build.props | 20 + .../AsyncExecution.cs | 42 + .../Column.cs | 11 + .../CommandLineInvocationService.cs | 158 + .../ComponentComparer.cs | 18 + .../ComponentStream.cs | 14 + .../ComponentStreamEnumerable.cs | 66 + .../ComponentStreamEnumerableFactory.cs | 31 + .../ConsoleWritingService.cs | 14 + .../DependencyGraph/ComponentRecorder.cs | 202 + .../DependencyGraph/DependencyGraph.cs | 185 + .../DetectorDependencies.cs | 30 + .../DirectoryItemFacade.cs | 16 + .../DockerService.cs | 240 + .../Exceptions/InvalidUserInputException.cs | 12 + .../FastDirectoryWalkerFactory.cs | 292 + .../FileUtilityService.cs | 35 + .../FileWritingService.cs | 74 + .../IConsoleWritingService.cs | 7 + .../IFileWritingService.cs | 16 + .../ISafeFileEnumerableFactory.cs | 17 + .../LazyComponentStream.cs | 52 + .../Logger.cs | 154 + .../MatchedFile.cs | 11 + ...Microsoft.ComponentDetection.Common.csproj | 32 + .../PathUtilityService.cs | 289 + .../PatternMatchingUtility.cs | 56 + .../Resources.Designer.cs | 81 + .../Resources.resx | 127 + .../SafeFileEnumerable.cs | 149 + .../SafeFileEnumerableFactory.cs | 23 + .../ScanType.cs | 9 + .../TabularStringFormat.cs | 104 + .../Telemetry/Attributes/MetricAttribute.cs | 13 + .../Attributes/TelemetryServiceAttribute.cs | 14 + .../Telemetry/CommandLineTelemetryService.cs | 55 + .../Telemetry/ITelemetryService.cs | 23 + .../Records/BaseDetectionTelemetryRecord.cs | 51 + .../Records/BcdeExecutionTelemetryRecord.cs | 48 + .../CommandLineInvocationTelemetryRecord.cs | 41 + .../DetectorExecutionTelemetryRecord.cs | 21 + ...erviceImageExistsLocallyTelemetryRecord.cs | 13 + ...ockerServiceInspectImageTelemetryRecord.cs | 17 + .../DockerServiceSystemInfoTelemetryRecord.cs | 11 + .../Records/DockerServiceTelemetryRecord.cs | 17 + ...ockerServiceTryPullImageTelemetryRecord.cs | 13 + .../Records/FailedReadingFileRecord.cs | 13 + .../Records/GoGraphTelemetryRecord.cs | 13 + .../Records/IDetectionTelemetryRecord.cs | 12 + ...uxContainerDetectorImageDetectionFailed.cs | 15 + .../LinuxContainerDetectorLayerAwareness.cs | 17 + ...inerDetectorMissingRepoNameAndTagRecord.cs | 7 + .../LinuxContainerDetectorMissingVersion.cs | 13 + .../Records/LinuxContainerDetectorTimeout.cs | 7 + .../LinuxContainerDetectorUnsupportedOs.cs | 9 + .../LinuxScannerSyftTelemetryRecord.cs | 11 + .../Records/LinuxScannerTelemetryRecord.cs | 19 + .../LoadComponentDetectorsTelemetryRecord.cs | 9 + .../NuGetProjectAssetsTelemetryRecord.cs | 35 + .../Records/PypiFailureTelemetryRecord.cs | 24 + .../PypiMaxRetriesReachedTelemetryRecord.cs | 17 + .../Records/PypiRetryTelemetryRecord.cs | 24 + .../RustCrateV2DetectorTelemetryRecord.cs | 11 + .../Telemetry/TelemetryConstants.cs | 30 + .../Telemetry/TelemetryMode.cs | 11 + .../Telemetry/TelemetryRelay.cs | 93 + .../VerbosityMode.cs | 9 + .../WarnOnAlertSeverity.cs | 11 + .../BcdeModels/ContainerDetails.cs | 45 + .../BcdeModels/DefaultGraphScanResult.cs | 11 + .../BcdeModels/DependencyGraph.cs | 8 + .../BcdeModels/DependencyGraphCollection.cs | 8 + .../BcdeModels/DependencyGraphWithMetadata.cs | 18 + .../BcdeModels/Detector.cs | 21 + .../BcdeModels/DockerLayer.cs | 22 + .../BcdeModels/LayerMappedLinuxComponents.cs | 12 + .../BcdeModels/ScanResult.cs | 20 + .../BcdeModels/ScannedComponent.cs | 24 + .../BcdeModels/TypedComponentConverter.cs | 59 + .../DetectedComponent.cs | 70 + .../DetectorClass.cs | 39 + .../FileComponentDetector.cs | 126 + .../ICommandLineInvocationService.cs | 75 + .../IComponentDetector.cs | 63 + .../IComponentRecorder.cs | 98 + .../IComponentStream.cs | 22 + .../IComponentStreamEnumerableFactory.cs | 31 + .../IDetectorDependencies.cs | 19 + .../IDockerService.cs | 23 + .../IFileUtilityService.cs | 18 + .../ILogger.cs | 58 + .../IObservableDirectoryWalkerFactory.cs | 16 + .../IPathUtilityService.cs | 33 + .../IndividualDetectorScanResult.cs | 17 + .../Internal/InjectionParameters.cs | 59 + .../Internal/ProcessRequest.cs | 9 + .../KillSwitchConfiguration.cs | 9 + ...rosoft.ComponentDetection.Contracts.csproj | 10 + .../ProcessingResultCode.cs | 21 + .../ScanRequest.cs | 38 + .../TypedComponent/CargoComponent.cs | 28 + .../TypedComponent/ComponentType.cs | 48 + .../TypedComponent/CondaComponent.cs | 42 + .../TypedComponent/DockerImageComponent.cs | 27 + .../TypedComponent/GitComponent.cs | 34 + .../TypedComponent/GoComponent.cs | 64 + .../TypedComponent/LinuxComponent.cs | 81 + .../TypedComponent/MavenComponent.cs | 31 + .../TypedComponent/NpmComponent.cs | 31 + .../TypedComponent/NugetComponent.cs | 28 + .../TypedComponent/OtherComponent.cs | 32 + .../TypedComponent/PipComponent.cs | 28 + .../TypedComponent/PodComponent.cs | 44 + .../TypedComponent/RubyGemsComponent.cs | 31 + .../TypedComponent/TypedComponent.cs | 51 + .../IComponentGovernanceOwnedDetectors.cs | 9 + ...rosoft.ComponentDetection.Detectors.csproj | 34 + .../cocoapods/PodComponentDetector.cs | 402 + .../go/GoComponentDetector.cs | 227 + .../gradle/GradleComponentDetector.cs | 83 + .../ivy/IvyDetector.cs | 235 + .../ivy/Resources/build.xml | 19 + .../IvyComponentDetectionAntTask.java | 323 + .../linux/Contracts/Classification.cs | 11 + .../linux/Contracts/ConfigurationUnion.cs | 47 + .../linux/Contracts/Descriptor.cs | 11 + .../linux/Contracts/Digest.cs | 9 + .../linux/Contracts/DigestElement.cs | 9 + .../linux/Contracts/Distribution.cs | 11 + .../linux/Contracts/File.cs | 18 + .../linux/Contracts/FileClassifications.cs | 9 + .../linux/Contracts/FileContents.cs | 9 + .../linux/Contracts/FileMetadata.cs | 9 + .../linux/Contracts/FileMetadataEntry.cs | 17 + .../linux/Contracts/FileRecord.cs | 27 + .../linux/Contracts/ImageScanningResult.cs | 13 + .../linux/Contracts/JavaManifest.cs | 11 + .../linux/Contracts/Location.cs | 9 + .../linux/Contracts/Metadata.cs | 73 + .../linux/Contracts/Package.cs | 29 + .../linux/Contracts/PomProperties.cs | 19 + .../linux/Contracts/Relationship.cs | 13 + .../linux/Contracts/Schema.cs | 9 + .../linux/Contracts/SearchResult.cs | 17 + .../linux/Contracts/Secrets.cs | 9 + .../linux/Contracts/Size.cs | 18 + .../linux/Contracts/Source.cs | 9 + .../linux/Contracts/SyftOutput.cs | 30 + .../MissingContainerDetailException.cs | 12 + .../linux/ILinuxScanner.cs | 12 + .../linux/LinuxContainerDetector.cs | 249 + .../linux/LinuxScanner.cs | 136 + .../maven/GraphNode.cs | 22 + .../maven/IMavenCommandService.cs | 16 + ...IMavenStyleDependencyGraphParserService.cs | 9 + .../maven/MavenCommandService.cs | 55 + .../maven/MavenParsingUtilities.cs | 52 + .../maven/MavenStyleDependencyGraphParser.cs | 135 + .../MavenStyleDependencyGraphParserService.cs | 15 + .../maven/MvnCliComponentDetector.cs | 151 + .../npm/NpmComponentDetector.cs | 96 + .../npm/NpmComponentDetectorWithRoots.cs | 339 + .../npm/NpmComponentUtilities.cs | 172 + .../nuget/NuGetComponentDetector.cs | 215 + .../nuget/NuGetNuspecUtilities.cs | 64 + ...ectModelProjectCentricComponentDetector.cs | 448 + .../pip/IPyPiClient.cs | 222 + .../pip/IPythonCommandService.cs | 13 + .../pip/IPythonResolver.cs | 15 + .../pip/PipComponentDetector.cs | 124 + .../pip/PipDependencySpecification.cs | 122 + .../pip/PipGraphNode.cs | 23 + .../pip/PythonCommandService.cs | 146 + .../pip/PythonNotFoundException.cs | 8 + .../pip/PythonProject.cs | 12 + .../pip/PythonProjectRelease.cs | 20 + .../pip/PythonResolver.cs | 221 + .../pip/PythonVersion.cs | 322 + .../pip/PythonVersionComparer.cs | 15 + .../pip/PythonVersionUtilities.cs | 140 + .../pnpm/Package.cs | 24 + .../pnpm/PnpmComponentDetector.cs | 106 + .../pnpm/PnpmParsingUtilities.cs | 81 + .../pnpm/PnpmYaml.cs | 13 + .../pnpm/pnpmVersionSpecification.md | 100 + .../ruby/RubyComponentDetector.cs | 272 + .../rust/CargoDependencyData.cs | 23 + .../rust/Contracts/CargoLock.cs | 15 + .../rust/Contracts/CargoPackage.cs | 45 + .../rust/Contracts/CargoToml.cs | 10 + .../rust/DependencySpecification.cs | 70 + .../rust/InvalidRustTomlFileException.cs | 27 + .../rust/RustCrateDetector.cs | 94 + .../rust/RustCrateUtilities.cs | 440 + .../rust/RustCrateV2Detector.cs | 94 + .../rust/SemVer/Comparator.cs | 255 + .../rust/SemVer/ComparatorSet.cs | 194 + .../rust/SemVer/Desugarer.cs | 244 + .../rust/SemVer/PartialVersion.cs | 115 + .../rust/SemVer/Range.cs | 250 + .../rust/SemVer/cgmanifest.json | 14 + .../yarn/IYarnLockParser.cs | 12 + .../yarn/InvalidYarnLockFileException.cs | 27 + .../yarn/Parsers/IYarnBlockFile.cs | 9 + .../yarn/Parsers/YarnBlock.cs | 22 + .../yarn/Parsers/YarnBlockFile.cs | 247 + .../yarn/Parsers/YarnLockParser.cs | 163 + .../yarn/YarnDependency.cs | 11 + .../yarn/YarnEntry.cs | 44 + .../yarn/YarnLockComponentDetector.cs | 291 + .../yarn/YarnLockFile.cs | 17 + .../yarn/YarnLockFileFactory.cs | 33 + .../yarn/YarnLockVersion.cs | 9 + .../ArgumentHelper.cs | 57 + .../ArgumentSets/BaseArguments.cs | 40 + .../ArgumentSets/BcdeArguments.cs | 51 + .../ArgumentSets/BcdeDevArguments.cs | 12 + .../ArgumentSets/IDetectionArguments.cs | 26 + .../ArgumentSets/IListDetectionArgs.cs | 6 + .../ArgumentSets/IScanArguments.cs | 22 + .../ArgumentSets/ListDetectionArgs.cs | 11 + .../CommandLineArgumentsExporter.cs | 20 + .../DetectorRestrictions.cs | 13 + .../DetectorRunResult.cs | 15 + .../InvalidDetectorCategoriesException.cs | 28 + .../InvalidDetectorFilterException.cs | 28 + .../Exceptions/NoDetectorsFoundException.cs | 29 + .../IArgumentHelper.cs | 9 + ...oft.ComponentDetection.Orchestrator.csproj | 22 + .../Orchestrator.cs | 309 + .../Services/BcdeDevCommandService.cs | 37 + .../Services/BcdeScanCommandService.cs | 59 + .../Services/BcdeScanExecutionService.cs | 87 + .../Services/DetectorListingCommandService.cs | 44 + .../Services/DetectorProcessingResult.cs | 16 + .../Services/DetectorProcessingService.cs | 327 + .../Services/DetectorRegistryService.cs | 159 + .../Services/DetectorRestrictionService.cs | 84 + .../DefaultGraphTranslationService.cs | 214 + .../GraphTranslationServiceMetadata.cs | 14 + .../GraphTranslationUtility.cs | 78 + .../IGraphTranslationService.cs | 10 + .../Services/IArgumentHandlingService.cs | 13 + .../Services/IBcdeScanExecutionService.cs | 11 + .../Services/IDetectorProcessingService.cs | 12 + .../Services/IDetectorRegistryService.cs | 14 + .../Services/IDetectorRestrictionService.cs | 10 + .../Services/ServiceBase.cs | 11 + .../LoaderInteropConstants.cs | 10 + .../Microsoft.ComponentDetection.csproj | 20 + src/Microsoft.ComponentDetection/Program.cs | 52 + stylecop.json | 16 + test/Directory.Build.props | 18 + .../BaseDetectionTelemetryRecordTests.cs | 82 + .../CommandLineInvocationServiceTests.cs | 115 + .../ComponentRecorderTests.cs | 214 + .../ComponentStreamEnumerableTests.cs | 85 + .../ConsoleWritingServiceTests.cs | 34 + .../CreateDirectoryTraversalStructure.ps1 | 77 + .../DependencyGraphTests.cs | 354 + .../DockerServiceTests.cs | 85 + .../FileEnumerationTests.cs | 47 + .../FileWritingServiceTests.cs | 104 + .../LoggerTests.cs | 282 + ...oft.ComponentDetection.Common.Tests.csproj | 11 + .../SafeFileEnumerableTests.cs | 161 + .../TabularStringFormatTests.cs | 81 + .../DetectedComponentTests.cs | 29 + ....ComponentDetection.Contracts.Tests.csproj | 20 + .../PurlGenerationTests.cs | 113 + .../ScanResultSerializationTests.cs | 115 + .../TypedComponentSerializationTests.cs | 180 + .../GoComponentDetectorTests.cs | 321 + .../GoComponentTests.cs | 97 + .../GradleComponentDetectorTests.cs | 228 + .../GradleTestUtilities.cs | 29 + .../IvyDetectorTests.cs | 189 + .../LinuxContainerDetectorTests.cs | 209 + .../LinuxScannerTests.cs | 68 + .../MavenCommandServiceTests.cs | 127 + .../MavenStyleDependencyGraphParserTests.cs | 90 + .../MavenTestUtilities.cs | 131 + ....ComponentDetection.Detectors.Tests.csproj | 38 + .../Mocks/GradlewDependencyOutput.txt | 25 + .../Mocks/MvnCliDependencyOutput.txt | 44 + .../Mocks/TestResources.Designer.cs | 182 + .../Mocks/TestResources.resx | 136 + .../MvnCliDetectorTests.cs | 207 + .../NpmDetectorWithRootsTests.cs | 634 ++ .../NpmTestUtilities.cs | 143 + .../NpmUtilitiesTests.cs | 270 + .../NuGetComponentDetectorTests.cs | 191 + .../NuGetNuspecUtilitiesTests.cs | 100 + ...delProjectCentricComponentDetectorTests.cs | 328 + .../NugetTestUtilities.cs | 170 + .../PipComponentDetectorTests.cs | 321 + .../PipDependencySpecifierTests.cs | 34 + .../PipResolverTests.cs | 241 + .../PnpmDetectorTests.cs | 354 + .../PnpmParsingUtilitiesTest.cs | 117 + .../PodDetectorTest.cs | 597 ++ .../PyPiClientTests.cs | 68 + .../PythonCommandServiceTests.cs | 381 + .../PythonVersionTests.cs | 92 + .../Resources/project_assets_2_2.json | 9344 +++++++++++++++++ .../project_assets_2_2_additional.json | 4294 ++++++++ .../Resources/project_assets_3_1.json | 782 ++ .../RubyDetectorTest.cs | 436 + .../RustCrateDetectorTests.cs | 997 ++ .../RustDependencySpecifierTest.cs | 57 + .../ComponentRecorderTestUtilities.cs | 149 + .../Utilities/EnumerableStringComparer.cs | 20 + .../Utilities/TestUtilityExtensions.cs | 17 + .../YarnBlockFileTests.cs | 295 + .../YarnLockDetectorTests.cs | 953 ++ .../YarnParserTests.cs | 189 + .../YarnTestUtilities.cs | 46 + .../GraphTranslationUtilityTests.cs | 46 + ...mponentDetection.Orchestrator.Tests.csproj | 16 + .../Services/BcdeDevCommandServiceTests.cs | 65 + .../Services/BcdeScanExecutionServiceTests.cs | 696 ++ .../DetectorListingCommandServiceTests.cs | 75 + .../DetectorProcessingServiceTests.cs | 539 + .../DetectorRestrictionServiceTests.cs | 178 + .../TelemetryHelper.cs | 40 + .../SkipTestIfNotWindowsAttribute.cs | 25 + .../Attributes/SkipTestOnWindowsAttribute.cs | 25 + .../DetectorTestUtility.cs | 220 + .../DetectorTestUtilityCreator.cs | 13 + .../EnumerableStringComparer.cs | 23 + .../ExtensionMethods.cs | 17 + ...t.ComponentDetection.TestsUtilities.csproj | 10 + .../ComponentDetectionIntegrationTests.cs | 273 + ...pendencyDetective.VerificationTests.csproj | 28 + 390 files changed, 46693 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/library-scripts/azcli-debian.sh create mode 100644 .gitattributes create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE/new_detector_template.md create mode 100644 .github/dependabot.yml create mode 100644 .github/labeler.yml create mode 100644 .github/release-drafter.yml create mode 100644 .github/workflows/build-release.yml create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .github/workflows/detector-version-bump-reminder.yml create mode 100644 .github/workflows/gen-docs.yml create mode 100644 .github/workflows/labeler.yml create mode 100644 .github/workflows/publish-release-snapshot.yml create mode 100644 .github/workflows/test-linux.yml create mode 100644 .github/workflows/verify-snapshot.yml create mode 100644 .gitignore create mode 100644 .tours/add-a-telemetry-record.tour create mode 100644 .tours/getting-started.tour create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 ComponentDetection.sln create mode 100644 Directory.Build.props create mode 100644 Directory.Build.targets create mode 100644 Directory.Packages.props create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 analyzers.ruleset create mode 100644 docs/creating-a-new-detector.md create mode 100644 docs/detector-arguments.md create mode 100644 docs/enable-default-off.md create mode 100644 docs/feature-overview.md create mode 100644 docs/go-detection.md create mode 100644 docs/gradle-detection.md create mode 100644 docs/images/creating-a-new-detector/component-type.png create mode 100644 docs/images/creating-a-new-detector/component-utilities.png create mode 100644 docs/images/creating-a-new-detector/detector-class.png create mode 100644 docs/images/creating-a-new-detector/go-detector-test-utility.png create mode 100644 docs/images/creating-a-new-detector/multi-root-dependency.png create mode 100644 docs/images/creating-a-new-detector/npm-component.png create mode 100644 docs/images/creating-a-new-detector/ruby-component-detector.png create mode 100644 docs/images/creating-a-new-detector/single-root-dependency.png create mode 100644 docs/images/creating-a-new-detector/test-projects.png create mode 100644 docs/images/creating-a-new-detector/vs-projects.png create mode 100644 docs/linux-scanner.md create mode 100644 docs/maven-detection.md create mode 100644 docs/pip-detection.md create mode 100644 docs/renewing-secrets.md create mode 100644 global.json create mode 100644 src/Directory.Build.props create mode 100644 src/Microsoft.ComponentDetection.Common/AsyncExecution.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Column.cs create mode 100644 src/Microsoft.ComponentDetection.Common/CommandLineInvocationService.cs create mode 100644 src/Microsoft.ComponentDetection.Common/ComponentComparer.cs create mode 100644 src/Microsoft.ComponentDetection.Common/ComponentStream.cs create mode 100644 src/Microsoft.ComponentDetection.Common/ComponentStreamEnumerable.cs create mode 100644 src/Microsoft.ComponentDetection.Common/ComponentStreamEnumerableFactory.cs create mode 100644 src/Microsoft.ComponentDetection.Common/ConsoleWritingService.cs create mode 100644 src/Microsoft.ComponentDetection.Common/DependencyGraph/ComponentRecorder.cs create mode 100644 src/Microsoft.ComponentDetection.Common/DependencyGraph/DependencyGraph.cs create mode 100644 src/Microsoft.ComponentDetection.Common/DetectorDependencies.cs create mode 100644 src/Microsoft.ComponentDetection.Common/DirectoryItemFacade.cs create mode 100644 src/Microsoft.ComponentDetection.Common/DockerService.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Exceptions/InvalidUserInputException.cs create mode 100644 src/Microsoft.ComponentDetection.Common/FastDirectoryWalkerFactory.cs create mode 100644 src/Microsoft.ComponentDetection.Common/FileUtilityService.cs create mode 100644 src/Microsoft.ComponentDetection.Common/FileWritingService.cs create mode 100644 src/Microsoft.ComponentDetection.Common/IConsoleWritingService.cs create mode 100644 src/Microsoft.ComponentDetection.Common/IFileWritingService.cs create mode 100644 src/Microsoft.ComponentDetection.Common/ISafeFileEnumerableFactory.cs create mode 100644 src/Microsoft.ComponentDetection.Common/LazyComponentStream.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Logger.cs create mode 100644 src/Microsoft.ComponentDetection.Common/MatchedFile.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Microsoft.ComponentDetection.Common.csproj create mode 100644 src/Microsoft.ComponentDetection.Common/PathUtilityService.cs create mode 100644 src/Microsoft.ComponentDetection.Common/PatternMatchingUtility.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Resources.Designer.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Resources.resx create mode 100644 src/Microsoft.ComponentDetection.Common/SafeFileEnumerable.cs create mode 100644 src/Microsoft.ComponentDetection.Common/SafeFileEnumerableFactory.cs create mode 100644 src/Microsoft.ComponentDetection.Common/ScanType.cs create mode 100644 src/Microsoft.ComponentDetection.Common/TabularStringFormat.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Attributes/MetricAttribute.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Attributes/TelemetryServiceAttribute.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/CommandLineTelemetryService.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/ITelemetryService.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/BaseDetectionTelemetryRecord.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/BcdeExecutionTelemetryRecord.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/CommandLineInvocationTelemetryRecord.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/DetectorExecutionTelemetryRecord.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/DockerServiceImageExistsLocallyTelemetryRecord.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/DockerServiceInspectImageTelemetryRecord.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/DockerServiceSystemInfoTelemetryRecord.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/DockerServiceTelemetryRecord.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/DockerServiceTryPullImageTelemetryRecord.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/FailedReadingFileRecord.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/GoGraphTelemetryRecord.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/IDetectionTelemetryRecord.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxContainerDetectorImageDetectionFailed.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxContainerDetectorLayerAwareness.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxContainerDetectorMissingRepoNameAndTagRecord.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxContainerDetectorMissingVersion.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxContainerDetectorTimeout.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxContainerDetectorUnsupportedOs.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxScannerSyftTelemetryRecord.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxScannerTelemetryRecord.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/LoadComponentDetectorsTelemetryRecord.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/NuGetProjectAssetsTelemetryRecord.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/PypiFailureTelemetryRecord.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/PypiMaxRetriesReachedTelemetryRecord.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/PypiRetryTelemetryRecord.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/Records/RustCrateV2DetectorTelemetryRecord.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/TelemetryConstants.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/TelemetryMode.cs create mode 100644 src/Microsoft.ComponentDetection.Common/Telemetry/TelemetryRelay.cs create mode 100644 src/Microsoft.ComponentDetection.Common/VerbosityMode.cs create mode 100644 src/Microsoft.ComponentDetection.Common/WarnOnAlertSeverity.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/BcdeModels/ContainerDetails.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/BcdeModels/DefaultGraphScanResult.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/BcdeModels/DependencyGraph.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/BcdeModels/DependencyGraphCollection.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/BcdeModels/DependencyGraphWithMetadata.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/BcdeModels/Detector.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/BcdeModels/DockerLayer.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/BcdeModels/LayerMappedLinuxComponents.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/BcdeModels/ScanResult.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/BcdeModels/ScannedComponent.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/BcdeModels/TypedComponentConverter.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/DetectedComponent.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/DetectorClass.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/FileComponentDetector.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/ICommandLineInvocationService.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/IComponentDetector.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/IComponentRecorder.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/IComponentStream.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/IComponentStreamEnumerableFactory.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/IDetectorDependencies.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/IDockerService.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/IFileUtilityService.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/ILogger.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/IObservableDirectoryWalkerFactory.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/IPathUtilityService.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/IndividualDetectorScanResult.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/Internal/InjectionParameters.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/Internal/ProcessRequest.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/KillSwitchConfiguration.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/Microsoft.ComponentDetection.Contracts.csproj create mode 100644 src/Microsoft.ComponentDetection.Contracts/ProcessingResultCode.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/ScanRequest.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/TypedComponent/CargoComponent.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/TypedComponent/ComponentType.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/TypedComponent/CondaComponent.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/TypedComponent/DockerImageComponent.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/TypedComponent/GitComponent.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/TypedComponent/GoComponent.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/TypedComponent/LinuxComponent.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/TypedComponent/MavenComponent.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/TypedComponent/NpmComponent.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/TypedComponent/NugetComponent.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/TypedComponent/OtherComponent.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/TypedComponent/PipComponent.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/TypedComponent/PodComponent.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/TypedComponent/RubyGemsComponent.cs create mode 100644 src/Microsoft.ComponentDetection.Contracts/TypedComponent/TypedComponent.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/IComponentGovernanceOwnedDetectors.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/Microsoft.ComponentDetection.Detectors.csproj create mode 100644 src/Microsoft.ComponentDetection.Detectors/cocoapods/PodComponentDetector.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/go/GoComponentDetector.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/gradle/GradleComponentDetector.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/ivy/IvyDetector.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/ivy/Resources/build.xml create mode 100644 src/Microsoft.ComponentDetection.Detectors/ivy/Resources/java-src/IvyComponentDetectionAntTask.java create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Classification.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/ConfigurationUnion.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Descriptor.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Digest.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/DigestElement.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Distribution.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/File.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/FileClassifications.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/FileContents.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/FileMetadata.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/FileMetadataEntry.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/FileRecord.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/ImageScanningResult.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/JavaManifest.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Location.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Metadata.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Package.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/PomProperties.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Relationship.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Schema.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/SearchResult.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Secrets.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Size.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Source.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Contracts/SyftOutput.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/Exceptions/MissingContainerDetailException.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/ILinuxScanner.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/LinuxContainerDetector.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/linux/LinuxScanner.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/maven/GraphNode.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/maven/IMavenCommandService.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/maven/IMavenStyleDependencyGraphParserService.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/maven/MavenCommandService.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/maven/MavenParsingUtilities.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/maven/MavenStyleDependencyGraphParser.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/maven/MavenStyleDependencyGraphParserService.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/maven/MvnCliComponentDetector.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/npm/NpmComponentDetector.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/npm/NpmComponentDetectorWithRoots.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/npm/NpmComponentUtilities.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/nuget/NuGetComponentDetector.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/nuget/NuGetNuspecUtilities.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/nuget/NuGetProjectModelProjectCentricComponentDetector.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/pip/IPyPiClient.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/pip/IPythonCommandService.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/pip/IPythonResolver.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/pip/PipComponentDetector.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/pip/PipDependencySpecification.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/pip/PipGraphNode.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/pip/PythonCommandService.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/pip/PythonNotFoundException.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/pip/PythonProject.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/pip/PythonProjectRelease.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/pip/PythonResolver.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/pip/PythonVersion.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/pip/PythonVersionComparer.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/pip/PythonVersionUtilities.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/pnpm/Package.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetector.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmParsingUtilities.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmYaml.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/pnpm/pnpmVersionSpecification.md create mode 100644 src/Microsoft.ComponentDetection.Detectors/ruby/RubyComponentDetector.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/rust/CargoDependencyData.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/rust/Contracts/CargoLock.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/rust/Contracts/CargoPackage.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/rust/Contracts/CargoToml.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/rust/DependencySpecification.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/rust/InvalidRustTomlFileException.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/rust/RustCrateDetector.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/rust/RustCrateUtilities.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/rust/RustCrateV2Detector.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/rust/SemVer/Comparator.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/rust/SemVer/ComparatorSet.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/rust/SemVer/Desugarer.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/rust/SemVer/PartialVersion.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/rust/SemVer/Range.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/rust/SemVer/cgmanifest.json create mode 100644 src/Microsoft.ComponentDetection.Detectors/yarn/IYarnLockParser.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/yarn/InvalidYarnLockFileException.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/yarn/Parsers/IYarnBlockFile.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/yarn/Parsers/YarnBlock.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/yarn/Parsers/YarnBlockFile.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/yarn/Parsers/YarnLockParser.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/yarn/YarnDependency.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/yarn/YarnEntry.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/yarn/YarnLockComponentDetector.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/yarn/YarnLockFile.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/yarn/YarnLockFileFactory.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/yarn/YarnLockVersion.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/ArgumentHelper.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/BaseArguments.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/BcdeArguments.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/BcdeDevArguments.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/IDetectionArguments.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/IListDetectionArgs.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/IScanArguments.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/ListDetectionArgs.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/CommandLineArgumentsExporter.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/DetectorRestrictions.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/DetectorRunResult.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/Exceptions/InvalidDetectorCategoriesException.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/Exceptions/InvalidDetectorFilterException.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/Exceptions/NoDetectorsFoundException.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/IArgumentHelper.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/Microsoft.ComponentDetection.Orchestrator.csproj create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/Orchestrator.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/Services/BcdeDevCommandService.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/Services/BcdeScanCommandService.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/Services/BcdeScanExecutionService.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorListingCommandService.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorProcessingResult.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorProcessingService.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorRegistryService.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorRestrictionService.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/Services/GraphTranslation/DefaultGraphTranslationService.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/Services/GraphTranslation/GraphTranslationServiceMetadata.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/Services/GraphTranslation/GraphTranslationUtility.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/Services/GraphTranslation/IGraphTranslationService.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/Services/IArgumentHandlingService.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/Services/IBcdeScanExecutionService.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/Services/IDetectorProcessingService.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/Services/IDetectorRegistryService.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/Services/IDetectorRestrictionService.cs create mode 100644 src/Microsoft.ComponentDetection.Orchestrator/Services/ServiceBase.cs create mode 100644 src/Microsoft.ComponentDetection/LoaderInteropConstants.cs create mode 100644 src/Microsoft.ComponentDetection/Microsoft.ComponentDetection.csproj create mode 100644 src/Microsoft.ComponentDetection/Program.cs create mode 100644 stylecop.json create mode 100644 test/Directory.Build.props create mode 100644 test/Microsoft.ComponentDetection.Common.Tests/BaseDetectionTelemetryRecordTests.cs create mode 100644 test/Microsoft.ComponentDetection.Common.Tests/CommandLineInvocationServiceTests.cs create mode 100644 test/Microsoft.ComponentDetection.Common.Tests/ComponentRecorderTests.cs create mode 100644 test/Microsoft.ComponentDetection.Common.Tests/ComponentStreamEnumerableTests.cs create mode 100644 test/Microsoft.ComponentDetection.Common.Tests/ConsoleWritingServiceTests.cs create mode 100644 test/Microsoft.ComponentDetection.Common.Tests/CreateDirectoryTraversalStructure.ps1 create mode 100644 test/Microsoft.ComponentDetection.Common.Tests/DependencyGraphTests.cs create mode 100644 test/Microsoft.ComponentDetection.Common.Tests/DockerServiceTests.cs create mode 100644 test/Microsoft.ComponentDetection.Common.Tests/FileEnumerationTests.cs create mode 100644 test/Microsoft.ComponentDetection.Common.Tests/FileWritingServiceTests.cs create mode 100644 test/Microsoft.ComponentDetection.Common.Tests/LoggerTests.cs create mode 100644 test/Microsoft.ComponentDetection.Common.Tests/Microsoft.ComponentDetection.Common.Tests.csproj create mode 100644 test/Microsoft.ComponentDetection.Common.Tests/SafeFileEnumerableTests.cs create mode 100644 test/Microsoft.ComponentDetection.Common.Tests/TabularStringFormatTests.cs create mode 100644 test/Microsoft.ComponentDetection.Contracts.Tests/DetectedComponentTests.cs create mode 100644 test/Microsoft.ComponentDetection.Contracts.Tests/Microsoft.ComponentDetection.Contracts.Tests.csproj create mode 100644 test/Microsoft.ComponentDetection.Contracts.Tests/PurlGenerationTests.cs create mode 100644 test/Microsoft.ComponentDetection.Contracts.Tests/ScanResultSerializationTests.cs create mode 100644 test/Microsoft.ComponentDetection.Contracts.Tests/TypedComponentSerializationTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/GoComponentDetectorTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/GoComponentTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/GradleComponentDetectorTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/GradleTestUtilities.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/IvyDetectorTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/LinuxContainerDetectorTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/LinuxScannerTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/MavenCommandServiceTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/MavenStyleDependencyGraphParserTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/MavenTestUtilities.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/Microsoft.ComponentDetection.Detectors.Tests.csproj create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/GradlewDependencyOutput.txt create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/MvnCliDependencyOutput.txt create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/TestResources.Designer.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/TestResources.resx create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/MvnCliDetectorTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/NpmDetectorWithRootsTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/NpmTestUtilities.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/NpmUtilitiesTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/NuGetComponentDetectorTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/NuGetNuspecUtilitiesTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/NuGetProjectModelProjectCentricComponentDetectorTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/NugetTestUtilities.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/PipComponentDetectorTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/PipDependencySpecifierTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/PipResolverTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/PnpmParsingUtilitiesTest.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/PodDetectorTest.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/PyPiClientTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/PythonCommandServiceTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/PythonVersionTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/Resources/project_assets_2_2.json create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/Resources/project_assets_2_2_additional.json create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/Resources/project_assets_3_1.json create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/RubyDetectorTest.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/RustCrateDetectorTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/RustDependencySpecifierTest.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/Utilities/ComponentRecorderTestUtilities.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/Utilities/EnumerableStringComparer.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/Utilities/TestUtilityExtensions.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/YarnBlockFileTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/YarnLockDetectorTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/YarnParserTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/YarnTestUtilities.cs create mode 100644 test/Microsoft.ComponentDetection.Orchestrator.Tests/GraphTranslationUtilityTests.cs create mode 100644 test/Microsoft.ComponentDetection.Orchestrator.Tests/Microsoft.ComponentDetection.Orchestrator.Tests.csproj create mode 100644 test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/BcdeDevCommandServiceTests.cs create mode 100644 test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/BcdeScanExecutionServiceTests.cs create mode 100644 test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/DetectorListingCommandServiceTests.cs create mode 100644 test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/DetectorProcessingServiceTests.cs create mode 100644 test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/DetectorRestrictionServiceTests.cs create mode 100644 test/Microsoft.ComponentDetection.Orchestrator.Tests/TelemetryHelper.cs create mode 100644 test/Microsoft.ComponentDetection.TestsUtilities/Attributes/SkipTestIfNotWindowsAttribute.cs create mode 100644 test/Microsoft.ComponentDetection.TestsUtilities/Attributes/SkipTestOnWindowsAttribute.cs create mode 100644 test/Microsoft.ComponentDetection.TestsUtilities/DetectorTestUtility.cs create mode 100644 test/Microsoft.ComponentDetection.TestsUtilities/DetectorTestUtilityCreator.cs create mode 100644 test/Microsoft.ComponentDetection.TestsUtilities/EnumerableStringComparer.cs create mode 100644 test/Microsoft.ComponentDetection.TestsUtilities/ExtensionMethods.cs create mode 100644 test/Microsoft.ComponentDetection.TestsUtilities/Microsoft.ComponentDetection.TestsUtilities.csproj create mode 100644 test/Microsoft.ComponentDetection.VerificationTests/ComponentDetectionIntegrationTests.cs create mode 100644 test/Microsoft.ComponentDetection.VerificationTests/Microsoft.DependencyDetective.VerificationTests.csproj diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..c998ac67c --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,23 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.202.1/containers/dotnet/.devcontainer/base.Dockerfile + +# [Choice] .NET version: 6.0, 5.0, 3.1, 2.1 +ARG VARIANT="6.0" +#FROM mcr.microsoft.com/vscode/devcontainers/dotnet:0-${VARIANT} +FROM mcr.microsoft.com/vscode/devcontainers/dotnet:dev-6.0 + +# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 +ARG NODE_VERSION="none" +RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi + +# [Option] Install Azure CLI +ARG INSTALL_AZURE_CLI="false" +COPY library-scripts/*.sh library-scripts/*.env /tmp/library-scripts/ +RUN if [ "$INSTALL_AZURE_CLI" = "true" ]; then bash /tmp/library-scripts/azcli-debian.sh; fi \ + && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..bb7f2ca8d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,65 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.202.1/containers/dotnet +{ + "name": "C# (.NET)", + "runArgs": ["--init"], + "build": { + "dockerfile": "Dockerfile", + "args": { + // Update 'VARIANT' to pick a .NET Core version: 2.1, 3.1, 5.0, 6.0 + "VARIANT": "6.0", + // Options + "NODE_VERSION": "lts/*", + "INSTALL_AZURE_CLI": "false" + } + }, + + // Set *default* container specific settings.json values on container create. + "settings": {}, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-dotnettools.csharp", + "vsls-contrib.codetour" + ], + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [5000, 5001], + + // [Optional] To reuse of your local HTTPS dev cert: + // + // 1. Export it locally using this command: + // * Windows PowerShell: + // dotnet dev-certs https --trust; dotnet dev-certs https -ep "$env:USERPROFILE/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere" + // * macOS/Linux terminal: + // dotnet dev-certs https --trust; dotnet dev-certs https -ep "${HOME}/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere" + // + // 2. Uncomment these 'remoteEnv' lines: + // "remoteEnv": { + // "ASPNETCORE_Kestrel__Certificates__Default__Password": "SecurePwdGoesHere", + // "ASPNETCORE_Kestrel__Certificates__Default__Path": "/home/vscode/.aspnet/https/aspnetapp.pfx", + // }, + // + // 3. Do one of the following depending on your scenario: + // * When using GitHub Codespaces and/or Remote - Containers: + // 1. Start the container + // 2. Drag ~/.aspnet/https/aspnetapp.pfx into the root of the file explorer + // 3. Open a terminal in VS Code and run "mkdir -p /home/vscode/.aspnet/https && mv aspnetapp.pfx /home/vscode/.aspnet/https" + // + // * If only using Remote - Containers with a local container, uncomment this line instead: + // "mounts": [ "source=${env:HOME}${env:USERPROFILE}/.aspnet/https,target=/home/vscode/.aspnet/https,type=bind" ], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "dotnet restore", + + // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + // "remoteUser": "vscode", + "features": { + "docker-from-docker": "latest", // This enables the linux detector + // "git": "latest", + // "github-cli": "latest", + // "python": "latest", + // "golang": "latest", + // "java": "lts" + } +} diff --git a/.devcontainer/library-scripts/azcli-debian.sh b/.devcontainer/library-scripts/azcli-debian.sh new file mode 100644 index 000000000..e6b28c1f0 --- /dev/null +++ b/.devcontainer/library-scripts/azcli-debian.sh @@ -0,0 +1,186 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/azcli.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./azcli-debian.sh + +set -e + +AZ_VERSION=${1:-"latest"} +MICROSOFT_GPG_KEYS_URI="https://packages.microsoft.com/keys/microsoft.asc" +AZCLI_ARCHIVE_ARCHITECTURES="amd64" +AZCLI_ARCHIVE_VERSION_CODENAMES="stretch buster bullseye bionic focal" + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Get central common setting +get_common_setting() { + if [ "${common_settings_file_loaded}" != "true" ]; then + curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping." + common_settings_file_loaded=true + fi + if [ -f "/tmp/vsdc-settings.env" ]; then + local multi_line="" + if [ "$2" = "true" ]; then multi_line="-z"; fi + local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')" + if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi + fi + echo "$1=${!1}" +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +export DEBIAN_FRONTEND=noninteractive + +# Soft version matching that resolves a version for a given package in the *current apt-cache* +# Return value is stored in first argument (the unprocessed version) +apt_cache_version_soft_match() { + + # Version + local variable_name="$1" + local requested_version=${!variable_name} + # Package Name + local package_name="$2" + # Exit on no match? + local exit_on_no_match="${3:-true}" + + # Ensure we've exported useful variables + . /etc/os-release + local architecture="$(dpkg --print-architecture)" + + dot_escaped="${requested_version//./\\.}" + dot_plus_escaped="${dot_escaped//+/\\+}" + # Regex needs to handle debian package version number format: https://www.systutorials.com/docs/linux/man/5-deb-version/ + version_regex="^(.+:)?${dot_plus_escaped}([\\.\\+ ~:-]|$)" + set +e # Don't exit if finding version fails - handle gracefully + fuzzy_version="$(apt-cache madison ${package_name} | awk -F"|" '{print $2}' | sed -e 's/^[ \t]*//' | grep -E -m 1 "${version_regex}")" + set -e + if [ -z "${fuzzy_version}" ]; then + echo "(!) No full or partial for package \"${package_name}\" match found in apt-cache for \"${requested_version}\" on OS ${ID} ${VERSION_CODENAME} (${architecture})." + + if $exit_on_no_match; then + echo "Available versions:" + apt-cache madison ${package_name} | awk -F"|" '{print $2}' | grep -oP '^(.+:)?\K.+' + exit 1 # Fail entire script + else + echo "Continuing to fallback method (if available)" + return 1; + fi + fi + + # Globally assign fuzzy_version to this value + # Use this value as the return value of this function + declare -g ${variable_name}="=${fuzzy_version}" + echo "${variable_name} ${!variable_name}" +} + +install_using_apt() { + # Install dependencies + check_packages apt-transport-https curl ca-certificates gnupg2 dirmngr + # Import key safely (new 'signed-by' method rather than deprecated apt-key approach) and install + get_common_setting MICROSOFT_GPG_KEYS_URI + curl -sSL ${MICROSOFT_GPG_KEYS_URI} | gpg --dearmor > /usr/share/keyrings/microsoft-archive-keyring.gpg + echo "deb [arch=${architecture} signed-by=/usr/share/keyrings/microsoft-archive-keyring.gpg] https://packages.microsoft.com/repos/azure-cli/ ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/azure-cli.list + apt-get update + + if [ "${AZ_VERSION}" = "latest" ] || [ "${AZ_VERSION}" = "lts" ] || [ "${AZ_VERSION}" = "stable" ]; then + # Empty, meaning grab the "latest" in the apt repo + AZ_VERSION="" + else + # Sets AZ_VERSION to our desired version, if match found. + apt_cache_version_soft_match AZ_VERSION "azure-cli" false + if [ "$?" != 0 ]; then + return 1 + fi + fi + + if ! (apt-get install -yq azure-cli${AZ_VERSION}); then + rm -f /etc/apt/sources.list.d/azure-cli.list + return 1 + fi +} + +install_using_pip() { + echo "(*) No pre-built binaries available in apt-cache. Installing via pip3." + if ! dpkg -s python3-minimal python3-pip libffi-dev python3-venv > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install python3-minimal python3-pip libffi-dev python3-venv + fi + export PIPX_HOME=/usr/local/pipx + mkdir -p ${PIPX_HOME} + export PIPX_BIN_DIR=/usr/local/bin + export PYTHONUSERBASE=/tmp/pip-tmp + export PIP_CACHE_DIR=/tmp/pip-tmp/cache + pipx_bin=pipx + if ! type pipx > /dev/null 2>&1; then + pip3 install --disable-pip-version-check --no-cache-dir --user pipx + pipx_bin=/tmp/pip-tmp/bin/pipx + fi + + if [ "${AZ_VERSION}" = "latest" ] || [ "${AZ_VERSION}" = "lts" ] || [ "${AZ_VERSION}" = "stable" ]; then + # Empty, meaning grab the "latest" in the apt repo + ver="" + else + ver="==${AZ_VERSION}" + fi + + set +e + ${pipx_bin} install --system-site-packages --pip-args '--no-cache-dir --force-reinstall' -f azure-cli${ver} + + # Fail gracefully + if [ "$?" != 0 ]; then + echo "Could not install azure-cli${ver} via pip" + rm -rf /tmp/pip-tmp + return 1 + fi + set -e +} + +# See if we're on x86_64 and if so, install via apt-get, otherwise use pip3 +echo "(*) Installing Azure CLI..." +. /etc/os-release +architecture="$(dpkg --print-architecture)" +if [[ "${AZCLI_ARCHIVE_ARCHITECTURES}" = *"${architecture}"* ]] && [[ "${AZCLI_ARCHIVE_VERSION_CODENAMES}" = *"${VERSION_CODENAME}"* ]]; then + install_using_apt || use_pip="true" +else + use_pip="true" +fi + +if [ "${use_pip}" = "true" ]; then + install_using_pip + + if [ "$?" != 0 ]; then + echo "Please provide a valid version for your distribution ${ID} ${VERSION_CODENAME} (${architecture})." + echo + echo "Valid versions in current apt-cache" + apt-cache madison azure-cli | awk -F"|" '{print $2}' | grep -oP '^(.+:)?\K.+' + exit 1 + fi +fi + +echo "Done!" \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..6e47c0ee8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,69 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto +*.sh text eol=lf +package-lock.json text eol=lf +package.json text eol=lf +resources.resjson text eol=lf +task.loc.json text eol=lf +task.json text eol=lf + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..ca8f9b9ce --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,7 @@ +# Relevant documentation for CODEOWNERS file: https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners + +# All files in this repo are owned by ose-component-detection-maintainers team. + +# Reviewers are then assigned round-robin style: https://docs.github.com/en/github/setting-up-and-managing-organizations-and-teams/managing-code-review-assignment-for-your-team + +* @microsoft/ose-component-detection-maintainers diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..6973eb75d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,39 @@ +name: Bug report +about: Create a report to help us improve +title: "[Bug]: " +labels: ['triage', 'bug'] +assignees: '' + +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: input + id: summary + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. + - type: textarea + id: reproduce + attributes: + label: Steps To Reproduce + description: Contents of folder being scanned - minimal data to reproduce the issue is preferred. Alternatively, you can provide a link to a public github repo or gist. You can also attach files to this issue. + placeholder: Tell us what you see! + validations: + required: true + - type: input + id: version + attributes: + label: Which version of the tool was used? + description: The version of the binaries or package, or a git commit hash from the branch if you're using the sources in this repo. + - type: input + id: cli + attributes: + label: Provide the full command line input that you used to invoke the tool. + - type: textarea + id: logs + attributes: + label: Steps To Reproduce + description: 'These files are created by the tool and will provide valuable information: GovCompDisc_Log_{timestamp}.log, ScanManifest_{timestamp}.json, ScanTelemetry_{timestamp}.json You can usually find these in the %TEMP% location, or redirect them with the --Output parameter.' + placeholder: Tell us what you see! diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..674ecf83d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,28 @@ +name: Feature request +about: Suggest an idea for this project +title: '[Feature]: ' +labels: ['triage', 'enhancement'] +assignees: '' + +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this feature request! + - type: input + id: problem + attributes: + label: Is your feature request related to a problem? Please describe. + description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + - type: input + id: summary + attributes: + label: Describe the feature + description: A clear and concise description of what you want to happen. + validations: + required: true + - type: textarea + id: context + attributes: + label: Additional context, alternatives considered, etc. + description: Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE/new_detector_template.md b/.github/PULL_REQUEST_TEMPLATE/new_detector_template.md new file mode 100644 index 000000000..b5e10903f --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/new_detector_template.md @@ -0,0 +1,10 @@ + + + + +#### New Detector Checklist + +- [ ] I have gone through the docs for creating a new detector [here](https://github.com/microsoft/componentdetection-bcde/blob/main/docs/creating-a-new-detector.md) +- [ ] My new detector implements `IDefaultOffComponentDetector` +- [ ] I have created a PR to the [verification repo](https://github.com/microsoft/componentdetection-verification) with components that my new detector can find +- [ ] (If necessary) I have updated the [Feature Overview](https://github.com/microsoft/componentdetection-bcde/blob/main/README.md#feature-overview) table in the README diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..8f9cdd904 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'daily' + # Disable version updates for actions (doesn't impact security updates) + # https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates#open-pull-requests-limit + open-pull-requests-limit: 0 + + - package-ecosystem: 'nuget' + directory: '/' + schedule: + interval: 'daily' + # Disable version updates for nuget (doesn't impact security updates) + # https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates#open-pull-requests-limit + open-pull-requests-limit: 0 diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 000000000..670294732 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,22 @@ +repo-configuration: + - .github/**/* + - /* + - .vscode/**/* + - .devcontainer/**/* + +documentation: + - docs/**/* + - README.md + - SECURITY.md + - CONTRIBUTING.md + - .tours/**/* + +component-detection: + - src/**/* + - test/**/* + +scripts: + - Scripts/**/* + +build: + - Build/**/* diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 000000000..d91914762 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,43 @@ +# release-drafter automatically creates a draft release for you each time you complete a PR in the main branch. +# It uses GitHub labels to categorize changes (See categories) and draft the release. +# release-drafter also generates a version for your release based on GitHub labels. You can add a label of 'major', +# 'minor' or 'patch' to determine which number in the version to increment. +# You may need to add these labels yourself. +# See https://github.com/release-drafter/release-drafter +name-template: 'v$RESOLVED_VERSION' +tag-template: 'v$RESOLVED_VERSION' +change-template: '- $TITLE by @$AUTHOR (#$NUMBER)' +change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +no-changes-template: '- No changes' +categories: + - title: '📚 Documentation' + labels: + - 'documentation' + - title: '🚀 New Features' + labels: + - 'enhancement' + - title: '🐛 Bug Fixes' + labels: + - 'bug' + - title: '🧰 Maintenance' + labels: + - 'repo-configuration' + - 'scripts' + - 'build' +version-resolver: + major: + labels: + - 'version:major' + minor: + labels: + - 'version:minor' + patch: + labels: + - 'version:patch' + default: patch +template: | + ## ⚙️ Changes + $CHANGES + + ## 👨🏼‍💻 Contributors + $CONTRIBUTORS diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 000000000..af6b61f25 --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,81 @@ +name: Component Detection CI + +on: + push: + branches: + - main + +jobs: + build: + + runs-on: ubuntu-latest + + env: + OFFICIAL_BUILD: 'True' + # Set the build number in MinVer. + MINVERBUILDMETADATA: build.${{github.run_number}} + + steps: + - uses: actions/checkout@v2.3.2 + with: + fetch-depth: 0 + + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.x + include-prerelease: true + + - name: dotnet restore + run: dotnet restore + + - name: Build and publish + run: dotnet publish -o dist --no-self-contained -c Release ./src/Microsoft.ComponentDetection + + - name: Create temporary folder for release + run: mkdir ./dist-release + + - name: Tar the output + run: tar -C ./dist -cvf ./dist-release/component-detection.tar.gz . + + - name: Create a release draft + id: create_release_draft + uses: release-drafter/release-drafter@v5 + with: + disable-autolabeler: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload Extension Release Asset + id: upload-component-detection-release-asset + uses: actions/upload-release-asset@v1.0.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release_draft.outputs.upload_url }} + asset_path: ./dist-release/component-detection.tar.gz + asset_name: component-detection.tar.gz + asset_content_type: application/zip + + - name: Add NuGet publication source for Github packages + run: dotnet nuget add source https://nuget.pkg.github.com/microsoft/index.json --password $GITHUB_TOKEN --username notused --store-password-in-clear-text --name cgwriter + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Add NuGet publication source for Azure Artifacts Packaging feed + run: dotnet nuget add source https://1essharedassets.pkgs.visualstudio.com/1esPkgs/_packaging/ComponentDetection/nuget/v3/index.json --password $AZART_TOKEN --username az --store-password-in-clear-text --name Packaging + env: + AZART_TOKEN: ${{ secrets.AZART_TOKEN }} + + - name: Generate NuGet packages + run: dotnet pack -o dist-nuget -c Release + + # dotnet nuget push seems to have some probs, use curl for GH + - name: Publish nuget + run: | + for f in ./dist-nuget/*.nupkg + do + curl -vX PUT -u "[user]:${{ secrets.GITHUB_TOKEN }}" -F package=@$f https://nuget.pkg.github.com/microsoft/ + dotnet nuget push --source "Packaging" --api-key az $f + done + shell: bash diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..2e0adf852 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,33 @@ +name: Component Detection PR + +on: [pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + + env: + OFFICIAL_BUILD: 'True' + # Set the build number in MinVer. + MINVERBUILDMETADATA: build.${{github.run_number}} + + steps: + - uses: actions/checkout@v2.3.2 + with: + fetch-depth: 0 + + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.x + include-prerelease: true + + - name: dotnet restore + run: dotnet restore + + - name: Build and publish + run: dotnet publish -o dist --no-self-contained -c Release ./src/Microsoft.ComponentDetection + + - name: Run tests + run: dotnet test diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 000000000..0ec31c4aa --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,86 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ main ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ main ] + schedule: + - cron: '27 10 * * 1' + +jobs: + analyze: + name: Analyze + env: + OFFICIAL_BUILD: 'True' + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'csharp' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - uses: actions/checkout@v2.3.2 + with: + fetch-depth: 0 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) +# - name: Autobuild +# uses: github/codeql-action/autobuild@v1 + + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.x + include-prerelease: true + + - name: dotnet restore + run: dotnet restore + + - name: Build and publish + run: dotnet publish -o dist --no-self-contained -c Release ./src/Microsoft.ComponentDetection + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/detector-version-bump-reminder.yml b/.github/workflows/detector-version-bump-reminder.yml new file mode 100644 index 000000000..760a42f60 --- /dev/null +++ b/.github/workflows/detector-version-bump-reminder.yml @@ -0,0 +1,21 @@ +name: "Detector version bump reminder" +on: + push: + paths: + - 'src/Microsoft.ComponentDetection.Detectors/**' + +jobs: + comment: + runs-on: ubuntu-latest + steps: + - uses: mshick/add-pr-comment@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + message: | + 👋 Hi! It looks like you modified some files in the `Detectors` folder. + You may need to bump the detector versions if any of the following scenarios apply: + * The detector detects more or fewer components than before + * The detector generates different parent/child graph relationships than before + * The detector generates different `devDependencies` values than before + + If none of the above scenarios apply, feel free to ignore this comment 🙂 diff --git a/.github/workflows/gen-docs.yml b/.github/workflows/gen-docs.yml new file mode 100644 index 000000000..82d88a66e --- /dev/null +++ b/.github/workflows/gen-docs.yml @@ -0,0 +1,47 @@ +name: 'Generate docs' + +on: + push: + branches: + - main + paths: + - 'src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/*.cs' + +jobs: + gen-docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2.3.2 + with: + fetch-depth: 0 + + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.x + include-prerelease: true + + - name: Generate docs + run: | + touch version.json + touch version_dev.json + + # Run CLI + dotnet run -p src/Microsoft.ComponentDetection help scan 2> help.txt || true + cat < docs/detector-arguments.md + # Detector arguments + + \`\`\`shell + dotnet run -p './src/Microsoft.ComponentDetection' help scan + \`\`\` + + \`\`\` + $(tail --lines=+4 help.txt) + \`\`\` + EOF + + - name: Commit + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: 'Update docs' + file_pattern: '*.md' \ No newline at end of file diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 000000000..7e02c83cf --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,10 @@ +name: "Pull Request Labeler" +on: [pull_request] + +jobs: + triage: + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v2 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" \ No newline at end of file diff --git a/.github/workflows/publish-release-snapshot.yml b/.github/workflows/publish-release-snapshot.yml new file mode 100644 index 000000000..b898d6613 --- /dev/null +++ b/.github/workflows/publish-release-snapshot.yml @@ -0,0 +1,60 @@ +name: Publish snapshot of test scan + +on: + push: + branches: + - main + +jobs: + + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - uses: actions/checkout@v2.3.2 + with: + fetch-depth: 0 + + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.x + include-prerelease: true + + - name: dotnet restore + run: dotnet restore + +# Need fetch depth : 0 because https://github.com/dotnet/Nerdbank.GitVersioning/issues/174#issuecomment-384961489 + - name: Clone verification repo + uses: actions/checkout@v2.3.2 + with: + repository: microsoft/componentdetection-verification + path: componentdetection-verification + token: ${{ secrets.GH_Private_Repo_Pat }} + fetch-depth: 0 + + - name: Install Apache Ivy on Windows to support Ivy detector + if: ${{ matrix.os == 'windows-latest' }} + run: Choco-Install -PackageName ivy + shell: powershell + + - name: Install Apache Ivy on Ubuntu to support Ivy detector + if: ${{ matrix.os == 'ubuntu-latest' }} + run: curl https://archive.apache.org/dist/ant/ivy/2.5.0/apache-ivy-2.5.0-bin.tar.gz | tar xOz apache-ivy-2.5.0/ivy-2.5.0.jar > /usr/share/ant/lib/ivy.jar + + - name: Make output directory + run: mkdir output + + - name: Scan verification repo + working-directory: src/Microsoft.ComponentDetection/ + run: dotnet run scan --Verbosity Verbose --SourceDirectory ../../componentdetection-verification --Output ../../output + + - name: Upload output folder + uses: actions/upload-artifact@v2 + with: + name: release-snapshot-output-${{ matrix.os }} + path: output diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml new file mode 100644 index 000000000..9d74bc0b6 --- /dev/null +++ b/.github/workflows/test-linux.yml @@ -0,0 +1,48 @@ +name: Test Linux Container Scanning + +on: [pull_request] + +jobs: + test-linux: + + runs-on: ubuntu-latest + + env: + OFFICIAL_BUILD: 'True' + + steps: + - uses: actions/checkout@v2.3.2 + with: + fetch-depth: 0 + + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.x + include-prerelease: true + + - name: dotnet restore + run: dotnet restore + + - name: Build and publish + run: dotnet publish -o dist --no-self-contained -c Release ./src/Microsoft.ComponentDetection + + - name: Make empty directory + working-directory: src/Microsoft.ComponentDetection/ + run: mkdir empty + + - name: Preload node image to scan + run: docker pull node:slim + + - name: Preload golang image to scan + run: docker pull golang:alpine + + - name: Preload ubuntu image to scan + run: docker pull ubuntu@sha256:2b90cad5ded7946db07a28252618b9c8b7f4f103fc39266bcc795719d1362d40 + + - name: Preload distroless image to scan + run: docker pull gcr.io/distroless/nodejs-debian10 + + - name: Test linux scanner + working-directory: src/Microsoft.ComponentDetection/ + run: dotnet run scan --Verbosity Verbose --SourceDirectory empty --DockerImagesToScan node:slim,golang:alpine,ubuntu@sha256:2b90cad5ded7946db07a28252618b9c8b7f4f103fc39266bcc795719d1362d40,alpine:latest,gcr.io/distroless/nodejs-debian10 diff --git a/.github/workflows/verify-snapshot.yml b/.github/workflows/verify-snapshot.yml new file mode 100644 index 000000000..227883e37 --- /dev/null +++ b/.github/workflows/verify-snapshot.yml @@ -0,0 +1,110 @@ +name: Verify snapshot of test scan + +on: [pull_request] + +jobs: + build: + + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + env: + OFFICIAL_BUILD: 'True' + + steps: + - uses: actions/checkout@v2.3.2 + with: + fetch-depth: 0 + + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.x + include-prerelease: true + + - name: dotnet restore + run: dotnet restore + + - name: Make release snapshot output directory + run: mkdir ${{ github.workspace }}/release-output + + - name: Get latest release snapshot download url + id: download-latest-release-snapshot + uses: actions/github-script@v3 + with: + result-encoding: string + github-token: ${{ secrets.GH_Private_Repo_Pat }} + script: | + const res = await github.paginate( + github.actions.listArtifactsForRepo.endpoint.merge({ + owner: 'microsoft', + repo: 'componentdetection-bcde', + }) + ); + + return res + .filter( + (artifact) => artifact.name === 'release-snapshot-output-${{ matrix.os }}' + ) + .sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at))[0] + .archive_download_url; + + - name: Download latest release snapshot + working-directory: ${{ github.workspace }}/release-output + run: curl -v -L -u octocat:${{ secrets.GH_Private_Repo_Pat }} -o output.zip "${{ steps.download-latest-release-snapshot.outputs.result }}" + + - name: Unzip latest release snapshot + run: unzip output.zip + working-directory: ${{ github.workspace }}/release-output + + - name: Remove zip + run: rm output.zip + working-directory: ${{ github.workspace }}/release-output + + - name: Verify we have the correct files + run: ls + working-directory: ${{ github.workspace }}/release-output + +# Need fetch depth : 0 because https://github.com/dotnet/Nerdbank.GitVersioning/issues/174#issuecomment-384961489 + - name: Clone verification repo + uses: actions/checkout@v2.3.2 + with: + repository: microsoft/componentdetection-verification + path: componentdetection-verification + token: ${{ secrets.GH_Private_Repo_Pat }} + fetch-depth: 0 + + - name: Bootstrap the verification repo + run: node ./bootstrap.js + working-directory: ./componentdetection-verification + + - name: Install Apache Ivy on Windows to support Ivy detector + if: ${{ matrix.os == 'windows-latest' }} + run: Choco-Install -PackageName ivy + shell: powershell + + - name: Install Apache Ivy on Ubuntu to support Ivy detector + if: ${{ matrix.os == 'ubuntu-latest' }} + run: curl https://archive.apache.org/dist/ant/ivy/2.5.0/apache-ivy-2.5.0-bin.tar.gz | tar xOz apache-ivy-2.5.0/ivy-2.5.0.jar > /usr/share/ant/lib/ivy.jar + + - name: Make output directory + run: mkdir ${{ github.workspace }}/output + + - name: Scan verification repo + working-directory: src/Microsoft.ComponentDetection + run: dotnet run scan --Verbosity Verbose --SourceDirectory ../../componentdetection-verification --Output ${{ github.workspace }}/output + + - name: Build Verification Tests + run: dotnet build test/Microsoft.ComponentDetection.VerificationTests + env: + OFFICIAL_BUILD: 'True' + + - name: Run Verification Tests + run: dotnet test --no-restore test/Microsoft.ComponentDetection.VerificationTests + env: + GITHUB_OLD_ARTIFACTS_DIR: ${{ github.workspace }}/release-output + GITHUB_NEW_ARTIFACTS_DIR: ${{ github.workspace }}/output + ALLOWED_TIME_DRIFT_RATIO: '.75' diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..533090632 --- /dev/null +++ b/.gitignore @@ -0,0 +1,56 @@ +*.swp +*.*~ +project.lock.json +.DS_Store +*.pyc + +# FAKE Build +.paket +.fake +.patcache +LegacyPackage.nuspec + +# Visual Studio Code +**/.vscode/* +!launch.json +!tasks.json +**/.ionide/* + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates +ComponentDetection/src/Microsoft.ComponentDetection.Loader/Properties/launchSettings.json + +# Diff work files +*.orig + +# Node excludes +node_modules/ +dist/ +dist-nuget/ + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +msbuild.log +msbuild.err +msbuild.wrn +*.vsix +TestResults/ +coverage.cobertura.xml + +# Visual Studio +.vs/ +launchSettings.json + +# Dev nupkgs +dev-source diff --git a/.tours/add-a-telemetry-record.tour b/.tours/add-a-telemetry-record.tour new file mode 100644 index 000000000..dca0f2ad5 --- /dev/null +++ b/.tours/add-a-telemetry-record.tour @@ -0,0 +1,59 @@ +{ + "$schema": "https://aka.ms/codetour-schema", + "title": "Add and using a telemetry record", + "steps": [ + { + "directory": "src/Common/Telemetry/Records", + "description": "Create a new file `MyTelemetryRecord.cs` in `./src/Common/Telemetry/Records/`", + "title": "Creating the file" + }, + { + "file": "src/Common/Telemetry/Records/LoadComponentDetectorsTelemetryRecord.cs", + "selection": { + "start": { + "line": 3, + "character": 5 + }, + "end": { + "line": 3, + "character": 11 + } + }, + "description": "Make sure it's a public class.", + "title": "Configuring the class" + }, + { + "file": "src/Common/Telemetry/Records/LoadComponentDetectorsTelemetryRecord.cs", + "selection": { + "start": { + "line": 3, + "character": 55 + }, + "end": { + "line": 3, + "character": 86 + } + }, + "description": "Derive `BaseDetectionTelemetryRecord`", + "title": "Inheritance" + }, + { + "file": "src/Common/Telemetry/Records/LoadComponentDetectorsTelemetryRecord.cs", + "description": "Every telemetry record must have a unique `RecordName`", + "line": 5, + "title": "Record Name" + }, + { + "file": "src/Common/Telemetry/Records/LoadComponentDetectorsTelemetryRecord.cs", + "description": "Add any additional properties you wish your record to track", + "line": 7, + "title": "Setting the properties" + }, + { + "file": "src/Detectors/linux/LinuxScanner.cs", + "description": "Here is an example of how you would use a telemetry record", + "line": 30, + "title": "Using your record" + } + ] +} \ No newline at end of file diff --git a/.tours/getting-started.tour b/.tours/getting-started.tour new file mode 100644 index 000000000..18170df15 --- /dev/null +++ b/.tours/getting-started.tour @@ -0,0 +1,53 @@ +{ + "$schema": "https://aka.ms/codetour-schema", + "title": "Getting Started", + "steps": [ + { + "title": "Introduction", + "description": "ComponentDetection is a command line package scanning tool. It produces a graph-based output of all detected components and supports a variety of open source package ecosystems. Let's take a look at how to run it." + }, + { + "file": ".vscode/launch.json", + "description": "These are some the basic arguments necessary for the tool to run. The most important of which are:\n\n* `scan` - the action we want to take\n* `SourceDirectory` - the root directory where we want the recursive scan to start\n\nFor a full breakdown of the arguments available check out [./docs/detector-arguments.md](./docs/detector-arguments.md)\n\nYou can run ComponentDetection with these default arguments using the shortcut `Ctrl`+`Shift`+`B`\n\n[Build](command:workbench.action.tasks.build)", + "line": 15, + "title": "Command Line Arguments" + }, + { + "title": "Concepts", + "description": "At a high level:\n\n* `ComponentDetection` starts from a `SourceDirectory` and iterates recursively through the files and directories\n* A `Detector` can specify a list of patterns which it's interested in\n* If a file name matches a pattern, the file contents are read and sent to the `Detector`\n* A `Detector` is responsible for producing a graph of `Components` as the output from processing a file\n* Once all files have been read and processed, `ComponentDetection` accumulates all of the graphs into a final output manifest" + }, + { + "directory": "src/Detectors", + "description": "There are many detectors, at least 1 for each package ecosystem. Let's take a look at a simple example.", + "title": "Detectors Folder" + }, + { + "file": "src/Detectors/cocoapods/PodComponentDetector.cs", + "description": "Here is an example of a `Detector` for cocoapods components", + "line": 15, + "title": "Detector Class" + }, + { + "file": "src/Detectors/cocoapods/PodComponentDetector.cs", + "description": "These are the file patterns which the `Detector` is interested in", + "line": 21, + "title": "Search Patterns" + }, + { + "file": "src/Detectors/cocoapods/PodComponentDetector.cs", + "description": "Here is where we register a detected `PodComponent` in the output graph for this file.", + "line": 330, + "title": "Registering Components" + }, + { + "directory": "src/Contracts/TypedComponent", + "description": "There are many component types, one for each package ecosystem. That allows us to keep only the information relavant for each ecosystem.", + "title": "TypedComponents Folder" + }, + { + "title": "Wrap Up", + "description": "Run ComponentDetection (remember: `Ctrl`+`Shift`+`B`) and take a look at the final output in `./scan-output/bcde-output.json`." + } + ], + "isPrimary": true +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..3a4067d9b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/src/Microsoft.ComponentDetection/bin/Debug/net6.0/Microsoft.ComponentDetection.dll", + "args": ["scan", "--Debug", "--Verbosity", "Verbose", "--Output", "${workspaceFolder}/scan-output", "--SourceDirectory", "${workspaceFolder}"], + "cwd": "${workspaceFolder}/src/Microsoft.ComponentDetection", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..c201dd55b --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/src/Microsoft.ComponentDetection/Microsoft.ComponentDetection.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/src/Microsoft.ComponentDetection/Microsoft.ComponentDetection.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "${workspaceFolder}/src/Microsoft.ComponentDetection/Microsoft.ComponentDetection.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..f9ba8cf65 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,9 @@ +# Microsoft Open Source Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). + +Resources: + +- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) +- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..c389289c2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,66 @@ +# Contributing to Component Detection + +This project welcomes contributions and suggestions. Most contributions require you to +agree to a Contributor License Agreement (CLA) declaring that you have the right to, +and actually do, grant us the rights to use your contribution. For details, visit +https://cla.microsoft.com. + +When you submit a pull request, a CLA-bot will automatically determine whether you need +to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the +instructions provided by the bot. You will only need to do this once across all repositories using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +We strive to make this codebase as contributor friendly as possible to encourage teams, which are subject matter experts on the tooling they use every day, to contribute back and improve the experience for everyone. If there is a bug fix or feature you would like to contribute, please follow the guidelines below and let us know how you would like to contribute. + +For bugs, issues, and support please email [OpenSourceEngSupport@microsoft.com](mailto:OpenSourceEngSupport@microsoft.com) + +First, let's get to know the file structure. + +# File structure + +## [.github/workflows](.github/workflows) +All CI / CD is handled through github actions. Most are self-explanatory, but a few interesting ones exist: +* [test-linux.yml](.github/workflows/test-linux.yml) -- This is used specifically to test the linux detector, which must be run on *nix variant systems until tern supports windows in a first class way. +* [verify-snapshot.yml](.github/workflows/verify-snapshot.yml) -- This is essentially an "end to end" test, using the componentdetection-verification repo as a baseline. It looks at scan output captured from [publish-release-snapshot.yml](./github/workflows/publish-release-snapshot.yml) and compares it to the output being created by the changes in the current PR. Because of the end to end nature of the test, this workflow can be a little less stable (e.g. components detected can change for ecosystems that don't have locked deps). + +## [src](src) +All dotnet core code that is used to run component detection. + +### [src/Microsoft.ComponentDetection](src/Microsoft.ComponentDetection) +The entry point for the dotnet application. Handles the --Debug argument and does little else. +### [src/Microsoft.ComponentDetection.Orchestrator](src/Microsoft.ComponentDetection.Orchestrator) +The code that pulls together the arguments ands runs different commands based on input, deciding which services to call and wrapping the detectors so they don't break everything. Generally, the app "glue". +### [src/Microsoft.ComponentDetection.Common](src/Microsoft.ComponentDetection.Common) +Code shared by everything in the app, not including Contracts. +### [src/Microsoft.ComponentDetection.Contracts](src/Microsoft.ComponentDetection.Contracts) +Models used in the solution, used to deserialize / reserialize output from the application. +### [src/Microsoft.ComponentDetection.Detectors](src/Microsoft.ComponentDetection.Detectors) +bulk of the code that actually runs detection -- each built-in detector should have it's own folder (usually by ecosystem). Ecosystem specific utilities live adjacent to the detector implementation itself. [Additional documentation on the linux detector.](./docs/linux-scanner.md) + +# What is a Detector? +The bulk of contributions/work will be in this area. Currently, there are two kinds of detector. The main kind, 'FileDetector', relies on traversing the file directory and finding a specified file to parse and discover what components to register. The set of file names for a detector is specified as a part of the class variables, and the orchestrator runs a file scanning process, sending the results to the appropriate detectors. The other kind of detector, `IComponentDetector`, isn't necessarily file focused (though it can be) but is generally intended to do "one-shot" scanning. An example of this is the LinuxDetector that takes scan arguments from the command line. Since it doesn't have to discover new docker images to scan and it doesn't operate over any files, it simply implements `IComponentDetector`. + +The detector returns the set of all components found by the detector, typically mapped by its locations and in some cases including the dependency chain (one way). + +## Contributing a new detector + +Please see [creating a new detector](./docs/creating-a-new-detector.md). + +### PR Policies +* Branch names should follow `{user}/{title}` convention + * eg: tevoinea/IssueTemplate +* All checks must pass +* At least 1 required review + +### Style + +Analysis rulesets are defined in [analyzers.ruleset](analyzers.ruleset) and validated by our PR builds. + +### Testing + +L0s are defined in `MS.VS.Services.Governance.CD.*.L0.Tests`. + +Verification tests are run on the sample projects defined in [microsoft/componentdetection-verification](https://github.com/microsoft/componentdetection-verification). diff --git a/ComponentDetection.sln b/ComponentDetection.sln new file mode 100644 index 000000000..4f0a5d4cf --- /dev/null +++ b/ComponentDetection.sln @@ -0,0 +1,192 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31808.319 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ComponentDetection.Common", "src\Microsoft.ComponentDetection.Common\Microsoft.ComponentDetection.Common.csproj", "{4DF6C2D6-B231-4A03-BBDE-44D9980B5F0A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ComponentDetection.Contracts", "src\Microsoft.ComponentDetection.Contracts\Microsoft.ComponentDetection.Contracts.csproj", "{15A2FC95-1232-45BF-A732-7B5CE96F7173}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ComponentDetection.Detectors", "src\Microsoft.ComponentDetection.Detectors\Microsoft.ComponentDetection.Detectors.csproj", "{F0C6D4D7-3F14-4DCA-A5FC-050D987247FC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ComponentDetection", "src\Microsoft.ComponentDetection\Microsoft.ComponentDetection.csproj", "{4B3F743B-7729-48D7-B43E-AEC22AD83DE0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ComponentDetection.Orchestrator", "src\Microsoft.ComponentDetection.Orchestrator\Microsoft.ComponentDetection.Orchestrator.csproj", "{4A109A41-C5A9-4C59-B7CE-F168CBADCC83}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ComponentDetection.Common.Tests", "test\Microsoft.ComponentDetection.Common.Tests\Microsoft.ComponentDetection.Common.Tests.csproj", "{6DF72D9F-7FE6-4250-8837-64AB50AA85EA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ComponentDetection.Contracts.Tests", "test\Microsoft.ComponentDetection.Contracts.Tests\Microsoft.ComponentDetection.Contracts.Tests.csproj", "{E1EE8396-BDA6-4BE3-BA79-29889DAFA871}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ComponentDetection.Detectors.Tests", "test\Microsoft.ComponentDetection.Detectors.Tests\Microsoft.ComponentDetection.Detectors.Tests.csproj", "{5CC31737-F845-4056-9DD1-8F8838DD4EBD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ComponentDetection.Orchestrator.Tests", "test\Microsoft.ComponentDetection.Orchestrator.Tests\Microsoft.ComponentDetection.Orchestrator.Tests.csproj", "{411CBBC3-DA4B-4922-B5D9-3FEFC0A7696F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ComponentDetection.TestsUtilities", "test\Microsoft.ComponentDetection.TestsUtilities\Microsoft.ComponentDetection.TestsUtilities.csproj", "{AB674039-1D82-43CF-BDE5-21A14FE1B1F0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{095097AB-BB67-48E4-A4EE-EB2775F02F7F}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets + Directory.Packages.props = Directory.Packages.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F452E8E5-D1AB-4B67-9FFC-5974EA1A8584}" + ProjectSection(SolutionItems) = preProject + src\Directory.Build.props = src\Directory.Build.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{FA0D23F6-DAF3-48AE-80B7-AB44B4B4659A}" + ProjectSection(SolutionItems) = preProject + test\Directory.Build.props = test\Directory.Build.props + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4DF6C2D6-B231-4A03-BBDE-44D9980B5F0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4DF6C2D6-B231-4A03-BBDE-44D9980B5F0A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4DF6C2D6-B231-4A03-BBDE-44D9980B5F0A}.Debug|x64.ActiveCfg = Debug|Any CPU + {4DF6C2D6-B231-4A03-BBDE-44D9980B5F0A}.Debug|x64.Build.0 = Debug|Any CPU + {4DF6C2D6-B231-4A03-BBDE-44D9980B5F0A}.Debug|x86.ActiveCfg = Debug|Any CPU + {4DF6C2D6-B231-4A03-BBDE-44D9980B5F0A}.Debug|x86.Build.0 = Debug|Any CPU + {4DF6C2D6-B231-4A03-BBDE-44D9980B5F0A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4DF6C2D6-B231-4A03-BBDE-44D9980B5F0A}.Release|Any CPU.Build.0 = Release|Any CPU + {4DF6C2D6-B231-4A03-BBDE-44D9980B5F0A}.Release|x64.ActiveCfg = Release|Any CPU + {4DF6C2D6-B231-4A03-BBDE-44D9980B5F0A}.Release|x64.Build.0 = Release|Any CPU + {4DF6C2D6-B231-4A03-BBDE-44D9980B5F0A}.Release|x86.ActiveCfg = Release|Any CPU + {4DF6C2D6-B231-4A03-BBDE-44D9980B5F0A}.Release|x86.Build.0 = Release|Any CPU + {15A2FC95-1232-45BF-A732-7B5CE96F7173}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {15A2FC95-1232-45BF-A732-7B5CE96F7173}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15A2FC95-1232-45BF-A732-7B5CE96F7173}.Debug|x64.ActiveCfg = Debug|Any CPU + {15A2FC95-1232-45BF-A732-7B5CE96F7173}.Debug|x64.Build.0 = Debug|Any CPU + {15A2FC95-1232-45BF-A732-7B5CE96F7173}.Debug|x86.ActiveCfg = Debug|Any CPU + {15A2FC95-1232-45BF-A732-7B5CE96F7173}.Debug|x86.Build.0 = Debug|Any CPU + {15A2FC95-1232-45BF-A732-7B5CE96F7173}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15A2FC95-1232-45BF-A732-7B5CE96F7173}.Release|Any CPU.Build.0 = Release|Any CPU + {15A2FC95-1232-45BF-A732-7B5CE96F7173}.Release|x64.ActiveCfg = Release|Any CPU + {15A2FC95-1232-45BF-A732-7B5CE96F7173}.Release|x64.Build.0 = Release|Any CPU + {15A2FC95-1232-45BF-A732-7B5CE96F7173}.Release|x86.ActiveCfg = Release|Any CPU + {15A2FC95-1232-45BF-A732-7B5CE96F7173}.Release|x86.Build.0 = Release|Any CPU + {F0C6D4D7-3F14-4DCA-A5FC-050D987247FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0C6D4D7-3F14-4DCA-A5FC-050D987247FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0C6D4D7-3F14-4DCA-A5FC-050D987247FC}.Debug|x64.ActiveCfg = Debug|Any CPU + {F0C6D4D7-3F14-4DCA-A5FC-050D987247FC}.Debug|x64.Build.0 = Debug|Any CPU + {F0C6D4D7-3F14-4DCA-A5FC-050D987247FC}.Debug|x86.ActiveCfg = Debug|Any CPU + {F0C6D4D7-3F14-4DCA-A5FC-050D987247FC}.Debug|x86.Build.0 = Debug|Any CPU + {F0C6D4D7-3F14-4DCA-A5FC-050D987247FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0C6D4D7-3F14-4DCA-A5FC-050D987247FC}.Release|Any CPU.Build.0 = Release|Any CPU + {F0C6D4D7-3F14-4DCA-A5FC-050D987247FC}.Release|x64.ActiveCfg = Release|Any CPU + {F0C6D4D7-3F14-4DCA-A5FC-050D987247FC}.Release|x64.Build.0 = Release|Any CPU + {F0C6D4D7-3F14-4DCA-A5FC-050D987247FC}.Release|x86.ActiveCfg = Release|Any CPU + {F0C6D4D7-3F14-4DCA-A5FC-050D987247FC}.Release|x86.Build.0 = Release|Any CPU + {4B3F743B-7729-48D7-B43E-AEC22AD83DE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4B3F743B-7729-48D7-B43E-AEC22AD83DE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4B3F743B-7729-48D7-B43E-AEC22AD83DE0}.Debug|x64.ActiveCfg = Debug|Any CPU + {4B3F743B-7729-48D7-B43E-AEC22AD83DE0}.Debug|x64.Build.0 = Debug|Any CPU + {4B3F743B-7729-48D7-B43E-AEC22AD83DE0}.Debug|x86.ActiveCfg = Debug|Any CPU + {4B3F743B-7729-48D7-B43E-AEC22AD83DE0}.Debug|x86.Build.0 = Debug|Any CPU + {4B3F743B-7729-48D7-B43E-AEC22AD83DE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4B3F743B-7729-48D7-B43E-AEC22AD83DE0}.Release|Any CPU.Build.0 = Release|Any CPU + {4B3F743B-7729-48D7-B43E-AEC22AD83DE0}.Release|x64.ActiveCfg = Release|Any CPU + {4B3F743B-7729-48D7-B43E-AEC22AD83DE0}.Release|x64.Build.0 = Release|Any CPU + {4B3F743B-7729-48D7-B43E-AEC22AD83DE0}.Release|x86.ActiveCfg = Release|Any CPU + {4B3F743B-7729-48D7-B43E-AEC22AD83DE0}.Release|x86.Build.0 = Release|Any CPU + {4A109A41-C5A9-4C59-B7CE-F168CBADCC83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A109A41-C5A9-4C59-B7CE-F168CBADCC83}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A109A41-C5A9-4C59-B7CE-F168CBADCC83}.Debug|x64.ActiveCfg = Debug|Any CPU + {4A109A41-C5A9-4C59-B7CE-F168CBADCC83}.Debug|x64.Build.0 = Debug|Any CPU + {4A109A41-C5A9-4C59-B7CE-F168CBADCC83}.Debug|x86.ActiveCfg = Debug|Any CPU + {4A109A41-C5A9-4C59-B7CE-F168CBADCC83}.Debug|x86.Build.0 = Debug|Any CPU + {4A109A41-C5A9-4C59-B7CE-F168CBADCC83}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A109A41-C5A9-4C59-B7CE-F168CBADCC83}.Release|Any CPU.Build.0 = Release|Any CPU + {4A109A41-C5A9-4C59-B7CE-F168CBADCC83}.Release|x64.ActiveCfg = Release|Any CPU + {4A109A41-C5A9-4C59-B7CE-F168CBADCC83}.Release|x64.Build.0 = Release|Any CPU + {4A109A41-C5A9-4C59-B7CE-F168CBADCC83}.Release|x86.ActiveCfg = Release|Any CPU + {4A109A41-C5A9-4C59-B7CE-F168CBADCC83}.Release|x86.Build.0 = Release|Any CPU + {6DF72D9F-7FE6-4250-8837-64AB50AA85EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6DF72D9F-7FE6-4250-8837-64AB50AA85EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6DF72D9F-7FE6-4250-8837-64AB50AA85EA}.Debug|x64.ActiveCfg = Debug|Any CPU + {6DF72D9F-7FE6-4250-8837-64AB50AA85EA}.Debug|x64.Build.0 = Debug|Any CPU + {6DF72D9F-7FE6-4250-8837-64AB50AA85EA}.Debug|x86.ActiveCfg = Debug|Any CPU + {6DF72D9F-7FE6-4250-8837-64AB50AA85EA}.Debug|x86.Build.0 = Debug|Any CPU + {6DF72D9F-7FE6-4250-8837-64AB50AA85EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6DF72D9F-7FE6-4250-8837-64AB50AA85EA}.Release|Any CPU.Build.0 = Release|Any CPU + {6DF72D9F-7FE6-4250-8837-64AB50AA85EA}.Release|x64.ActiveCfg = Release|Any CPU + {6DF72D9F-7FE6-4250-8837-64AB50AA85EA}.Release|x64.Build.0 = Release|Any CPU + {6DF72D9F-7FE6-4250-8837-64AB50AA85EA}.Release|x86.ActiveCfg = Release|Any CPU + {6DF72D9F-7FE6-4250-8837-64AB50AA85EA}.Release|x86.Build.0 = Release|Any CPU + {E1EE8396-BDA6-4BE3-BA79-29889DAFA871}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1EE8396-BDA6-4BE3-BA79-29889DAFA871}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1EE8396-BDA6-4BE3-BA79-29889DAFA871}.Debug|x64.ActiveCfg = Debug|Any CPU + {E1EE8396-BDA6-4BE3-BA79-29889DAFA871}.Debug|x64.Build.0 = Debug|Any CPU + {E1EE8396-BDA6-4BE3-BA79-29889DAFA871}.Debug|x86.ActiveCfg = Debug|Any CPU + {E1EE8396-BDA6-4BE3-BA79-29889DAFA871}.Debug|x86.Build.0 = Debug|Any CPU + {E1EE8396-BDA6-4BE3-BA79-29889DAFA871}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1EE8396-BDA6-4BE3-BA79-29889DAFA871}.Release|Any CPU.Build.0 = Release|Any CPU + {E1EE8396-BDA6-4BE3-BA79-29889DAFA871}.Release|x64.ActiveCfg = Release|Any CPU + {E1EE8396-BDA6-4BE3-BA79-29889DAFA871}.Release|x64.Build.0 = Release|Any CPU + {E1EE8396-BDA6-4BE3-BA79-29889DAFA871}.Release|x86.ActiveCfg = Release|Any CPU + {E1EE8396-BDA6-4BE3-BA79-29889DAFA871}.Release|x86.Build.0 = Release|Any CPU + {5CC31737-F845-4056-9DD1-8F8838DD4EBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5CC31737-F845-4056-9DD1-8F8838DD4EBD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5CC31737-F845-4056-9DD1-8F8838DD4EBD}.Debug|x64.ActiveCfg = Debug|Any CPU + {5CC31737-F845-4056-9DD1-8F8838DD4EBD}.Debug|x64.Build.0 = Debug|Any CPU + {5CC31737-F845-4056-9DD1-8F8838DD4EBD}.Debug|x86.ActiveCfg = Debug|Any CPU + {5CC31737-F845-4056-9DD1-8F8838DD4EBD}.Debug|x86.Build.0 = Debug|Any CPU + {5CC31737-F845-4056-9DD1-8F8838DD4EBD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5CC31737-F845-4056-9DD1-8F8838DD4EBD}.Release|Any CPU.Build.0 = Release|Any CPU + {5CC31737-F845-4056-9DD1-8F8838DD4EBD}.Release|x64.ActiveCfg = Release|Any CPU + {5CC31737-F845-4056-9DD1-8F8838DD4EBD}.Release|x64.Build.0 = Release|Any CPU + {5CC31737-F845-4056-9DD1-8F8838DD4EBD}.Release|x86.ActiveCfg = Release|Any CPU + {5CC31737-F845-4056-9DD1-8F8838DD4EBD}.Release|x86.Build.0 = Release|Any CPU + {411CBBC3-DA4B-4922-B5D9-3FEFC0A7696F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {411CBBC3-DA4B-4922-B5D9-3FEFC0A7696F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {411CBBC3-DA4B-4922-B5D9-3FEFC0A7696F}.Debug|x64.ActiveCfg = Debug|Any CPU + {411CBBC3-DA4B-4922-B5D9-3FEFC0A7696F}.Debug|x64.Build.0 = Debug|Any CPU + {411CBBC3-DA4B-4922-B5D9-3FEFC0A7696F}.Debug|x86.ActiveCfg = Debug|Any CPU + {411CBBC3-DA4B-4922-B5D9-3FEFC0A7696F}.Debug|x86.Build.0 = Debug|Any CPU + {411CBBC3-DA4B-4922-B5D9-3FEFC0A7696F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {411CBBC3-DA4B-4922-B5D9-3FEFC0A7696F}.Release|Any CPU.Build.0 = Release|Any CPU + {411CBBC3-DA4B-4922-B5D9-3FEFC0A7696F}.Release|x64.ActiveCfg = Release|Any CPU + {411CBBC3-DA4B-4922-B5D9-3FEFC0A7696F}.Release|x64.Build.0 = Release|Any CPU + {411CBBC3-DA4B-4922-B5D9-3FEFC0A7696F}.Release|x86.ActiveCfg = Release|Any CPU + {411CBBC3-DA4B-4922-B5D9-3FEFC0A7696F}.Release|x86.Build.0 = Release|Any CPU + {AB674039-1D82-43CF-BDE5-21A14FE1B1F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB674039-1D82-43CF-BDE5-21A14FE1B1F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB674039-1D82-43CF-BDE5-21A14FE1B1F0}.Debug|x64.ActiveCfg = Debug|Any CPU + {AB674039-1D82-43CF-BDE5-21A14FE1B1F0}.Debug|x64.Build.0 = Debug|Any CPU + {AB674039-1D82-43CF-BDE5-21A14FE1B1F0}.Debug|x86.ActiveCfg = Debug|Any CPU + {AB674039-1D82-43CF-BDE5-21A14FE1B1F0}.Debug|x86.Build.0 = Debug|Any CPU + {AB674039-1D82-43CF-BDE5-21A14FE1B1F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB674039-1D82-43CF-BDE5-21A14FE1B1F0}.Release|Any CPU.Build.0 = Release|Any CPU + {AB674039-1D82-43CF-BDE5-21A14FE1B1F0}.Release|x64.ActiveCfg = Release|Any CPU + {AB674039-1D82-43CF-BDE5-21A14FE1B1F0}.Release|x64.Build.0 = Release|Any CPU + {AB674039-1D82-43CF-BDE5-21A14FE1B1F0}.Release|x86.ActiveCfg = Release|Any CPU + {AB674039-1D82-43CF-BDE5-21A14FE1B1F0}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {4DF6C2D6-B231-4A03-BBDE-44D9980B5F0A} = {F452E8E5-D1AB-4B67-9FFC-5974EA1A8584} + {15A2FC95-1232-45BF-A732-7B5CE96F7173} = {F452E8E5-D1AB-4B67-9FFC-5974EA1A8584} + {F0C6D4D7-3F14-4DCA-A5FC-050D987247FC} = {F452E8E5-D1AB-4B67-9FFC-5974EA1A8584} + {4B3F743B-7729-48D7-B43E-AEC22AD83DE0} = {F452E8E5-D1AB-4B67-9FFC-5974EA1A8584} + {4A109A41-C5A9-4C59-B7CE-F168CBADCC83} = {F452E8E5-D1AB-4B67-9FFC-5974EA1A8584} + {6DF72D9F-7FE6-4250-8837-64AB50AA85EA} = {FA0D23F6-DAF3-48AE-80B7-AB44B4B4659A} + {E1EE8396-BDA6-4BE3-BA79-29889DAFA871} = {FA0D23F6-DAF3-48AE-80B7-AB44B4B4659A} + {5CC31737-F845-4056-9DD1-8F8838DD4EBD} = {FA0D23F6-DAF3-48AE-80B7-AB44B4B4659A} + {411CBBC3-DA4B-4922-B5D9-3FEFC0A7696F} = {FA0D23F6-DAF3-48AE-80B7-AB44B4B4659A} + {AB674039-1D82-43CF-BDE5-21A14FE1B1F0} = {FA0D23F6-DAF3-48AE-80B7-AB44B4B4659A} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {4EA56A5D-4CD8-4D3D-A4E9-6A1A9AADD903} + EndGlobalSection +EndGlobal diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 000000000..841882bd5 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,40 @@ + + + + net6.0 + latest + true + true + + + + Microsoft + Microsoft + Copyright © Microsoft Corporation. All rights reserved. + true + MIT + https://github.com/microsoft/componentdetection-bcde + https://github.com/microsoft/componentdetection-bcde.git + git + https://github.com/microsoft/componentdetection-bcde/releases + + + + + + + + + + $(MSBuildThisFileDirectory)analyzers.ruleset + true + NU1608,NU5119 + + + true + + + + + + \ No newline at end of file diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 000000000..074fd65ab --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,11 @@ + + + + + preview + v + normal + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 000000000..d443ec5b1 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,42 @@ + + + + + + Compile + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..b2f52a2ba --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +Copyright (c) Microsoft Corporation. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 000000000..3c002da09 --- /dev/null +++ b/README.md @@ -0,0 +1,103 @@ + +# Component Detection +![Component Detection CI](https://github.com/microsoft/componentdetection-bcde/workflows/Component%20Detection%20CI/badge.svg) + +**For bugs, issues, and support please create an issue.** + +# Introduction + +ComponentDetection (BCDE) is a package scanning tool intended to be used at build time. BCDE produces a graph-based output of all detected components and supports a variety of open source package ecosystems. + +# Table of Contents + +* [Feature Overview](#Feature-Overview) +* [My favorite language/ecosystem isn't supported!](#My-favorite-language/ecosystem-isn't-supported!) +* [Building and running BCDE](#Building-and-running-BCDE) + * [Running in Visual Studio (2019+)](#Running-in-Visual-Studio-(2019+)) + * [Running from command line](#Running-from-command-line) + * [After building](#After-building) +* [A detector is marked as DefaultOff/Experimental. What does that mean?](#A-detector-is-marked-as-DefaultOff/Experimental.-What-does-that-mean?) +* [Telemetry](#Telemetry) + +# Feature Overview + +| Ecosystem | Scanning | Graph Creation | +| - | - | - | +| CocoaPods | ✔ | ✔ | +| Linux (Debian, Alpine, Rhel, Centos, Fedora, Ubuntu)| ✔ (via [syft](https://github.com/anchore/syft)) | ❌ | +| Gradle (lockfiles only) | ✔ | ❌ | +| Go | ✔ | ❌ | +| Maven | ✔ | ✔ | +| NPM (including Yarn, Pnpm) | ✔ | ✔ | +| NuGet | ✔ | ✔ | +| Pip (Python) | ✔ | ✔ | +| Ruby | ✔ | ✔ | +| Rust | ✔ | ✔ | + +For a complete feature overview refer to [feature-overview.md](docs/feature-overview.md) + +# My favorite language/ecosystem isn't supported! + +BCDE is built with extensibility in mind! Please see our [CONTRIBUTING.md](CONTRIBUTING.md) to get started where you can find additional docs on adding your own detector. + + +# Building and running BCDE +DotNet Core SDK 6.0.0-rc2 is currently in use, you can install it from https://dotnet.microsoft.com/download/dotnet/6.0 +We also use node and npm, you can install them from https://nodejs.org/en/download/ + +The below commands mirror what we do to setup our CI environments: + +From the base folder: +``` dotnet build ``` + +## Running in Visual Studio (2019+) +1. open [ComponentDetection.sln](ComponentDetection.sln) in Visual Studio +1. Set the Loader project as the startup project (rightclick-> Set as Startup Project) +1. Set Run arguments for the Loader project (rightclick->properties->Debug) + *Minimum:* `scan --SourceDirectory ` +1. Now, any time you make a change, you can press `F5`. This will build the changes, and start the process in debug mode (hitting any breakpoints you set) + +## Using Codespaces + +If you have access to [GitHub Codespaces](https://docs.github.com/en/free-pro-team@latest/github/developing-online-with-codespaces/about-codespaces), select the `Code` button from the [repository homepage](https://github.com/microsoft/componentdetection-bcde) then select `Open with Codespaces`. That's it! You have a full developer environment that supports debugging, testing, auto complete, jump to definition, everything you would expect. + +## Using VS Code DevContainer + +This is similar to Codespaces: + +1. Make sure you meet [the requirements](https://code.visualstudio.com/docs/remote/containers#_getting-started) and follow the installation steps for DevContainers in VS Code +1. `git clone https://github.com/microsoft/componentdetection-bcde` +1. Open this repo in VS Code +1. A notification should popup to reopen the workspace in the container. If it doesn't, open the [`Command Palette`](https://code.visualstudio.com/docs/getstarted/tips-and-tricks#_command-palette) and type `Remote-Containers: Reopen in Container`. + +## Running from command line +The most basic run: +``` +dotnet run --project src/Microsoft.ComponentDetection scan --SourceDirectory .\ +``` +You can add `--no-restore` or `--no-build` if you don't want to rebuild before the run + +You can add `--Debug` to get the application to wait for debugger attachment to complete. + +## After building +Additional arguments for detection can be found in [detector arguments](docs/detector-arguments.md) + +# A detector is marked as DefaultOff/Experimental. What does that mean? + +Detectors have 3 levels of "stability": +* `DefaultOff` +* `Experimental` +* `Stable` + +DefaultOff detectors need to be explicitly enabled to run and produce a final graph output. Experimental detectors run by default but **will not** produce a final graph output. Stable detectors run and produce a final graph output by default. Here is how you can [enable default off/experimental](./docs/enable-default-off.md) detectors. + +# Telemetry +By default, telemetry will output to your output file path and will be a JSON blob. No data is submitted to Microsoft. + +# Code of Conduct +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +# Trademarks +This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow Microsoft's Trademark & Brand Guidelines. Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..e0dfff56a --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). + + diff --git a/analyzers.ruleset b/analyzers.ruleset new file mode 100644 index 000000000..5d9637319 --- /dev/null +++ b/analyzers.ruleset @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/creating-a-new-detector.md b/docs/creating-a-new-detector.md new file mode 100644 index 000000000..f041a7398 --- /dev/null +++ b/docs/creating-a-new-detector.md @@ -0,0 +1,142 @@ +# How detectors work +When the detection tool runs, it uses a parameter called --SourceDirectory to specify the path to run the detectors. Before executing the detectors on the Path specified by the parameter SourceDirectory, detectors are filtered using one of the following parameters: + +- **DetectorCategory**: It filter using the property Categories of the detector +- **DetectorFilters**: It filter the detector using the property Id of the detector + +Once the detectors are filtered, they are executed asynchronously. There are 2 types of detectors. Base detectors which only get executed once and file based detectors which are executed every time a file matches the detector's SearchPattern property. You can easily differentiate a base detector from a file based detector. Base detectors simply implement the `IComponentDetector` interface while file based detectors are a subclass of `FileComponentDetector`. + +Since base detectors are relatively basic and unopinionated we will focus on file based detectors for the rest of this document. + +# Detector File Processing Lifecycle + +Once a file is matched, it is processed through 3 lifecycle methods in the following order. + +## `OnPrepareDetection` + +This is an optional override for file based detectors. It is used to logically separate the actual detection of components from any preparatory setup/filtering that may be required before the main parsing. For example, when detecting `npm` components, we want to ignore files found in `node_modules` if we already have a top level `package-lock.json`. This type of filtering/setup fits in `OnPrepareDetection`. + +## `OnFileFound` + +This is a required override for file based detectors. It contains the main logic for parsing manifest files and building dependency graphs. + +## `OnDetectionFinished` + +This is an optional override for file based detectors. It is used for any terminal or cleanup work that should happen after the main processing. For example, deleting any temporary files that might have been created in either of the previous 2 lifecycle methods. + +# Detector Lifecycle + +A new detector goes through different stages before it is part of the set of the default detectors, your job as a contributor is just to create the detector in the first stage, a member of the maintainers team is going to be in charge on continuing progressing the detector through the next stages. + +| **DefaultOffComponentDetector** | **ExperimentalDetector** | **DefaultComponentDetector** | +|---|---|---| +| this is the first stage of the detector in this stage the detector is not going to run unless a user voluntarily opts it in. During this first stage we need to evaluate and test the detector, the detector should be totally functional and produce the expected components. | In this stage we are going to measure the performance impact. The detector's component graph is not captured in the final result output. Normally detectors stay in this stage for a week before being analyzed to proceed to the next and final stage. | This is the last stage, when a detector is part of the default set, it can be called using the parameters **DetectorCategory** or **DetectorFilters** | + +# How to create a new detector + +## Create a new branch +The default branch of the Component Detection Tool is main, all new branches should branch out from it using the following structure: + + [username]/[branch-title] + + Ex: + jufioren/my-first-detector + +## Setting up your component type + +Before creating the actual detector it is necessary to create the type of components that your detector is going to recognize (you can skip this step if your detector is going to reuse one of the existing types). +First register the category of your detector. The detector category is a way to group detectors that create the same of type of components. If your detector needs a different component type then you need to +add a new category. In order to create a new category you only need to add a new entry in the _enum DetectorClass_ and you can find it in the project _Contracts_. + +![detector-class.png](./images/creating-a-new-detector/detector-class.png) + +Once the new category was added you can proceed to add the type of components that your detector is going to create, to do that just add a new entry in the _enum ComponentType_, the file is in the project Contracts inside the folder _TypedComponent_ + +![component-type.png](./images/creating-a-new-detector/component-type.png) + +After the type is registered your next step is to create a class for the component, to do that just create a class in the same folder _TypedComponent_, this class is going to contain the properties that you are going to parse from the file to represent the component, most of components just need the dependency name and version, but in same cases like the MavenComponent, the properties are different, we highly recommend investigating how that is managed by the package manager and align with that community's terminology. +To add a new type, go to the Contracts project and create a new class inside the folder _TypedComponent_. In the next figure you can see an example of a component in this case is the definition of NpmComponent. + +![npm-component.png](./images/creating-a-new-detector/npm-component.png) + +**Important:** Not all properties on a component are required. To set your required properties use the ValidateRequiredInput method. It is possible for other detectors to create your new component type so you must validate as much as possible internally in the component. For an example of a component that have a mix of required properties and optional look at the class OtherComponent. Components should be immutable so don't expose setters for the properties. + +## Create the detector class + +Go inside the project MS.VS.Services.Governance.ComponentDetection.Detectors, and create a new folder for your detector. Inside this folder is where all the classes that your detector need are going to live. After you create the folder you can proceed and create a new class for your detector, and you can choose other detectors as a template to start yours. Since all detectors should implement the same properties and methods, for this guide we are going to use the RubyComponentDetector class as an example to explain the different properties that comprise a detector and how to implement the main methods. + +![ruby-component-detector.png](./images/creating-a-new-detector/ruby-component-detector.png) + +- **Export**: All detectors are loaded during runtime, so this attribute helps with the discoverability. +- **FileComponentDetector**: This is base class of all file based detectors +- **IDefaultOffComponentDetector**: All new detectors should also implement this interface (not shown in above screenshot) so new detectors are not going to run as part of the default set of detectors. They are only run when a user manually enables the detector via [argument](./enable-default-off.md). +- **Id**: Detector's identifier +- **Categories**: The categories is a way to group detectors, detectors that create the same type of components belong to the same category, for example Npm, Pnpm and Yarn create the same component type Npm, then they belong to the same category. +- **SearchPatterns**: This property defines a list of glob patterns targeting file names. For example your detector can be interested in files with the name _my_dependencies_ and the extension foo, in such a case you define the pattern _my_dependencies.foo_. Another example: your detector can be interested in files with the extension foo, in such a case you define the pattern "*.foo". +- **SupportedComponentTypes**: Similar to categories, here we specify the kind of component(s) that this detector can create. +- **OnFileFound**: This is the main method of the detector class, this method is called with files that match the pattern defined by the property SearchPattern. All files are processed asynchronously. In this example the method that actually extracts the dependencies from the file is called ParseGemLockFile. + +- **IndividualDetectorScanResult**: As the name suggest this is the result of the scanning and is composed of two properties, ResultCode and AdditionalTelemetry, the former indicates if the scanning was successful or not, the latter is a free form dictionary for telemetry. The ResultCode is `Success` unless otherwise specified. In general we recommend creating new telemetry records over using the `AdditionalTelemetry` property. + +### Advanced detection +The above example is a basic introduction to creating your own detector and should be sufficient for most new detectors. Sometimes you need more granularity when processing files, as such we have 2 additional detection [lifecycle methods](#-Detector-File-Processing-Lifecycle). + +## Registering Components + +Each instance of a detector has a `ComponentRecorder`. A `ComponentRecorder` contains a set of `SingleFileComponentRecorder`s. A `SingleFileComponentRecorder` is an immutable graph store for components associated to a particular file. The purpose of any detector is to populate a given `SingleFileComponentRecorder` with a graph representation of all of the components found in a given file. + +For each matching file that was found, `OnFileFound` will be called with a `ProcessRequest`. The `ProcessRequest` contains an `IComponentStream` and an `ISingleFileComponentRecorder`. + +### `IComponentStream` + +This interface is the current file to parse has the following properties: + +- `Stream`: A stream of the bytes of the file +- `Pattern`: The pattern that was used to match against the file name. +- `Location`: The full path of the file on disk. + +### `ISingleFileComponentRecorder` + +This interface provides methods for inserting into the immutable `SingleFileComponentRecorder` graph. + +- `RegisterUsage`: The most important method. Allows you to register a component you have parsed from your file into the immutable graph. + +#### `RegisterUsage` + +Lets break down the arguments available to `RegisterUsage` and what they mean. + +- `DetectedComponent`: It will be represented as a node in the graph. In general, only the `Component` property must be populated on `DetectedComponent`. This `Component` will be one of the `TypedComponent` subclasses that either already exist or you created earlier. + +- `isExplicitlyReferencedDependency`: This flag tracks whether a user explicitly referenced this component. In other words "Is this component as transitive dependency?". An example would be all of the packages listed in a `package.json`. + +- `parentComponentId`: This is the Id of the parent of the component currently being registered. This is how you add an edge to the graph. Not all packages managers structure their files in a way that supports a graph representation. In those cases we do not specify parent components. + +- `isDevelopmentDependency`: Is the component being registered being referenced as a development dependency. The definition of development dependency is not always consistent across package managers. It might sometimes be called a build dependency. In general, if the component will not be present/referenced in the final output, it is a development dependency. + +It is expected that once `OnFileFound` completes, the `SingleFileComponentRecorder` associated with that file contains an accurate graph representation of the components found in that file. + +## Create Detector Tests + +We have two kind of tests for our detectors, unit tests and pre-production tests that verify complete graph outputs across 2 scan runs over an identical set of files (this set of files can be found over at https://github.com/microsoft/componentdetection-verification). In this section we are going to discuss how to add a unit test for your detector. + +Detectors' unit tests are in the project _MS.VS.Services.Governance.CD.Detectors.L0.Tests_, to create a new test for your detector you just need to create a new test class inside this project. +We recommend to test just one unique scenario in each test, avoid creating dependent tests. Since a detector depends on the content of the file to extract the components, we recommend using the minimum amount of file's sections that are needed to test your scenario. In order to reduce boilerplate, typically around configuring file locations, in our testing code we created a `DetectorTestUtility`. + +![go-detector-test-utility.png](./images/creating-a-new-detector/go-detector-test-utility.png) + +From the example above you can see each test is initialized with a new `DetectorTestUtility`. The test utility is generic over all `FileComponentDetectors` so it will just work with your new detector. The test defines the contents of the "file" we want our detector to scan. We then provide the file name and contents via `WithFile` to the test utility and execute the scan. We can then verify properties of the output graph using `componentRecorder` and assert success using the scan result. + +## How to run/debug your detector + +``` +dotnet run -p "[YOUR REPO PATH]\src\Microsoft.ComponentDetection\Microsoft.ComponentDetection.csproj" scan +--Verbosity Verbose +--SourceDirectory [PATH TO THE REPO TO SCAN] +--DetectorArgs [YOUR DETECTOR ID]=EnableIfDefaultOff +``` + +- **DetectorsArgs**: The value that you care about for this parameter is [YOUR DETECTOR ID]=EnableIfDefaultOff, the id of your detector is the value of the property Id from your detector class, so if that property has the value "MyFirstDetector" then the value of this parameter should be MyFirstDetector=EnableIfDefaultOff, you need to execute your detector in this way and not using the parameter DetectorCategories or DetectorFilters because your detector implement the class IDefaultOffComponentDetector, so your detector does not belongs to the set of default detectors until some test are executed to validate that it works well in production, please refer to the section Detector Life Cycle for more details. + +## How to setup E2E Verification Test + +The final step of the contribution to detectors is to setup its end to end verification tests. Please follow [these contributing guidelines](https://github.com/microsoft/componentdetection-verification/blob/main/CONTRIBUTING.md#contributing-a-new-project) from componentdetection-verification. As a good practice you should include the link to the verification test PR in the description of the pull request that contains the detector code. diff --git a/docs/detector-arguments.md b/docs/detector-arguments.md new file mode 100644 index 000000000..16fd9bbda --- /dev/null +++ b/docs/detector-arguments.md @@ -0,0 +1,71 @@ +# Detector arguments + +``` shell +dotnet run -p "src\Microsoft.ComponentDetection\Microsoft.ComponentDetection.csproj" help scan +``` + +``` + + --DirectoryExclusionList Filters out specific directories following a + minimatch pattern. + + --IgnoreDirectories Filters out specific directories, providing + individual directory paths separated by semicolon. + Obsolete in favor of DirectoryExclusionList's glob + syntax. + + --SourceDirectory Required. Directory to operate on. + + --SourceFileRoot Directory where source files can be found. + + --DetectorArgs Comma separated list of properties that can affect + the detectors execution, like EnableIfDefaultOff + that allows a specific detector that is in beta to + run, the format for this property is + DetectorId=EnableIfDefaultOff, for example + Pip=EnableIfDefaultOff. + + --DetectorCategories A comma separated list with the categories of + components that are going to be scanned. The + detectors that are going to run are the ones that + belongs to the categories.The possible values are: + Npm, NuGet, Maven, RubyGems, Cargo, Pip, GoMod, + CocoaPods, Linux. + + --DetectorsFilter A comma separated list with the identifiers of the + specific detectors to be used. This is meant to be + used for testing purposes only. + + --ManifestFile The file to write scan results to. + + --DockerImagesToScan Comma separated list of docker image names or + hashes to execute container scanning on, ex: + ubuntu:16.04, + 56bab49eef2ef07505f6a1b0d5bd3a601dfc3c76ad4460f24c + 91d6fa298369ab + + --Debug Wait for debugger on start + + --DebugTelemetry Used to output all telemetry events to the + console. + + --CorrelationId Identifier used to correlate all telemetry for a + given execution. If not provided, a new GUID will + be generated. + + --Verbosity (Default: Normal) Flag indicating what level of + logging to output to console during execution. + Options are: Verbose, Normal, or Quiet. + + --Timeout An integer representing the time limit (in + seconds) before detection is cancelled + + --Output Output path for log files. Defaults to %TMP% + + --AdditionalDITargets Comma separated list of paths to additional + dependency injection targets. + + --help Display this help screen. + + --version Display version information. +``` \ No newline at end of file diff --git a/docs/enable-default-off.md b/docs/enable-default-off.md new file mode 100644 index 000000000..579fa1d97 --- /dev/null +++ b/docs/enable-default-off.md @@ -0,0 +1,7 @@ +Whenever a detector implemenets the IDefaultOffDetector interface, it must be explicitly enabled using one of the following methods. This is intended to allow customers who want to test out the detectors in Beta and they should be warned that these detectors are not guaranteed to be completely accurate/functional + +# From CLI Run +add the arg `--DetectorArgs =EnableIfDefaultOff` Additional Detectors can be added with a comma separator. + +eg:`--DetectorArgs Pip=EnableIfDefaultOff,GradlewCli=EnableIfDefaultOff` + diff --git a/docs/feature-overview.md b/docs/feature-overview.md new file mode 100644 index 000000000..d27784fcc --- /dev/null +++ b/docs/feature-overview.md @@ -0,0 +1,18 @@ +# Feature Overview + +| Ecosystem | Detection Mechanism | Requirements | Development Dependencies labeling | Graph Creation | +| - | - | - | - | - | +| CocoaPods |
  • podfile.lock
| - | ❌ | - | +| Conda (Python) (Beta) |
  • environment.yml
  • environment.yaml
|
  • Conda v4.10.2+
| ❌ | ❌ | +| Linux (Debian, Alpine, Rhel, Centos, Fedora, Ubuntu)|
  • (via [syft](https://github.com/anchore/syft))
| - | - | - | - | +| Gradle |
  • *.lockfile
|
  • Gradle 7 or prior using [Single File lock](https://docs.gradle.org/6.8.1/userguide/dependency_locking.html#single_lock_file_per_project)
| ❌ | ❌ | +| Go |
  • *go mod graph*
Fallback
  • go.mod
  • go.sum
|
  • Go 1.11+ (will fallback if not present)
| ❌ | ✔ (root idenditication only for fallback) | +| Maven |
  • pom.xml
  • *mvn dependency:tree -f {pom.xml}*
|
  • Maven
  • Maven Dependency Plugin (auto-installed with Maven)
| ✔ (test dependency scope) | ✔ | +| NPM |
  • package.json
  • package-lock.json
  • npm-shrinkwrap.json
  • lerna.json
| - | ✔ (dev-dependencies in package.json, dev flag in package-lock.json) | ✔ | +| Yarn (v1, v2) |
  • package.json
  • yarn.lock
| - | ✔ (dev-dependencies in package.json) | ✔ | +| Pnpm |
  • shrinkwrap.yaml
  • pnpm-lock.yaml
| - | ✔ (packages/{package}/dev flag) | ✔ | +| NuGet |
  • project.assets.json
  • *.nupkg
  • *.nuspec
  • nuget.config
| - | - | ✔ (required project.assets.json) | +| Pip (Python) |
  • setup.py
  • requirements.txt
  • *setup=distutils.core.run_setup({setup.py}); setup.install_requires*
  • dist package METADATA file
|
  • Python 2 or Python 3
  • Internet connection
| ❌ | ✔ | +| Ruby |
  • gemfile.lock
| - | ❌ | ✔ | +| Cargo |
  • cargo.lock (v1, v2)
  • cargo.toml
| - | ✔ (dev-dependencies in cargo.toml) | ✔ | + diff --git a/docs/go-detection.md b/docs/go-detection.md new file mode 100644 index 000000000..d7a67cde2 --- /dev/null +++ b/docs/go-detection.md @@ -0,0 +1,27 @@ +# Go Detection +## Requirements +Go detection depends on the following to successfully run: + +- Go v1.11+. + +## Detection strategy +Go detection is performed by parsing output from executing *go mod graph*. + +Full dependency graph generation is supported if Go v1.11+ is present on the build agent. + +If no Go v1.11+ is present, a fallback detection strategy is performed, dependent on: + +- One or more *go.mod* or *go.sum* files. + +For the fallback strategy: + +Go detection is performed by parsing any *go.mod* or *go.sum* found under the scan directory. + +Only root dependency information is generated instead of full graph. Ie. tags the top level component or explicit dependency a given transitive dependency was brought by. Given a dependency tree A -> B -> C, C's root dependency is A. + +## Known limitations +*Dev dependency* tagging is not supported. + +Go detection will fallback if no Go v1.11+ is present. If executing `go mod graph` takes too long (currently if it takes more than 10 seconds), go detection will fall back. This can happen if modules are not restored before the scan. + +Due to the nature of *go.sum* containing references for all dependencies, including historical, no-longer-needed dependencies; the fallback strategy can result in over detection. Executing *go mod tidy* before detection via fallback is encouraged. diff --git a/docs/gradle-detection.md b/docs/gradle-detection.md new file mode 100644 index 000000000..0a38a9d41 --- /dev/null +++ b/docs/gradle-detection.md @@ -0,0 +1,16 @@ +# Gradle Detection +## Requirements +Gradle detection depends on the following to successfully run: + +- Gradle 7 or prior using [Single File lock](https://docs.gradle.org/6.8.1/userguide/dependency_locking.html#single_lock_file_per_project). +- One or more *.lockfile files. + +## Detection strategy +Gradle detection is performed by parsing any *.lockfile found under the scan directory. + +## Known limitations +Gradle detection will not work if lock files are not being used. + +*Dev dependency* tagging is not supported. + +Full dependency graph generation is not supported. \ No newline at end of file diff --git a/docs/images/creating-a-new-detector/component-type.png b/docs/images/creating-a-new-detector/component-type.png new file mode 100644 index 0000000000000000000000000000000000000000..5e92b44d5efd97d3f5eb3c4867927e63410d795b GIT binary patch literal 12581 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 ZXxwD*9WI?G^rsCla#G5Yl|aLQ{{@iXtvdh! literal 0 HcmV?d00001 diff --git a/docs/images/creating-a-new-detector/component-utilities.png b/docs/images/creating-a-new-detector/component-utilities.png new file mode 100644 index 0000000000000000000000000000000000000000..940e564e9793aea12edf9dcae36154e5ccceb2d4 GIT binary patch literal 25379 zcmb@uWmsKXwk?Xg6Ck)JI7x7a;O?@3;O_1g++Bhb+}$m>1_TmY%A6 z-u=PH$7D?zV~x>!>#emuL*!&ckrD6^z`($e#l?gaz`!8v!N9;d;2?k_4!7?|fnVVE z3Zfsu%0~(IfIpy&1*8SQz^bAUA9SICzu|4fK)@N;l+Qol;tCX}U|@QU;z9ySE?P%z zhz^*l8N1rC5uG|2`J~|fOUOOP227Kt-#F*t7+0*s)JG{`=Z@dS5|UsV%dk~J>TwYo z{e~0hayZe;KMSP$3{RR!_@#&c%UnT!95YER#TqQKsn*VWb5a|zju5}9k#3bo?}lUL zqF+=+zib3fPwe+QJXbXuw(j40|FQ! z#U-7nSjJCVdmh*!8G`O8nUxLIzz5)bmlBr14IdA+Q$gH#M31QiN-Dr5wvKmEs191j zj&;sVT&((Q3QK{{4lK($c+H$`PJ+aUGCD3XxwDqQhWUZ3M+@-38fw<*tg+u6)kk+_ z##95SsdrWaVPV@15W!KM@FP0 zxQ0I%x-qK|1VUhmXfuo%CmvCRY=GB+GH?o~=Nd;}PrVt#r^Q2MQ+eGsJ|Hb0pA?R~ z;Pu5-j4s)*Zo>YvAZIrm+B@Nhh&mffwH2KJ4>A15CxkVzL*kD}g{KXE#DN073Wm_2 zIyE{vGDL6l?^_2oNl31a4ef^!gN&9^)veAv?hM078!@6!`(+JJW6h4gFNnRb@A@JV zii(P&*$s!{RkdzUCis<1E_?HY>FA)J46yOlzCcJUO(=X?qZ%9>%oDAj?mVZMI&w6t zgAVAl#2%sfVV3TGKvGyq&F?zcLVUN}Wo1=kv@0gtkET}*Lrq#Ruw&5V2g`{npxv~j z{ATd&TX*HuCWIc5bvCAC)*N!u2r4K(BtHbX``Q$_KcCZ6Hck-f{uT9A^-sS5!~=}>BD z_9Xic6Ls3%?OlN!$R-rjhLB5XTIA7bSJ&YKO2W>)Ezmy5z=oVn_%iVpn16%lXcl_E z0E2OpeR|hhm7sndN>*}7WItb4^3BzOj>n_5Al*uI5U8kB#3AXNyT2fEe{Yuy^?1gk z(rD}J^#sA$U4g0~u~9;Q_NE>Y47$=(6M~ggbU|_$M;_YSAXvZq1}MN!)65C$cB| zjS{SF2l?ZlK?0_(5)W^~I;;0wAu=)E^@@(mu`NFit*W31gr&}5>G^fA#o?P z=LK0FG(T@+ zZmr(hGNmpFWz4|2d(C-kUfqL zAMeh4AM3QeRA6=O__O&|z3%BBxXfoteNJr7DqtWB@KG8$LQrC(SZWyaIvVsPwRo4@ z);1L;F=SqSr6{bs;lbne6;dNa&JGlwj@J}FN7gA5-_?{}F2|ET2rToWU9^EcEqgO< zS#*9)g&4V&l2x-6Dj^-Mbyy|_qf>;shy$v|#!5-VDGv0r`fjA{*!EVr)+-OZIpYBH zzS)p+%N#X9@m4X|iUx|CNc9rITJ(utl4stAQwL)YVJCQ!C|~cFB97Ri>q%0DoX{6y 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 literal 0 HcmV?d00001 diff --git a/docs/images/creating-a-new-detector/go-detector-test-utility.png b/docs/images/creating-a-new-detector/go-detector-test-utility.png new file mode 100644 index 0000000000000000000000000000000000000000..700311996abb1043f2494a99c9b453b7bce6c790 GIT binary patch literal 88414 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

Elb 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 hscrlJZ-mQUCQA(NAYh)O?J;?jVCmgv4kRI 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 UloHAxO54Ir4J{2S^<5(V1EE;0j{pDw literal 0 HcmV?d00001 diff --git a/docs/images/creating-a-new-detector/npm-component.png b/docs/images/creating-a-new-detector/npm-component.png new file mode 100644 index 0000000000000000000000000000000000000000..82385855b45b5ddaaf9bc7542b296e375d42b395 GIT binary patch literal 57191 zcmagF1yo#1x30}5kc42tAwUT365I*yZjHOUySux)ySuwvaEIXTZry)pXMb|wcL>a))rLg+W5G#vpav=cPTFfXb`%8II5|1|J zHJG@#7}mjmt|>8{S$TR+^9st!T#D;&oEp-KBYS&$$lzQ3UGld+gWmnA{-Cc$|5Jk) z>{k7!hWXCyuRm37xbolq0~hb_|9@W$n9eUP{nWPZ!x#8WdMAej5%-bP9eiatQZ>2| zO4lZj-M$18MD}GiP_#n zb29enoC>?{4tr|<_Ye?q-VBpm59&6?FdbCb*rjC(?v~`&s-R#hqgV0|s6}2X>-CGw zpxY6Rju_+fCy9?lKHvfbdLl^$LZMxbxr~w$N_qXl>-502Lg+oPs}uJ;EmghoOF)~J zA)=U(G%d!EbvRKUyi;7en}~qt&SF@>jvQR&qh-mmTZ+t1|Jo%Sikc)sso?77CI|@1 zZBAl>{!+T!G2S-OJq&P|gF(vZ^!2xE9s;5iUaqE^>fc}HgwEC(xvMHg&7R#1EAh$4 z$CBTvLqd2H3@j``c9j6uP;tMa^G3g`$;m`$ z5Db2~K)gp!n@%9je_&xzN?;FvVV1`_S?A~HCn)DDf^tU|m9*DuPz&2Ed>^`9t}0LK z&{&3i_)%~<<5Wq=$U2I=OrM;C)V5egi#IpVXjr4mP&bT`yZFnK+!NbIZB$?`ao$pG zURdvYLXzezuxUg{>l%JosbCyk-I3DCUGHUV?Gofw|vKMNG%H`@n z3hFx<8M~yVx~6KgV+yKa0<~&BL1T{W77V$nD{dYa?7y`9iLoub;1YqvxUYT((OB=*R}Rw|{Dx zA1hDC5-p(>rpAtwTFSduXrNKeXa3k%_JgOW$(9e*gM+=m^+ZX}Ah@YHQAn>{Erl=$S4B}#@#zita^5EGFDa4~5-7-V3|$F*S3uxw@Kn`PoCVUan?5R! zpG=VrHv}+#=fw~^xI4a092Q<#9r#al+G5(4_Csm(Jka4y1BCbrCO%2}~5qqRvr_LzW4& zkG{Q>trBGUWqOB7;~3E4IhUPWLmE{}--fdQ9(A4T9ylkuLkt?Kl=on(DkTi6~W_#Cy8CWuYgtzE3@1sr=ZdxqnfI!isHCaRS=k}4}H4H zko81ED#-03Fwx7$ijFRcRlqGZ@K_tr@SE0QZcW1QTRi2sxtS(a82vqW#o%Z>@!>FHbwg9PgNM_TE*zWN^q ztP)9dW$V?u*Hg4S6)hoduXmZoRz}XO@=o+)(Pk<7-gGJ@+lsC%W`(}EDk|e@KX+2K z4rrq~8!5k6`RT1k25O|X^0()QjELNx;9R6%14-*l?LCxpFiGICoebF9Nt=cFGTPu~ zu({F4Kbcd1?u)dT4%U0PoVKPZzEKoFENnO(9x`&PBI9{OKPgk-J1(iLE4r#2^CRWY zm~*tr^F@n6^DL=!SnH&$YM;Tr$_xU-K{*`qJ+d-+77`bCXx|tduiVvgA zHaIBCwStn{Q%C-?C6jw(=SERWBjcl{yfTsjo6N|GmyDC@gtyp9<2pasV;myk*_P{c zVx46mji}5o{YuQuk-F*`c`icL<>$)a(q;GEzI|Drma$6y&n)qDxRcST+E}*y9u^ zModf`*UFtJviu^7`f`8%z($y{J&|$l99N{xq^@|Xkg14pHwu_6zp8$9-BBD1o=un| zEaCjRFc7e*k$>Yku{=`mxJ5TF6qgh<8Rfrxl+qe!5_sBg>^}~FCy@ApIUIUWIIj?8 z(o;*?B(&%>X?`I#+U|*~W>wMBGEGW6=zfdIjs7Sd2;GPtdu$M=!ZVe&bR z8iW;zHh+Tfb&u6A%ZCc6N;v;oIK!P3bO-^VO`$iYnmy!UXXlqM^%*XZNKUYUEgPMu zfX_v`+@`zSMQAMf*};ywto$UQCVlybvN9=^eok%vWP@@7a~;fM zU;`m^K$&|!_a`RCaRoe%^$~!2Uw-7L8$V>alK@!bTm1r0s;b5DcKce(q4MEEJ3}{R z`m=lB%ah0S#tBhasuW6>33#-qMpVBUMz9Vd;&HF1L?B@{lv8EpS582m-L{J7Zfy*Y zh%02IK}(k|NUoWS(79jd>#nim1U2FRio5?%$Bc$q8H>D}@_G17agyrhS7P5IXVYI0 z+Q@NF>x1*$6FcCg+I)3-sOw(kH9yl-P(snHc0$ zi%akN9RJM6jr_7KO>!Pr+7_k!8MjOR208Q5)rC{@LkSJvjj_HNiLA&x#%u!rJY98^ z;8|cF5-Q~+c3}{eX}w8PD%6lxdqkqTIK!0Y@l`S2pw%kMvIDO|gpqrFMgA@CGU`oLeA8eR2p>dAu+i;X)%7_p zW1yvMVU`a-a1|)$aDll=b5$jych+D5H)GF^Pn=vYgt`hjr?uQhhm7xA4-eg3`*E???zYYF<}I)kBVJ~U;yScl0#|(VeNRseG^A$CmiJo| ziEBc}J%&6yZSWoXsWj>iM(VGZ%$ZaoW1(<2C`RSvTj-rgDQ#M?x8vkKGBd|oq^15K zfG<9ORm!rOfF1PwemVnblgOl^*e?+t=;4>jlpTgzkq#VLyR6anQ509v{`uQM%NCJ{ zzf`Cxoo$VCUplLoI%K~WU%T^kS_6ERhqSMd@0#nggdMXSOte^U$zr^FF7@ZNjI7M6 zEY?SF zWfFy-wZevPPOvL?R>Rl{dq@#84ULX4%4$RmdHUkxRq5_fAFL1dH{z~M`rLPoiH>1{ zsmnSlFDJ9kztB1J&O{xJ#nMFtoqcmNSXR+#g<6yndXFHa;jmbuY=d~QhT}My#oR5% zL{In4t+FlBft*JDJS@RQbO=VObLg# z6Pv08WE)OOh1>^9XHFJqqVGwl&WU^=rvnGuR1M;Z8k&x54G`oba#GT*KYc@^cFgAT ze;lWuB310eS$WoWaq5*#tKdCEv3gjBF3nTw(mRsQ!U-2DK5FPVOk?JfQk9rpR_|$1 z=@$%{OA;LyELNs#9kY^dScczLEK{4BE)+be5Yl)xJjegyAUjw%BsWS4nXa}fCVucz zTiti(6$DMbWU@P}zTymB*8F%%MqA?hR_LHlX^F+OJu>a}_<63@fpR~~NhmOJ4^Dj2 z>@{=tnB91#qhQ^}#%9IJa3U#JIYGralHQe3xS3~hlYAbuvit@k|#d94Q z);h)fJeA#obn=hh>(>JK#TrcHVKudF#H?Mj6zG)0wX&UD%82A(u%W$~8t>I0m{qK- zM(m#ThKS^3fL~Vd=Uah7=QfA4W1-} zAJcaByV3Lgw=B32svA@sfQcaJeo&Ry!Gy9F#|^K*zHoBtJnKz*|*i7agV#>~Kbq9B|z#IBBM%y{$=hf;md-)>=t~00a_gD^uRk*+!DOq){(Zbt9 zNxAsll&ZKAGymv8>Epu{%}UI6R;z5Sxz6)&`nus1Pg*_6>o)~&eaDqd?;3s9|NiUT zCv7|qwq&$e5w>+}PiPDb9;Luc;4ER+Gtn57A=zm7OxA{n&+Us7aHqD4$^(?qwfdY@ zTP`RB+-k}iy~gNlSm;o2y{LQv)!AkkVbzhvFS()ajuyyyz7Zp8-i-9|Ja%b;p31G* z<9jd&K$ktQ8Z%3pO#05Bq0@TKXNuIY1qds_)HGb`Mr%M~eRG&(89ASD5 z5!z{9Sqp@)pbA>|%V6ZOmR&$uULJf zd-n-dT~jBqUD>S9P&gHIucY?LvZr+5vFdKd*GzXLGkMG?>>Z7~z8?Gqr|j5C^K_sc zv90Q0BF-mII(c6;ag6M;AFWIAxi;^*j2gS}#67-<)=d)y5#d&g;uTGFB`x!a=e4Eqb~qB2#@RY!!|rJO*@3U5^|JQH)4k04n`}qg@crXV!T|%* zVag&PA8CzWLw@Yzvdw@paN?YJ$tP40|4iLis^yZjOJY(dVM8n3UieIz54dkZK6#5} zhlz$-&}fR!0cfVDbF*i-0PU1GrTBL#;1hflY}xj5*ND}8>d-`NJV#vc-L}Y)&i;V= zULFz;shCi0sK8+x?GU$qfe4rRY4d3%X_lIeh#%)b3abI zVOAq=lVf<6j2#hmUm$RfgUoY5M@jO94Pe_2ieW#yZ9*6t)=uUYek~+pn(Z*x77)Lj zA(Zx_^3>Ot$kv$#gabD_L80~w30e&4B4T3WjCXBR4=^wn?oqn8nc}mfG?-xf;d$9a zZ+E>;zyhzsF0L4jz8BaHs$huCv{iwOc%;p`Qz(<^i{LsxcgnI3;&`O&TK7P+5Wmh* z)AnTRh4HPlKnaiWN?H$qibIE9tO(a-UlfVR+V0z(A>sRQb~gm_kvW_hHQ}DcK~m=H zfZDZrJ8xXL^9$}=?7x1-<+Mmf@1gK#1l`)|DQMgq1ntaG!mO{W&ZR&rDkjaKuTfJV zu)~Fom;U`|1@QN(%EZ%p-Ve}L`yCy;Jsh~GBjcU5$=kKFy=_;C8t|;jdl)jeiUZjI zvZ@-}5_dAs`kRBOMx12iv z2lw_6ryeim9j%Z3rrt23#C7!nRin3Ak)ebyyIFo{m0^=Mtbv4Xf+>Cj>?2m)Tw8DSfa4+!)%29Yu-|Vwy#ywEWAdkhy$1br<0>_}YJoVm=j$dc?E3$kIe9Q|o zJt)`cp>C(esVEH!3&onA8U2ATrqffhL<;K*yaF-RGTK)_94I3}8X>D%lu9?EoPm;- zL!ABA^Of+;IGbWyDf{o~Bp9BcX%=u?PY938pjLJ;c3##WgOy@CoH*NO?%Tttj4n?u zH_Q`6`?6dg6m`FzlI)O8@pkkv?$-v#OOUQ>7hyy=jQRAvTIF#0=^#6oO}!>3 z&_GlPWnjF+BY|1b&q|V-V|2>)DtH{G*4Hg3z2P?Xh@cmu5gq8aC?TGCZ#tos_4Fn} z#cZ+Mnva_|Rs|~kk{<@#R8F26RJM1s%6QRh+%PcYn)i?AiyS59_PBVHUH%rnr^l$m zOlhjtc7MLUs+y)DT?b>*YF=r#+;C9w4f@$5tRW=$7N|{K-2B}dTFX03;WEA;=%YO* zSKiFu3$jh$xhL#16Bpa_a<@Ck#~u;KqRw}mt@wrEQjbd&0So2xk;%BD16?0`P`CkI zIZ4i$K+oxi`JdZYIB%J7SSSGg)>Kb3X!N{FMQPKxUZoPFm}lr6ruvc;&W_u)cV56d zm`=Xe;8sjd9w`-GEiNWr0+g9~;2B7VW}STrPPP-|7aLlmTR#FPfVOS z@8>=C7T%hTvKP5S6Cufnj_4>vzc6>+4$rE74~}wUS!O{C=Cpg%-%JHB76RULzI}-K zyCjHK9(qPI6r$t$rmMv&kXR`CVVsw*FIB;M3?eGi@f2z9&nJ?0Kdcu{hFU=r9L_km7=gter0qp@u-c1Box&3*t zQ_t|z0fkT)F1c6*yTw0vhmKeT02_KeFxB}8?MU|qR`WTdP{uVo94<^`H6}xL@OO)1QCYfd+Q|{s00(9;2nbT^+}eFQw!8Dss=@1M|G) zA+qeBPr>(!UYEW{sFun8P1Jy)CV=73`|^L4hd}+Q{?7wh+k`$k+C>og&yU2VfQK%q zuZiBe2D}aVyO7YTlxK@w3Mx?Ij1LCiS(&xNb2R+v8=c9Br8zHm?OvnghyNN|1PZq0^3|hLnwEX`fqbn^fwQSi-+}O}WdJhekNeaHBT5L6I z_6*G`<$wAG5tn*e9xyW_7d1rqkD9Iq2V)aGal z1~E4bf-%=F*p54*jyLG)6kFB+{~_FgBP}qYa;P*WLq?OOxb?I!FRqlkYAF~tdn2r+ z@*MlxI$GOmQUzAalcu2Qs-)8RGeyjtMn<11q({5jCp;X?tt)g^F3%`ba+(;WV!*gs_2+dmdW77u z3Igc)!Z|eROj3V2`mYfMLr3>6`jk&(Vk7acbKqwHATOZ@oA1Z`n@!1$Nw6z*aMb2i zoOQmOo0+X5**rszUE3zvx~xf&vvX4Ue)|B{gmF#7K0j>Yol-OBQA&?Lm-0R9X=`*a z#DA7D@(0DwML69iC^1P**ibV}?}hMP6N@jic@p5}g`%Po&!sIsicOInvQwDT>8huF z1?xsw%UpXe&(DshOZbpGupOsI6RS;rx5df9a`<;+f~6kCEV-UkNqZtzWE8ULzE1o) z_+!%>zur=9IYYjml=+vHZjc7}rn%*g=yB)^Jg&>-+Z7!S#z?U*o>irqt}pS~4-Dj& z1ViTsd~ly^VqbM?e@Y3afYX1{x3Zp9aQ;06kKdA#lEV53d^~&;*zc?$(RWb4U}7OE znn89N%RwR#-w6A4O#bj&L^@gRL6ISXKKh1!a$>%TMdDy7nO&-YDO;2CHPb?jzH(-H zgpP~&g*-D^&r!^k$j3oB%r^6@9?n$>PKP{r&B2ur+;r1qr2Nd0aksG1<9+ zDOJR_mtaa98MyGa?yhb1q<|g~8@%hYf7D zb~#6VFt6Hsc)4i1;6`F55VR);)aL=IsEcXQdML-d;Dz2^Nz)>vj0&8jV9P1_WPIZz zEoAA!Lem_y!v@8=>Yv?^7p71m-HujVpV<1?6N}d$+0FtB;w#@mbWfxdl|S2YA6^IFiI>#iUKpj8&CU zG(8c@%yE|bln zDWl+U-~!0L@15SZ6!^9Ay(_XSOA=&b$gQdpdwOM`s*kBV8eLSZdxH5JRvV_GprojM z5gCuuLE0lk0+u0=d~)(nB0bhw$)U$76i=SF1lT?uBxOam3(4sm{9lAB>;EKF2I);d z@nj9ls#gdQ^PFLBr+l{_tjhFt-BTXU%1wZz!ZkB7a*@JCM-&QndOI+=l zX-!3f9PaMk_z-$MYgKbD9FQ}4!0z_tB7)PIeok}(iG>1S0$@uzhC093aM??k{8;9T z-bZU>Nby-Ub@tr{x4cQQCIPvH)XY`n+;jUHiXdmOpMjP&0?g?bF<5Yg@nbOc+)af$u+p= z@PZkZQDH&I0MfcFopP%WUZP-6ph;0SD>`&cE|;&23W&6q`!egU4mRy4#S~(u62r`5 z8h$D#Pfm$~1s6vwB_dp56Gc6{y4LbJ*Lfq>k-R|iPuxZVcI`Rn`S0ytm^QaLq06>P zb&IacZHtu$ZTG!rvChn`jN(vM*!g6gJZ9-x5_DK4LZkOx1zhpdIO5(X4E41(U$%wW zZ&b0hCNI$76s{OwU;liHq2u5J)5D4A49tk=mYmFCTRa98EM3*=(~*kly9@?AI4dt; zoK8FZq{4LiOMtNAzhRYF z$bWEpW3LIMdQcojckLl1xu%CFeTt~0_%4cOgrBoTca2*9sYKu||9+t1J9?Z&2Yn)g zn~2lS9)3Vokg9@W>eAKzWbo{eWcmKSkkQ=m0k7iK1j#S#g-Cna%1^PX4k_`VK zslre5v44;hoW6k&@NG7t^K$wIw1B7!HU0|^bFC^f`U$+n7Ecf_F=UwJ#RW0ta*lvoC~UXx7FE1 z$lM4;+`nISyj_0H#gbQ4t(-*!Ce@w5?SeQdQ08AIoqthzv31Q`Hi<=V(Az;T9W17~ABcbsba0}dibe}# zlq@u1Vrh2J>K_W?Gas_SFA7a#UNGh=r$>&gwz{?y#HW6%uy#%^|A0=lHOOb_s9bI( zY}DM3Av3gRi_gyO@BzYmq{rARF%2`efg+6^UA*wc-5_v6-~TQCOJI@yXi$86V`OIW z>;WEOUxZ89$BTd(qLxUicp3e;VczO|)s~YJhfefxoD+k#^c2yu5K+rD6jAH|k#26Z zG;LXqdG*VuGC-ycEeZVWxqc#k9b3OxmBg&v8{>9(Zu$5~1#Zge83PU4NTc3?%x@DM zWsV|Q(#UMBld51>XV2~ttolSzS;xZmaf6ILJ2x|6hHA=n4Gy;InCJ81(96B20Y_H) zxsMgl4#gT(N7b~Wg_B>{<+ujc&7X~*!~U?n|H46IG+-R`4L6>n)*3fH0}szXryV5{ zo?X(HhW$D7s|^dLQ^zBth=ZDt;tufkNS_vvslzyzayvrz;;ND~dxTgm=|nA_8`Sod ztsUMT0eZr;Ldp>Sk?8w6AEFc2;;3HT{9!8*Z&1_d5Zvb&qy2MzVj3rYcezhUmvsWm z4JO>l7~yZaO9jx|RA#lMB8?v3V8G51vJAbZJG;i#1+e$YLDdk?nUiLoUB-|2+_j6t zk}8<|FLs|r+PmsZ%Vj%oJTW!-HjS+1k&y9(-dboRPmb!bo>Ylj)(T$NcE7bT#gJIj z8oBy0vM?(sD!ORYo?g%Vr6s+e?@nJ%b2ObzL(kv*2MLL|&E``{5sVSORSE7s-N7%0 z%$pGW1A~(C{vR-?!}H%TC=CpQ%#!IMipYP;c>JM3P3Zqm8Z`L7(V&Ha|CI*mXzbb3 zfHN+NmTLtKJrW#9W2Y`TEtKg?njf0*X13McdKtW&6nOI%@@)?X#q=D{5ZH+U8gMUn0a;>-^g`; zJe_UU6$X@>efjbO7!hy)&XA-hz= z>&t&{z`*%?yp9Wef1OYM*&{+=64H&aVN8~9yiUN=y6dS#eDovt&Yf#?Y*>)`*?M|t zD+u)^Y)7@jsc%DXqJky#vY5W+fe^f_CNGR`=+@e*YcI6I_*+fnEZFigq;(TaVOqCC z;<$b;+|giSl8ImT8CrbC=`1BCFy!P-q9phUz!6N@sYoA*L|Ga`fo z-w>u5U1Ky;t^2LQBP+)-Y{r)eG($#IgTNEetN8{)SM#f|a&WQp?jGpinKt}bT(%7z zgnjoK7C`%UXq3d`v6@6b?=dn6-p~52LNZ~u)>>X2jb(y_?8Lc4ca|L3q9MPS)kp8K zyximI34d_Bi}1nOt%PUtvGAMeZNMh&^#wcL5&`h=XlZRm;&KRR&)|bq(bY;+MWjOQf$3ot_b8?r;uH1753(nHo+yD!F%iwPx!)nY57KunHO* zMh-C7HX8Sa{mH%bHuky?eFnGTtW@S&jSW`=jRc+RW1`GWR=E!^nZScRK#L1rtSq(7 zbAHLZN6d76cs)wM2);DVBU4szclyfV{B*_7;*kY4FD?&7ClXZ zs1Vt7&K!>lWaw(Yxq}!&1bAXT$}qlAr>k^4*>*AhL}J~8pDZ&pP||&e%K4p&`#}ye z6wkHs+&C*(ww+pe9q=^W$$@ymF^U2>$j%{Ss#80+1%sz+hv=ascK?t99MA5{o1A|k zK0*2l2rSuntOA~n7s#WBi`O9oEtO{=hgZ`bkAX^9_-mgPPr@FpyIF->$?b;51ITEP zeTv>tmaOK1jFq$7GzYBoj`%IQUtb~uFBZMHPtp(!E(3;nUa!Q#E%wrXSbp#IJi;r+ z&-OD!^=J5R`^LZHQPTb3qO(-fAr3&5>pMmEpTKd}c(f6Vp#L}uHb7^V^}`H9?G2Cz z^z8ppW^`rYoJ0%M*!^2ZSDc)SMg0lPSZ;xHd z%E99Ox@}H0niFr*@yn7<_T^t!ZWBc2Iar>-!rugL)7xMfNo0U$Ip|UXWBUn(oF@W3 zuC1oIIKxkk-LlOJK#e@K>T18-tM^P&_qic=ZK?VoORNbrLC7m=Cf_h0do3}F|D>GR zx7Lg^o*FAA_~wIm!U3mKG-d);eVCea3Fb1xlD>f-Z4RQ2xKCNJftx0@Zp*To8<^=^ zqvvsQVA_$_tfa9LONSO*_hwet(O>kU#X~*8;T7=HZ#D(kmu03}d)oZV_ru~5SJGs; z7HRz@EG@^yP}8RowUCSL%95OXz`5EFr z$uWStNh56KGtzY5^!Q&AqPtwwMo+`S*_L!f=X(L=US3UC-H_yts!jyEGikI|03t{g z87{R&quL4oR+|Cbwph2j_Y*gn=8ITOMW$KH+t%Zy2=`_y_|R{-YV{>$JBu zq%q!P8q3rQeYL8fL9Nh!*o*Q^>Y3>C;yr0rcSI&D3tNL_z=8yD(Dj97iSi=iEu!ZkMs|u#X4kDc~!qdHI$P)-CQfQUbEi=C9Av_Ufvz)LMAnaYf zqixa@_X@g)myrl@YJ6ON52lhfy6>2 zpDD*=pRU|&!!W|xMZ2NIh@JkLcK&`yYog7Y!c`Z}NUf*GD{ETlwoIyDQK99!dlWSg z6}#3HBi!kbOj84@a675jC)blaP}n?;jeOwgMGme+(~=qjs(-1#v}aqEW%Gy(PTDUg znpeVdeU4Uz$V5yyn8KFtQZ#C3(aXVWqDd9?nhMm--)M=OtJ8adM0-{4gDOC)3v67s zwtBIYd)a?B#I>w))?bxnr+NDHqF4f7s}tl5BnS#+0-iGcs8f{)7>wOx(Q-FM6rGo6 zP!>QEKi+4@@sJL&0x=6}wCYB%Y!p5$ygt$?l6KZ3vGClotzj?W>=l&^U)OP%84*iK z>!lnXQvK-@7po$WbAcLeZ{(Z>07{gvH@FF}I?B9@0ruBLnrltL1cIxF~ z>ZX~9tOskBdkA41MSmQB3hFme#9re86U8G%*T$mfadS;%Fp?HDaR_0Uf43Fqfi1sr zi&ZB2dY8Z-*h(A3=rD1zA7{}85~nI;p%#I?*u2kleC3V09l3gNp(DKN3G%xrj+ExS zu0rIxJlB?Zel}PMa^Xm#qs*TW|D1nl#uokKaFc-+`F^lPD{IKsne02?^}m7PGBy|( z?ojTHtFk&0X-d6o-<%nY+})}Oip!8b%~t*(y2Cj?lvnp-a}S8@5(3QFM`pfMp(*ud zTqA5ZAOk(PM#Wh=7?wQSrH6aO-ZD>Q?M`HJ-C7+<P0&JI%O!|mx? zvAM7`IRy09nMuv-?uX4>hkukupazMA7B*3Y(Jlz=bC}JrESzf)u^8=bn(9HTH^!`U z>MPHcnnho};-B%@dJCWYjDQ^xUcqZPV;)piGI%;kPuehlO7nPw@$=W4-n$*Xj?o)e zB~ym~t>NcM_o_z!UsTf<*2d|eX4vX<%#4))-X>h&VCa4xEQ`mJkPf&cA0?=d0y2*OY-cH0XIn>|lI&gNKW zY63&7Aa$XL9?S&yc{tHpG(s?wMBQhHS5&FZKoyIIzrOM(E!BiXXe)eBAz8NS+$YCR zBV+TTXPqyday)V`E^osDXR_E3{+|dnE@_kPnsoDC0YEgI%nyY5gatJDi2$CtD>pI+ z_(vy;@yGQC(?8}{XxmT4^U}{y1Kg_HZ%;iPIUmm3=YD?Qo+x?_(zQLBxH;ERy;gi! zn<(7+VgVg}Q!zxccx>1pMTOpwOb~ImkD``4Be4};pFc^7d*Y!RKX2JW*^Lvivg{qu z_aGz13|7v)b926FUb5x3zKQS;Jr;YgYdLBWM~)B2^EX~f`=x5}cHXvoim`+9Z{Uy^ z^R~@qmlgaU&cg98r5~Tkp$7Avp-MKdiI3i-CyY4szjZ{rsQWJR@tpC#yv%}JxyNcy z2~2(vrZ~M%j^lryWSkSY;YM2G>Z*l-TQW(s6LkEEHd66+(mAg;T}?Soj-&qU{kd-C zQ4I6eX2PB3>5A5BspA`2e=*$?ws^+Lct*^=0`Qg=0WsL>^0iMU?#g<8PXr~Hxvh|h*zc15Ws1tk#aguFb4H?j{UFPHEv4T zKCR(5cl}`+dJ*j*_lb=n>G8JXLD)c;Eir2P)KqGXxV2X3;9E{kv(m(G%zG4IwN3dq z=Ka`n6h~FNmeqI7kq5CnzA)=39G0V}s|ty23TR8lTh({|l|13#@DI41SEcN2Oks}@ zB>8JqAcc)^eOEL2_`yJH&QRuMqf(7OL`y2IIuq%fau2+27gF}6M;t$kkDQA-wlo*U zs=y}3g@GUNUEY`o>iCV}_lFJNUBN5p%K0aC`2&Raw_ru_(Do6d@imy{!@w#sD8!)B zpr8Z@rum!3?BD!+KmuM#FK4Z!1vfBiVt215t&&M>TENcB#e>P`(XlAR+Z7M_(RWi1 zJVS;=W1H>NTeqM%v#D*Kp(5B5FM~2fVrlY5KSBJPu71+hOlK!L@()1e^QlR?ow&NY zC-*xwQ9rgeHw0E3w84GIt@aR(h|4aQ`MvzJ3|INr;VsAV1+_zID+$%OnlS?bMK4O) zX|z~i=k!``#YzfKzV}R*U7A1TD_0Mbf&ws3GSGK(brEleoBFxSV8rc~Ld;^G@6LE@ zbY8_{WM3j>@XIHlcot*b?gD#NeGw^lOS+!?92S|HN08-yA1lY&_?~3b#I$0xQl3*ILZU-+MkZK||_8QLH?pRM#N}ya9~UJT3W! z@4J-WLcB+d+w?!%ZVo1Wby1~!e`Rw~7|#@Q%Afl7Hw3;FM_k^W)v|itiPFTJMw7#G z$46eAGkDSlZI)1?4(L$AuD?24zJpDV=iB!JCwqKxb?`TD=4V1+TC!y^vC~vZlJVFh^`E7+)TLn=i57dFPwb}$xzM*8OcSmm zXLW<)gH7zEblWNw!aBtSLoV!5>rg7E26Hj3j3IRS(wfJImzGZABdrsQ&=trhkr%?W z*oAi8FpPnNAFDnSahl~czNnT4kCPR4q#qDl&2utswPAq3 zaa0{oG93^VoV^ui!*SpnU`45U?tV0dt#)Vn{b81Juub$I7CPf28K%}+t4sr%GO`HR zfnc_M4|bp5+G?PSW{>V%!O4LoK5Gp*T&R?48|h&WV^H!oiSjrm!5*W!?}~y1C1kse zsO;k`53O;Q7?&lW6GmnMJMU5=C&tEfa~zYwXBO!S!6I#ZVxlYOS0o7S(dsmp?)Zw2 zj!lMp8(de_xPo%(TP?~Ra}Ou!xVt3Lm`8z3ad-UJ`ex(sn{zk4m$I)g>qTz45M1Yx;HP%Jo&+hbs5?xk_-sl>ZMnFEm~d_y>rF zGhqG;t^d#G4#8l2DFgoRCcI1d9Ty-SjlfPGT(pm6;xk3WEKW2NDl5v2CBbmbx)z<8S^jcQBHL_|I8I(S=^40z2V$SfNQCmXrq6|4V=*Un>F%dJ9lVC4KetP5s=p@^K8_# zpLQ3nz-tivGwO8AsLNDFPs52t2qJRJ66RaKQ@nn|T0f?kKV5_v1a}{G_}u)@OK%$p zy?^I2gwCpxQG!BQ|79yQK@)0LoX!NFFu{K=reg+Q&6*)qwMYqx(tl3c_%Ni4M(OMJ zaDKMZ`X!9;9~%FAi2s?R$=`Mf)ht)-fBXxQ>j}l2{aZg`sV4d7nCgG$RMg)5Yr06_ z{%02b7orDSw_VeUpWpAJH-XXpG2Yqwi;VIKC(?V8A2KRAWDOM>6%9+_1(;+@gM753 ze|?1Cl^)dew`2zX?tAaKdG@7jyi0`!NMMB;#4u}vw%49Idb2d$+k$1~R&b9#8u}HH zfHPrOOy)`cG*{`rRH*k7sG&`JY1o=Sfrc*f9E!Xw3$YXwODkO1Iq}w^ZA`pKF;@$H z9c<_S{16H>*9T>*GhJ+vriD{jams&qFuLde9I}ujELqZF}hR{$u5vnPyedT=s zA}&q)^l8+V4@S^8R48Pq*}7$07pQ<(WV7yvcxBM@$9zB$U2HmaOiAXa08Qp*YMHtm zvvvdM+uuS>DK@2GxBu-RMLKB(*u{`kTga$3I}*lk;;6~<%Zh=Rn{93?UmiE}dIm70 zLX!*|)t76prZ?B!4OG_?RKhNvH2AomyM(--BuF`>Ci}!{}%jWkg*TF^3V4c%$_!9vlbt0@1^YdK`cfH+8(g3KHl!{!Ke2A_!%F)yA|Hx#?bQF`eVse{R(s0-hC?~%M z`fRnQh!4EmPw;_xl8~X>-AE0!HQCXy1C#!h&O%Er5bRpb0~83Wcv$#h%}pch^W?iK z`SCJD#;advi*MH~!mx)r9G3s|z&Uo^6IQcoap&q3?3?_HkWWn>ori2qUJBn5%#si- zRo7;S$5i!qEO>#_8AIj8pA7+2=-c2ku64@ts_b<|rqXd!yod{ftF2~p{WUUfXMr@H zpT4`@E&K}o997#h=I~cRV5=k8()iyHck#-|~KiVi9EXm6lHG_0+?Ool&n#4r=nht$tN+ z5WG8h5hw3KEuW$x0HeezuJ)c2QTGjv*nT4K=u0?bwzy)f840 zzC@zck&&Qti1m21USx;T9~=*XI@qW4952+{Tcy|9|64_?O>?pvvgo=h4f!o2SU4wU z@%;>N1`3QyKiL`H?T@|9{1E;X&%#=j%GH{%1Oa`=p8ju%k~)3~$YW*BHDI;{&KKX2 zB;=^#*aNN;+u@G>ob__m-?V3{0jE~;oSyC%eYVHXq(?tcBPtP2U&M3Ab^k^T0^hRpALZAeWMlI>lmSj~bymn{>w*#D%hA6i! zxJv)6{SlAhOX~}}faOHSy(r(k2<$>q)0lj9 z&&OYv9m|S7*m#RrRn91B>knT=870oqBp8-fRxjIU<^h3;h`PhndI7?Rj55j4c_Tgm z3%V?;^h#6P19;Jd9nwBN&$p5kB-gN3B%f8u(%l?`-;)2r$dzf$O7k)aySd15GWEFd zAm|&`&SKuI!pcP5u9{`I(aJ0dX_^4ASO+TK~*%HNbVB)Mmoy zS`u6s9q;Km}P^Q4FS_nYdc-n;EadKQvyDi0f5aEo|L~3OC{w>>%6R zykD;7=k$u(%Uu*sAze`n-A(Qt!Az1}O|M_V}k3a93D<3>md7RfRSM*b7sGTRED*=(l4%F)fUx;Frd0RaO`55O8VniV@Ly zrxu}zDXsdxgvc*{d3;>*AbF+2Wy1wSmiSpy64zG5dfF-Z9uIrT?SiTIfn-?q=JiC0 zgnLj6;0bJ#260|?pP+ViI|0FK&-%;li@p1XM2vSNt%5MNGs5eli*lLx$a34C6Bo;i z=wZ$V(-Y?U{09;P$nmtT_?bWY=svhy%&O{B-MiBLK6HTvpyN*r%6z%g@hHFy>wv|H z^m{=`{QqeC>!`XF{@WL>Kq<69ad&rjEAH-Iyf_s1Vx_pd6?b=ccXueoT^BAlw7bvl zet!F$_q=D^n?KhWD+wW4N#^{{&y1_W+@HaJ8FNXzWUxD-wWx8|JmEsEx{@dgoLYkj zy`Pjvp4-32lIIzR`9z>YMcR+TbNd5jw(b1v;r5J^8j^5ycFCsoxCwtixVlg+D|Jnf ztV=d2Znz>cBz2am$t{Sz;r%@y!u%w>vIv=9jO4kaC|&Xa;y8K%<}!Du1&`#C!Y18I zx8u?aVnF_@F1!)_#3b2*vGELpMMd+#@plt*8JqDFq~ofa&`c_4l!JF zcIq+F=URqAPf19>f*`i9V4GE_RI`f6Z+fulY0r~~>%;5?%n{1L7$HcOkqsCkgFGM1 zVND>HVGh&iGo?fOLQA3;qBr7z39n*a%ZvRq4{;;qHF%w__V7<^OiLH%`$Fh& zI}_wx1S)w7#@lp}FTu^H+hGhZiT#3}4;6hKp&m^{p1#aZ393o4^D2g}(l5?*>G8w_ zhL_9lR?%?W`(dQqj_V$OSc@Wxcm?u|urGqec$VGzo8NaIA=yp5tL{Y2Y3L(y|C+&K zBQK%#;M=`=%w*>fct!@%-@YnBmrMn9y{osYJn@72o5cbz1c+EIa#x0UpO8%}dN*Zx2rs)W{mQaRNYeUqGDumd_t@)oj_4j7 zq~&*^v6mhbS8J0%JwEA!)^xHob!81a;ry5UHu1SFfOIR@Ej(&Pn0A+y0eGqkM{p0Z z&AS^tt8NJ?IXuK7Zk!cS*XbIcMv4Vs@=>7X6!W(rQxDL`Z>u8ZZXqWY`?nWm4bxLA zuf3-Seb}$8zDXzYz3)98b$NO2u>)>hl88Xllty-QqsK)OLL`JvwBrpM5=f93i+hf< z0}5-h=4G>s8+g%TIfKj${osE6B0}FOZ#lS!(odE*X65RDfj@yEg5LB5^aeUphZy3v zs?_(oN%SrD1S~^S=!d!Ycf* zzK{~&PxUgs!VHso$UJoD`&>m#)2Hr~Am72Sf-ufTnSn+ru|K|K4i>v;uz>8}#J`f+(5?$#_`q+}!K*!*NTAAIc+^^gfaw4aY!X zTvHbmilRTLecC&P^2_i5roi^#m=5qx5}~i+)PmT~iSDe~s>~*rrW3m#8o?>xhedX& z+u2zNb^D3}r%bqW>q58ocPzYaR_g|{P!1#VGtT+*-2ttX=_korIoV z@sUH6mbuR!%1X9R!gR!z|86#Zh-jW-i;lca{z|5~ZbKQ=cTtSX0UXp&Yd);VZ%o%D zcM6ubwh>%jSISnR=*~1oC=qk9!wn)4ufD*qES=SYmH4{PdtW%Xd|Fi#dcD5jUs-Ppum(<0;J8T_WzCkUBcy~F;OCmyAW-XM`fve1|)a<(d ze2A)wj`iZXSrI$Q3iJ7>okHPu-1Y$^!m!Bm>?t&h{%p3G(vdq;VeOtN1sg%l${f*} zBwwL!`bLJ}#Rib(4-|G?=JKk#uZ2RyxePi0n!B6ZOLbhP3Uad- z3#+TCCC#+l_YMlwC(8}|8RZ$613e$@t0YC%jBJtzP?nCd=wE)JR7${?TYo=W=$o!_ z%(6FU)Z^Du@m>H;$J)|_RP3M|#&7{=GzOwCASmWbvWt!D<>%BwSE)N~&bQD8CtZ3H zIyNV}cPlT9_Eo%#a{ZPn({SQM_3E|C?bEgy~xLftN>dOEGi=Qy3> z8OBe~Tq$BovrghM?q#Iy7+H{BF2R(+kKro@$0Q+A0>wmQ(j7P z8T-{aTub!#`A7Xw%epscWe2Ut<``!2M?RWC44c1?K$>l2bx6Uy`}<2HqSV*#36)k zcRk6g$QVR;IR`$8CDD7xqCQ|3uD4UDMjVgou3h~suQxRZ1Pj^m;&6c5rlq#a>Jf70 zT|dd1q5KzchZ>quU>;-Uf}-~kz6ZzUrbgNA=*v3h*C6PLT1 zHYkgYUx)69b#gQp`C#l#tyk{`u5@R#8(ck4aY_;bN$;1}GZzzq7knj}H_(CVp?Xf1 z`5iD3nZw;MA#2r&k&$KhUs;g_GRB&lx#8ku0R1gROv`VrH%8IZ+&}Tl;k##ciYL`li}y8KVzN&}P{E*;JEHGJay7 zaNj58f$T|eSD`@Ziz2qlP|4+WQa>EWo$wr<{$gZdX-?;Rbw5ez@Qp(Pj41<~AEkyg;twV548c)}$`=OEo`u2Qaky1$Zv-Zv7bWz-1acX?foM)M zj<G`v-w)LSWhGzxbP$hRuk8nVo_O|muq>g*pSgWFp`0Vu#rfvX=_HCly5Nxi6O zw>nLMOGTI;b>P>(1=(lM-qR08yPjmrNF-k?eK>9sG&L2i@%hw8y51dOe<{<(yoAiC z9tvI*&&OM{sqZ;d+iTu7fsa2?6ie3@PE}xK8A3fb^c1Yh%F06E=!+`^5ACVY>w$s$ zYvwQrdM=pig9Gy~MLHWMuI9Hk{RE&$OG~q2rTk04{!EtveAGVo+fV3&{%s~aL-lt0 zkL>+{3X#IKpuKon4G@qA0hz1G1!vbJnWViH>@_(BkjRg=;K_RLkoKu)xk2ZsY;zhz z5H*Mnd>NlD@@4;06jTB@4EJXr8I+QO)m4P zjs+97%lj9CMe}q~o76vl8Cn8jb5#@X+nKZ0unlPH_?(WnR>b0bX=p#~HL*izpb3@@Z);hyJU=y6We3_nUr3Q) zc^J6jA~m=cxqRTYxBZq=TTM<_WDA=1*xkJe4=0R&K3Y{}fk;F)fMa{4?9j_&{@gb{krj z@v`BjG}_qxZ(Ta-fp%&c<?*Hu>g z==RnAnBdTd!%d2b-Qqzt*O4Rm)3Hacn}(zNgF>$`B##%FL7Vwr~ZJS1mnBWiX5S< zZ89GSw{F^~&~+S02V{Z;W=@KBToOcsY1hD_F{br_=rSs*+fy)pU`0^&1VWe2_t2s}f?dZV1}L652l=xrHxmb%Mc zn#+gFD^HsBbmqrT;`zwky7#vYE66%w{xuD-1rbTH#C-$0WeC7$D63@rx%a}E*$e|D zrHOGfI zvFCoBm;rv?{}tp1^LCu!!=U!o)4!#Tr9z!|Y7gYhnS#Y(|MghlyDhu8xOk7p0Jwc5 z&#(||4KQ@&1}_seAM7$st#8eHpgy)grS45Zeq~KLS^SvPF%es~4!=>I?xkva{(Ur6 zif|+Bj$)*3>z$#N*aJG?@klm1Ca8Rz_jOhH6(DoP=cp0ujQmIciP8f58WHHAD?AS~$NAEkRNajv}X52Et{V%vdg znMS=d`AGB`-yFTl4|rrbio%2nJ!+=ql3bzk!l~cEBp2Av`_-Ge4>I6&$gOzn6PAW~ z-%l$5+G?Wf{lbfaYD^#y*1w<4!LGysQDeJkzfvkixjdaQfQ-FoUO0zB<8KOD=1>~^ z(Ni`W3QB!M+UWkm^{7yPZ;1^lrp)tjG{rZWDF2^54Qfdm?+m8obJ<#U&$>xM z&hfV%Qx7}tsgit`ezwP4+XmJe3$LV&kXVd<#6MJW zOpbC|%*HA>HL$)kaZ*As^jt<5*6Q%zJg|QgA`;=waZ=b*vIV;3KJoY)Vf2)<9ZrOZg9A-eW?ONy<0rWZCc#vzD{PbH z`E{`aSdWEL;W#Lt3|+~!?!+s)@C5$T5@1xDZAu;CuQ}SB2KJq6+5-035=~#PhY+d^ z4s|vADCNa6@rglFmE~F;=kyLjV|SnZ+&dF-{O5E<(dFOnf$t;ktcwZ@{)~1UI*`%34;%MpLf{U-p=YWT%EX3QQ5YM55>y%?+OfV`nLFr!Hm(rCTS zTJYdChwmj5CEg+w`r{?d?!ognZ12!ab^IK*$#AJO&}gx=a?O?_U5$vAoDC>&P%aHs zH;OP4yZBps8_3p*jKbf`niPnzQtB17`3a=ra#JL8{LrNR^iUVj2WcsQ^@x60BN9gE zmo61XeHg`&dP@+oQ9~-_LMkLZuO5k!tJ@Xe3!k_u0BK@EDk`C#@5PeH4Pp01?1k## zyo~z5k86Kp4a^Uk0hD9!p$Aew7<|);lY4DQ5m=B5`4-uTHnPL(i1Us{h_ms*l3GN{ zm|Ao*sIrti%>um+&#jaSms4(AnrfZ%wj^y30V9S|7QBi>2EhSLZs|h=9hLVcXGa2_ zeJ)oDd(Brh$(1NoQDeV| zplNd%V?yPnX$r0&W75Nt&pM!ltZmIhL8#Z5J`uTV)gP%n z80H_Fe%yX8FB3EF7?D)n?Tro!JN=ELUb&0fixhgC5in7D+~^tLx_qBjh~#ewNciBt zkU60ib|PBvQ<*Qj1(f;mroWW=l$C!d^N2nFQ<+EP_=hr2^FNgNz=!t|RhQu2yMtz% z@v3g`#^UC&HXF~V)5Xr464=+nzNa729`G=iUM`#Z97%>%bzRRCNw@~Jl-}Jrq&_%( z2X3bBo=11eI3MdTcP@9XIPHQNz5dkZ-~9N0Pn)-dNM(PY@18G=?`OdXN+voiY^`<= z7;~W>8`+;WgOzO@=@EM6n2>4tE(s7|onAPLonwB0xPe96`Ii|NPcXj4K53KtQe|0Ean8M=MLy<6`1Cm# zqmb}+eKcF&0iEkQmY2yB#@ysO<8?*#Z1#npPrSfYf_r=Hvme8>5X?lF#q^uF@NqM` zl^>KR z9yECG(~L4#bbLu+|LF`HP1D>d%kAtLo^g?4)0>GzE|=|<@pyKM-dFLdmF&`?EdpBU z9yxWLJ@0=pq?~yWQr^Svm|M1xB2uusA4%?<({jm!0W+;#n1c5ENILyGJ>L9faRWoP z9D)*pOOIQ^?jTVO7>~J=rH8b*b&#YoqLbs^>VLR(-zv_<)^V_x@{=RdcqFne!G&W& zG4;^>pOk)ERMQ{cuq8a%Hl&CG%6esJ%9_r2@gFa!dBf0)HjeurI?j)R;`v4%oHsOL_)ULkUW@Iuj1d**N|duu zenJJmI5&>|0avJy&v+i!=jp|4ssmB4JlQm}Ntj zxaVk=Tzg=e7_%(Cwnwa7kfyvbJe+GC(sONju-up0PbSel63)x31=1 zzL;8W`wb50Crh^v_f465Y9bLF1YIrA7)4(Mh<)6J!5L~!8&NUcg)z}0rO`yhm2Nv4 zvn_PqmSWah%HDQ3eIciOyeVdbF&zAF6M(R5wrKMs;^}+oc3e9B<2ZM--OkS^Hg<*Y z65JNiL~HT7W@w2dm%6~J*1HcMpj|>wT3qUJbZy@DHz%wIMK$gl|1UO$?+mUb z6-^Tk#<_5AoZ*QjE%aT-VT9omRM`XM7{u)kzI*J5%eiAN=aA{#=y};TFo?%z^b%d} zi>-gF{~9)yQSW=4my6dl!DgJ4N?Vm5G>nCmJ$Z}oF{(qK2E&cHJ(2zHT{-9Z^0Rww zXWqrV%}#O4j{(gODkJ`S{wH#xH{ZS2e)MhF46pD?#@wzi6mriOm%j>GaDG8_#>hnS62eL*06{aE7d9lxv16j-dP^VwVN) zB(?|L70A3O7i3M_Du*w&SJziz1FZa;EN4RH35_zC`VJ0~b4zpZgs)PoMC z9D;)}>1XhhgCNUwgP_=;G&n=ewK=Xl=ZcK9UU^RkWw_s#okAPfB-b&FC*^nDTo2tL zG`qKdzL=@b*KAo2$MPAHvGLv&h_aF>7c8HE!#NMjBB>jb79;o>pK-CE9`Ir+TI;wN^ zU#7xHzi;RNg{x3|sikTV93Lx_6gWjSV`Hl@V70g5VQFYYB>^0DMtJXm=O@QAH56Qs zn5xvG;V;y`@zoFu-F*^@R15m9Riwaq-|QyNoWExrG(?@k7HK5}MO!p%CC@xm=Hs@h z&2htCoSi_io6aR!?~BVwAG4bLyt;YfR6Q~q-)dI6&lEzHK;s~GCB13C=$FyZru4rv z8fK1X^;SBQ-X)SU7-c3dzRB8tSlJ%>yy~epN39d(Mc=Svzl1J!_X(z_5WZmqVw0i^ znRh%|XIRMYK1=_SkYlns+d_%GKIm;v)1YPnwWd81Pj%l;WY5M^CXP16lYC$)=Vp@0 z+!nHV@f&r1ir{_{vAg^S+GPn-r zDetb|=cpf=Z*KOa3QJH$V)i^ZJ|0olMooR-AdhuCT1u0hlMMmjYYp?nnI{!>!yADX zS1|F<`H?9k;mAJWnoaY`QeP#4_SbegR#b7*gT7%L_#b#13@EFaofH8mGme;oqr#-d z2YNMh`$NeAw`?Ag2Z(AN?}32&q z_9gFn$jel|f^e8#Hmg;j7s0nEdAQzhE>R`sUlf9fVtEI%26j9nhX1xJG`!ddsCY(d zbRpHzQ|s`EhKaHyBMeJ-T134h!s_2QAH_7eSf)KzO`R|h&)#>YM882%F03`R)R?_b z;~#JDoWRpVW6eir?|>Ug9i=|8KkhS+=21(#^^a^$eI^_OTV<9Y-bHL~7s#&8q6dR! zig&<`spb+-Lj@5WD!!8y`NdQZ^dtKf%!FsW+C8w8RCG;L7d1V9BO? zmuF6TMdLQ`nD=vZ-^l2=^dSTl%H2|XqeYqFGVQlxaZ#XJeG_uX^g~5ZB2$S$#>D%Q zai?w_K$C7S}_>E;93U9K&H_ z|DF3ym*jnlAI0YK{ZO4lAZc`((cV>ey%|iwW%2krm~M(G^QYcf9N5<*)|cq zHoZk!_wZi}<+@)eXm;TL1S8O2{2EP`#E8Tn->t>>?f#AnsJQNpg;uAbdt~VZwFMqT zLshNSgMn+`VToq*`|D8UA;C@|G{B&uzy$@OhLq%STqjSZOi@U8{Fq)-Sx$6i4KHeO z-HZ4(M-HiX>uX7gijEHD*MU!qcid*c$5J>LKIes4b$~ZvK+3>}wpdMOJoKg-RnS!` z5o@-wrfWAH*}8m4=kEKkh(I|8{FC1IVgX)VC1RfBf(@H%ouI*DA?j-ReZY>n^xMGC z0Ymh?n$ihQq^gj{AcF{;M$DC;?vGh9VQ%vAXi42uk9erk`Z*RdUE#!qGmkSDRap$7 z-bl&~F%M&wa;|F!m&rP}*}8gWD)^!D`X`NVc2F$j3FM0HbdzDmUQ{nK3=HWra|y;Y zgqkVLRQh+sEE+F-eB>y@mG_8aKYBCHWZN%$6!OtjluVjrlVaksf5Gs ztA5V%JE5McT_{28*?ztmuH=Q*7s4&n@4_F6^qsa@JYzm!Hzg1pxg}UmB{8_>`p_mG z6p={6T$r33nwHpLt^PDWUe_z8_D|k}_6Q7o0dGOSo@FW?DW0^sDe3t$eZEmi^YT&FhDT)2s0M3X>#dXV_>z>$t2L^m-3WU*zNE=JyNcHz;jC zo<__+`x~&>Amny{$`i|j&yls~2|Fjlk2iPgi z7Cc+1ZCWx35DWu#DH=*0m7|(~em#hun6=y>ddP*L!Rd|fvrVaAJ_aqfK1=oI-^7JD zc3_hH54WR>#q#+c{a<7D+#h28qU-%RJs)5x34}79mZuQE-kSh=7m!jl4xuNsG>5<0Aj89nCigKWc5QF>u(1J z4G3vymDHrA@%|p_k4#8U`t8G@LPQk${wMQXoQ^tz_aMS{rrq%?(ctl-tv$NHNS*lC z&OBQe{ulOc)OQ}Dc&s=QV308d^0g{Ayx9UbX*7^?Ia+FP*xQ}^0hH1_Ev>l`|ASH( z@eM3B_>P5xRvOS)I1yeib2F3vrV8o_${ucLEX-HguYSMiUZ}>@(F4E-W78yi{YuT8Fl|7VLq zo+SSg&-feu4&)PCepLJ3BGgVN$8bzp0LAgC=)) zkZ<3? zDaw;WhQ;9{qiZ7Tex0+h6<*|%O-dxcTL)a>Y+*~Yz{xr469<(vD!f}TF&gzNy7{IlC%DbZSAJ${9mWf3nxn1V6_N3;$ryXcm zhL1HF?g1el3Ut^D!_;gptZl5grhqXszA|8LjsyCkChSfb8#kkDjCC* zd4C);9lVBR1@#H9hSP#mDEytIt!J2ijMgUU+|La8Kqz-?w{(-WZ~?0`uVfO$2J3mvVY$u9-n% zcQN6;VjgWe0LHuvAnU6YGa~6YkJ`_1)D4*<+X-*9zYAm{%h<6a{AXPmNGh$UTrW?a z!vDV-%XFqy{U)idi;-0;e|!y+HYwvUAfLkue#%^IgOFSt`(B zxXc#y%NEz19*@5XkPfmNmMa%S$>(1J>v#9!`8`-a(IUnyDo25i8cWvL>V@uzl9Pfq zENMC>qqubXpLyk`?J-*m*l}+ zRjGwxnAtLZ39*(n-oR_htC&}W;S>NRjsi7swzsz?)r5>R#vjb|uMEiL!T*04kdPyG zMwKjsQFz4kHH=|#Bpvwu!uWT}SpTbjvg@AzqCfuc`pFbuOdoul&C28G6irSF*R)q(^eOC5fCbm1S?*D9Knqf*@iL5k#f zAcdK^3CEd@w#YOolZIN^wt!x!7KIshibTkfdkoYm2Gq18xX(m%!|#6eAy5)p<)vj2}NvL*S>zPHb4!FvJvK-kjU zHaM>sd{fsIMtBqLVM{6o{6UkHoE#HBK9X~!iRGKWnUDsqA$H}M^phQ0WBW*PKBC-r z21n)}vHkiI&c!xgq5f_o^Kaa#_suUPR7~>qA@1mvFD1{snT(uAnW*>f^8QVOq%3zymL*_$rD)8M_}Nm0qn@L-UzjCOhJVTzYTuSb&l)`|4BH~}?GRWn;NPZl=Ai-=7^~*5uQKo33MIk66l4n66N7zniDtOveEPXT zJ3u*b3_rb@7NH|7^v`>$V}q>7axo(&*w3l#ro<@^Abp}iHvOVOa#_)UyW1V?fxrMJ zFV_b#?hfhMW5Jql?V-CWHHim(RIL| zOIrk_9rH!s&*6GaX0Z$m#!Pg5!7lj58SfxxNpuh>O>z?6aC;yPFbsjZDmmf}dd*)-aQ>po{X-eX#ls-y;T@sS zVQ;%{$8hNFt=uvs#H-})_b6Cy(xJ6_-VIu=2O0HL8Tr;8H@4*{lq0n0yAn50ac)h8 z1AX;c-Ne&vnxUA>uh{*(46Bru6kc%mwMq&qX%1GM0v%%Y)S{!SMRG@P{{%(yIuHvQ z%w!Xn-fASf=!34O8$ax$bWl^hGg6II6ddgQIL%_FIQozBurf%H)xSU^>!%!)MkB$H^)9IU;7iopwaD1895p8K&G=f8bQxK7p8Y$Rw9@(BrRA zrjnB$2^*19?h6*#x=+p=>B0z6J9kI$i@^k=aN;yG?#=H8?Z2g-O%6}mHE2mf&unF6 z85LxO-fKE}ksCZawzh77Te(>48>UhpD;>+2ujgnqT1DJCz@OuAKDl6AwVJZbjWZ{m z-=DRaP{t8&*xSvL9uAXs6yerP{4N*AP`rE-mx60j{GL0fV>R~&RT1B%bALTJG85C@ zI9T!&#tBodk%6m#{uo%SXbZ?ZMD#!v6LzoTIe^kK|eCcXCVC? zhTHGmOVp}Nbe0NH3&Qvhvq75Lnyeof@@jBAP%|AvczTr!dm<(>q95$-A$aBm{HM?> zhxnuX$Z!0j+5^}$pS*N~nyH`yXF08%H9b;&qANZv-1^EQ^9hq($A_miea3dgCkop* z%rKv6S#&-SzvQO<8ARuvtN9XfuVYm68ifQwJJ6S+kM&&-_+Ui7@401eli^spU==WZ z6_1Nx9rRNJnUb3V+@(J>kawlXt)1`oV6jjs3kvI0eBLDfVn(cq4gdfIkUCo5)8^y6 z5Qh%U{;QrV{wY=r=!mdQQJ;VKr4(_@ljtH^4k)rHxmO+ycie`Yf=#o1{8fjh;Rc)S z>acZ^1*4-&=7&RjM_WQ#L0!AH8Xy-gLGjrQpcR(UpP#235rq($q~n!=*Y`V#BzD`EfSheNTiUF0{Qhdtbwe9#0%wOP3~G_Yw=i0({P|t z1b7z2u1M$8z(;;_0=#p~6_8OS&&gNi^ljlPHlI`N2w z^i$`ji>nO$y$r;Kc!`*5irMxVYhEEm2*8RCJD#K;S^6Xm(3TPI z(EXWmUA_84u9AK+t~x?y_cVK62U5G?sI>g7E7n-iHNxD>;1`5|F`LWRA-Tm0AN#F^ z2Luz4Y8R&{|C5FXVR&%uBKF?_z)60S=Z@S8DLj@ESSr1vi+_!71p9k&)Kax( zLs`x9)6t`zOM#G(knW?>xz?=|u3VAQE`SkRcXsN>ClsE!K?h8l{qGE}#r3rBB%<{j z37cL*(yj%*YM1`l_*2mZX#xlNHX10M0Hhdf7fZTbD#&= zVJ14ox7j!u5occ7U?5M5EFRxwD%(3(P;on8HTl^6aiuO*f+ zyIt*x^pXN8^aa!}^SG1CS}qOXGgs@kG5z#dBV-=lmgN$YMLo4-6@p?F;g1e(AMxoZ-W$?3_hMQ^rS+ip;VU!e1beMF@VvXFR<4 zzIccOxGSc*$37rFX4GSIG(2F4O0evj@9~vwa8Q0qBkeo-!9rLW)7<5L4os%B)`zpW z%R+iQ9^+_yx&nRE~{(eY!i{n7eSpDZRHyj z6Ejd)oW$8grDzxH}L_zP7a)BvaX^sH%tb;GR-d{Ugu`6(~|ePVF0L5<090~o~A^z1SlUM zogAEO_8cGDa3oVw#!y+7gjwQsj`m{bT}DJWRrjFnoW?evRvy1t4s9_s(eK6Dk>#GVv+eCK=cUG((TsRcPGabdtz6d3>m)gU&Zw&h z(pCRlRWLrb`ZEN9E>$V(%(P?4ji21ELBfvJ<1FfhsCaAms96;J+Vu8SfZiN)hPe^= zlA^~54)CvlmPR;0Jf73pG?C6U6K63V%~l*OW3KRe)NiYW3>Z22pgv~6cGEUC;`Fua z#s)j#=_{?7V5#uD_Nevq6lI(6GS>|<#~HmyJ{ln-FHIYlfr zg2##hOruq60olcYgcs0LQ^0n(Q`JKnnQ8F@S0*n%>b<=6w-R6Veo64dFG(ip!N6`p zYvFE!!~rCf?q7fiPUz|B`J4f0idSfrZr{TR?l*i)R(#G|4L z2Q8S6zm)mK8BVcg*g!_x%>l&bR|UpqtC!Pr6pr6TG!BK-;q+#@=6ahRFMS}#%Xej3 zfh^DuHZ3Y{8Bxz?;^3@TiBCQ%f()bqc&nJCA!Xo#)=EWrvFr;#Z%}h}b%@O5&KpV_ ziXR0$J1T(kA5zVHu@}-jp5M(00j7anBOjov?C<&&8f2SG#kc6;QCc$_RLqIzl2=!j zKN;@qAuD|WF+Ih>54mb9bS0g!92^LizVY&fYeoRVNd&N%(g<3L{+*-biy9P?bL79`v_mzWS2>X&Cr_z;R ze;?8tds{KkbWwNzJ4y5$wj2bF*zz+V%=8le0|kNovkx3N7MnUWLx2 zc7<^1mu4=@tD(D9`zT3@y+<489q$lsLSFh@FeP6*i&Wp?B%=)c@{-yQlXl-DYn<)f zZH&#>`_8HBt99L7TNvZgwS5+h3Kejk(zavedZ0kj*m-ODFc;ZS8%XYPTWxum$)Yi- zs6B#t(-H2SR=VDtUg)vqhBbnPiR#~y85v2};^BgpoxKk!o>E_b{mI8A>;J$aD7D8j zEFPJ71=B5_<={Q-l`p5xa*Bu)LyZ`-H8azb>`Xe zpF%I6RlT;tRyJF+Kq1TqKiD2!{I=d6BbO3Vo_plI0tLR^yyA+Sli~qy68_{#d+Q7@ zq;Mqnykfh>W{Q%=aWoy!xbD@)paeLdxQ+Ao50;avJ24{XYCNmeKJQ*@7WU&|bMICa zUtRf1wrx4z5QdH@yV=>{J4$K_R$gT{J%9EY&mUI2>xGMXSP0TC672mdZ4)A6Dzvk8 zxhMp2=ND&omrh$S-)nan=Jqo3e|G_z;QDs-eHEm`@tc0E#bfSdGG<@bYN&PTr!XE= zv5@CJ!2fE5vae^1MePX4Tq7+qF|$yLjaDnEe@@~wc`Ow4F@yyuq%!hRleIw^mM=vw zCbuUaA@_4OpfrMEO&%MzAF^4}T*-7UUfAnazz$W%S3N!zU_Y8kN|rE1fsr?7baL9h z6NyVs$$OHuq=JNc7iFaN>q0#XQETNOhLG{h@{u*jKHEHW1I1nQ?Mj zS}abLzsqaiP?ldAFWJgch?OU=u|WWm%}v{#B>1cQDFo`w`&akVxqh^JVosRHV^M){_+#NAq23FfxZ_VHeW zj>tK~Ub~7;F$*ato@Pmp0ZoS|i^=d>hU;e!)St>f=sd4}P1?c>l$-jM31aU>VXPMP zP#uO1rAbdWmBNsW=6)c?o40$1`YCu6$i`6;P9_#sd(Ez)$X{yegwm??U>vgSz$=JI zJmZ*ubaK14!(B)&7m;RWwIIR3MuJ+sh!$0PB{j;Lv65FKIAc-+Li*)_sg{7)u?I?) z7}ope7i`0)2yhOoqueWMY4VeWrSUYclxU#4B{cIAJW#M{+bYp@%C2jUvnQ!|8$X1n+}1o6zcLas}TX_Eqk!%uTrR>YndE};k`ggd_!b(?IQGvD49U( z2>g@CKU+6M1=a0Nw?F>EI)+JAu~Q<*(5*uW92(~hvPxAH_^V)6dsv)A8CatW&z?28 zqL)l6KZ-9K*1Emo;J=wB-(+FitE<>E0J8-ZHXfCo%GyY&HXnS8Q%my@1GI3(XAG?e zo?4;4+x&QJ|B^E1o;tB=rNG3j?9aN1hO;Pfqi~v#wxmjE&=Fa|kp?ZtfWTmBW;{9`L`0w;~RRZ?@Lps!{A1zBK z>WX=r-u4w`^x(*>glAIOeOU~K9@453;o{=w7S|+V!B4()i1>e~d&{u6wsl*Vgb)%e z!Gi|~uECuIcXxMpcY?dSySoN=cXxMpD4a9MoNKPN@80L$`}3UoRS#7~RrNk1nlbIZ|}>g!vyp9F8oP(5rVO6gw ziZOnxHn_XqiA(RS+Dz2KWH=$3(ev#CKc4XX1^-VhB{c|9?vP^T)scaxVk8 zQJybI*ua9PHxvuw%a_jpuE_#iM2eB}vm^uzRS9iZ>oJVO1R(s#(?am`??v-CI$h4(sTStA0jr+BWdqR-YhZVE4Lz1ucyBz#?QU`2T>e<^v_;J+7H0e zeq8-K1>p}S2=~80n^1|OvQX}5fuzu8XAr<{3jAyL=1=QW3{i?XdXZ>W{2D-&n3hNP zI4Tk%7p*wSj#`0VX6`g@Hc88D6u}ut(+C*5uqcWPQzV#y{9T=D>PB6lPcN#7SD>%x zx83;y04ZAj)Afaf|3$eE9dJ95x+G&=`-q8-F|Y$Q>z9z7SeNR@fIX+Aq;~(%5DK*q|bVxvx z)&B=~iBgO(8%2fHo{5yym=Z-2UAWK%u6OPE1b?%r24}v~=v5EEXy9?c`xlJ4JhFI! zW(47jasK%4;=-K5>1sdXx^knBVXrNSKi2=+RQ`BwT5fLTL?HtrqtLj&hA^WXuC6}S zeh~i-G`FYDQG^mvyoQ{c>-xRcmD0zE6Q583Y?e#CI{My+6>p$Yzo4jV zlDz5u#I9^#E#5`E5?f?e{0QVX`GK&uKNee$Nkk{kQ`pzvV~gCKjBE;Hb#zI=acbOZ zs!8gpTJ&}z=invCfjs~*p#SiSrox7s_}Dlgj!B!s|K=6R1!s%UOn2m_=TbR8N7f6@ z#>N2w;Z3H4@DU1prMusZGbyNygq$Yq6U_pkDe|j8I zzLB8)r=oxnKld)XDpmSG0*>>{>aga@-e481JK8}%D9ton9}bO(pRs)=GFNOP!&KqO)ngYOB^+H8 z7Q$iPk>c|^GVpnD5+8|P0-KL6`+&f(tR~$>K|PEstxUXvQb};d9$2FXq*~^e7`F09 z)H%VGg9il=%uI`vTj<_n+P~IDkO%>jpJdDJJZftjTy7d@HEq{YP{K1Z66Q|=x@XwU zasjhsvfKG=_Lr37f{_C-ThTT)h=Q4A3121Y557iNYCa+7VAQg>P;lq1;YoiY`y}_KHHyq8 zZgl~{dS7d!F5>6u#a;LOrjbEhNZv6-Qc8K~d5JLK9`|JI8(iLG9g+vM9kWMs1q9b!f zv|15fw#vi<t1z5VFOg=m1YP_GB%)S0=J{Q46!&`jT?KPR<&e79;od zQ~X%hpl7ZelU#Po&d^?h#7_ZYHWB#sbOvyhfwv#lCc^O^wm416hCaZGvdMOO4#p`g zYhsccf3Tk?cy;gP0`9$yw6kl(_jFcsK|khTvLk7UGRfI@qEbR{)BV1|Vq?fQ@&T~= z0&tTqytw(!SX10VeoizkGOcq(*!xyawg&o=#W2S(gtu&GGH`=@uk}w2uGe;~R~QUU z`YQ}(hZ*k0~0$+}BsD!n*K9*X)`cz7wU)q@tBDazlw;{b`blUa?yyqm5qE0=hUri+6n-eJW@AxxtfOqm2HOiG<8!Ncis~zpF@|eJHU#Ul{ ze(3KBo%%n^dBbgV6)GiF-3__yUBz&c5&_i7>?d9ar>%sErxq!je3~xq_4XF+AY}DNJ|DO_syXUkY@y~rS7NV427;1^E)=T3QIA^iB%)>o)psOfwamfbXp3C` zC>P;6J=nYy2s+PFe@0aaX?~7t08*0r`OJKJdn8VZ525Z56`h#zk6>{hl6g!$V%qL! zJdm9&T33a)>HqolWl;a4Pyy42;@A0`CcIkG3$~|#qO0XIU`7BIKsU9?yTt043JDwY zua<>hZ;1e8N!je-xr5!&9B#+vhaMEDceI=ic)H3g8aPknK?*MnNr+#1009;T_9;1} zcx=>BAT8@vZurIZe)aB$-Ix3Jza$F{(k+pDa%k3IW#uzhTt3~A9uHA!u%VrnH z#=idjmtTUg|9*>qs{;Swr1-az;Prp0YN7Lc6^Q;Kr=WOksL=%b1@~Hj|0okfgU^P7 z0R_Pb;?sTp(|7aZJ0Q#uV*KY$MvXYoSy8%qHiAVBvPx4cgMVo#f!5i{iRTM2S77rN zJ@vb(WN_`-Yq82^ccU+lqtDN7@b5SMCO1LE%}MSzTcv7P@Q!ZWowg(EsUKi`1J*7E zsXz};Q=jo(*XX}Y+L|E1_#hQSk6?VisefIw4D9h6mAq~6?Vzxz zNT4}?W?1Ts*{@g0to}QPDNBP)zR2yIr}O?o2a;+#3h+68c!a0a0I(Xtn2&&KkS#P{ zY~91xC_@R2^gOV!rM{wl*P73{Q5tJZd%mY^in)+XX0V((>UOso zm%;X>HUvg&r3w;QemXT-T`FiG|LB{-L8>-|NKN<6A4wUHmxQ@~+SG z?&tfCW_IrHzkBgLyTOyNe+(blIMW!eY<{FQICRYvhV#RZAbyb%&dK@VLO1d%87ZSP zTV-4GtCP?C#^>Ff-o_T9QPzRS`D@NFpkhKI{JvHs)5N5A7LJEsM7-v+)Z)NWA`Ya za;IuU)GKBhEhH}40w^B0`472jthY6^NOy&Z^xrid(pwKqK(ZzXDAPJDF2+$j_`K0T(FZou?1 zBSR~+JLujK#Ew?ZUnSz=H;K0AZM`6!&-HN3O){z3d|LM_=%Rg1djqym0u@U z82$E8eBB@<#?8U zfiAW2oqrqmxXf2Zzq69<6hccMYRJ6a*Dj%8r7k(sdf5`XyGM~n`opk)F6US`KCzOB z-0|RGXE4G$@dU>7dB3%imoY0NM2g`+E!Av^?)x`%h==Q%6t3n`uZHL0qtNTG<+$4Nm4w${ZPWU(*|W;LuWZ9 zw5Tt#S6%#GJFl=Hem9p$7MC(jJ#WUFCL43dVK*AYX{dr7wKFE8ruK`B!_6ZyLIxPB zrQ;;Se1_~{&B|cAm6xfXslQ4GYw%!2Q}A*qxhAgCQq&M&I|ZNxm;jqfq1wQhIDbVQ zy~zr_o~h9c_t1h9lk_S5paebg9?Hkcn`1P)`z4`MJ&v$s5uaaF1A{#ap)lX^<6NPQ ze|B>9%SA^P($A)@^gz!LjjAiDTSn%IFbhMcVl&NQ*vd5!G*r~oi(efkGZ$w)ZxpP8 zt^`w!5wK!JAPW1ejl^|rRTA?*q}Xo9$g|)V-}`n8w`byc7jB=1ahfg-HW3wo=|NPL z0yDcMt>A8Xj2a@F8TgA4rLlUbNT5w2jZn<=AL)NNTx?%W zw5pcB%#sq1$lq(Cw91Z}h~LS&^o?_zI9ja<1tj7fJ;I*%R0XBkU50yynGJ3g&}W;P zq;-=y!-&+SK^r%91f=#n6tdZu&seVCV-r94U8izm3!#3z)bZ9dLkV}8nW8N)d`L+c zWUzifu(^LKqzXN=%yg0vH5FDaaoFH`SLVD5_nnhxFs<`(PeiB#Hz}E9KRag<-mvSy z<97Ii5$Q=h&)Gh40D%gH87zi<2yas2&F8Axk#FLYFw^s2oAL_HR0E!SNXP1<;wV<^ zo+Z;rzYY@FLa_F8#O$5;6bsl)MNLV@@bJERF-RxwRN^Iq>w2dubr$_Fesr(|DP#rUt( z6du+%% zxwe%7+#6}f%5a8RuM}AiTW^7(V8m;Ft|Ib}(L$BvlQI!h^*Z~VoYnS1*U`~V0!ok- zzfKUO_I&b(&0!nuLmZP3eA&B-OOE>{>jn#|I_r%DKgM5oY-cFJ3ubEKX4HgI+AQZ7 zZ$czl`}$*1$iT7|;B^v-_e`7fU4$gCmPR5lCyhjNcPpw3(#0-fwPH6`(^}H5sBL40 zKmq6cFF1gWc`Qpl;9D5KDi>dqA9|6{ba*)dV9#iv{r$uL;S?2TLh6t9F@av?(x;KB zq3_*nFPlTgpX3p~Z6Ho$Slx4(ye~Ss%<1r^nreWV7Blor{Ncb!n)wU#M^?Wz9D3SF zee1cH+QJbz`tsCWi|G5u!XJKqDbJ)VK3#)kgS{-k4s6BuJ!4S$*5s#xsHo<#rL}2} z_lw^$XqA`H9hc}agRr$`W@awvM#E+Ohd4;}t*yZmm>87ZtJ6I#PthgINoYfu@&akO z`Hjg(mG651EvE|T;BLE>j-Gj&;fjZ!Bh=(B!{X#S=SgE~njjnKEvFkWtmFL@jqaPL zGvHTUS{s_0FTIze{r2*!pq9!X1BZj)S=2i0GrzCkzaqn|N%kFaze7(oT>Y6@0(&;`ZM(Nm?DR-V# zs4n3BRAFK)?U)F7JwiXkgrL$pijdU1DiWh|(}82(1h6$aduSL{F;)Mx;+>1T{hlu{ z5YEeIm0ve`Na;A3UQyXh2V5e!@K}x853d>M==|H5A~%{v+nsO<)b2_O+ilSMOWeS6 zKfi?Qt$j87qpa}_kQ|`t-TA9C9}N#)Phe_B2_<~Ke!gL`DP&Nc4JAU@?_KxbDn%_7 zl~H(7ti;SwVs4T(YSb-o*!Df%v2ehs(;x|1?L`ukvN#rY&ew+|S$vGQF*79uyXY68Z~`-Ji^GU#WtekK|3-nW=39`8J(jwR zP2F#Kk}xVk9XV7M!Y7rRDb+lbMYNSi9d~BsO#0M|&1d+$QPKIM1JPn1t%xM-89c%u z@5F75D(1a8_95RlGVM;n9jtedlB^SYabGN_r|Z=o5*nt2&C446b&Y6z8;mAOiPVMf zVoEuBS^ZSG{ALL*ZT)yMnS5Q72B@uW9({dpNi$8lzix&C>tBUOotaz|cJC1(^TXf^ zx?u|)C7)GfIaE|Ur;f-5B?M&yVsuTi2?qb|ye65;v{UrY^ZVrrhym3zwjTEf2S(-v z+^Vz**HO{k?Aw#zPnElgEVj0i_BQSUq(QS+S8eV2mx+g^jfrA5{H%Fy)pLw-(lJQKPB*D1 z`|#_Y9YLiC3$Ss-N|6C1ac;JSz z5sKn0RKFcOI`HWyuBFFdvE-+d+zO{H$osnoXFb#BK+ILtt<>THOc`nhYd+JMoV487 zU32*YIJhbas{B3@Qworg{*d;F=v49N@+`ZF;vi$|FoE51l{{rQEB0LHJkI#9=r7S-(L8Qj; zdXjj$GKbe#Oozmvbd16RWUq#8XU6Ct!XVhAWdOLgT<{xVJca_Jwd(9ld)DkFY^#B+ z3gzY0z2&~cA#MgSsQIPwXON~RbVnexV`vwG{n`LB7c>nFh%w=Uz`zjw zGF28FW>r}HeQL`Jlt*9Q+8?pEyT2y|f`LG4vv}T@09NYq09=&K;lZ|*+*`cd+eK%v zaAAR$6hNf}eyo{2(=a_Q+(1{5Kmpiar{teKj((Qaa!Xzri4}9&Xp|RM?q~tY?!Pv& z4Q`Z~s+=yj-3-(5Ee0@><%b7~yWSH!Xel&dOkjo7fd=^>PS~1GiN!Tiyx!XjT3xfp zUraCM7DfI8?V~9HyGdv{x)a{%DMMI%*$pYlo6CW0F>)-MIAQa7nHb&)AUYrK#?``5Yw|(U|G~0kn}hoD(}__ZUlNl_ z8^_?CLH5U{FE1b)-kOhbxK7vK_NEgj$Nv*3Ns=lslM-w1{0AV3SflsCK(AS7kLY&- zArxh`2`oJfa91F^xdb~;>7JzQqT?9f>(^CcYz=cl8pm_3Mgkawswd-Pbx-mTwBJNP z`<9)ITMdC)Uxq7DR^(QbkyF8a64d_5WOF2v>%Pn*y-<&rnrPg=%s4FY^-i^=$p8sH zj*)z$*OG>i+GiH8TSlN=4L3wGdICZ|rt*OWh-OuqF)rvuO_a0=$o@Mn2=gWD`4i;5a-UmsNe!JGZYj$rvZM1(p zpzrmWrGK)%TvZ8I9od_Xs+4c-WI64Kc{8~VaVVx-dtpffK9x0~%*YT`&=wlioYd9a zmID_V*1~0}zPHI9Fmze4Wo!^%bU(QZVakaVC!D3&@ zb3^L|5W6y~N0Ff*&W=*N!l$ z84Awc$SW~jO*%DNx)elGUZUA-_NV1|HdwGP(Eo#XirD)d&T*skr9AV~Mwy^2`=Yezr57F1 z3uZBK38iH&%QSGt)()(SQl!62%pSTH*fqF{!&*JYnJBn0_mvuW)CI}f3=}Xr3cP!1 z(k}Z@g^8Ult#Xnb@n*VR(19}I#=S6%ABawBgVCdrEzlLPi<{8?9iDLSg?2t#{hId5 z>+mP-)n5?0d4WhoDxj9_#>4J$k$vJ^yWrphiCW@A`+2XL3q4Ab<#Y~eWUT%v!aEX7 zCAhL9PLY?9coMqEGWii*=4wK^czLCl5K2p4-AFyXh)&}2Jnf0+eq^ju>Miof4wQwdNG47DK|-POa?*a4zM^nT zox5p@m#QcG)1IA|Cwv_t|2d|;>E!@hp#vy)Yb{ zwx$}i?sBHz8R_1Defqpmk4!W%>C5ouzIB;hE(X)9HAp$8tw zDQ8U5mSCanCl+cM_{-0g#X)HO);P0QJSa=%2$e?Lotpni#5rBO^(K@2Q#0d_mpHZ0 zIPuKq6Bvm?*_<=h8}SlNG90&Sbxf6xAGuzbr9T(3MbtTOx&M4kiDYPX@dk-)+NPcs~OZjf&ptM>vx$;tEav{L-2j&L# zN0a9KiEABOB!a1nmuB>w14;q9BCEle;W5BDT{BUeHM8I^#f(^80P8{|I>8yPh>SmOGV&$Wt_ZXQtG?LD&ZEXL zI(awqhpjvCN8zS#u!Z`w8H z*l(`)!*I58`3GL@n37T)6d37Iod&ZV$#wPA$_XXt(Cn%+2}OiHbPh)!$hIQ1y@n$2 zCGcnTrX^+#YGehHE>S-4Dwf};&2I>lJspGsA;s+QNguC_@Ck*>R+h8G)W(JxsCi}$ zc>z%a39Q?QqMVlvW8Ot;_}I_*AaS$N4PJ^n-WeDZ=B!N#l>8{Dk%H&A`spej7-gdB z?wv$|r9poR&unGay+PQ64jz`)uM@T6K4C>80ztXXIbod-SLioDz-X0HtHIQI57_iZ zN71}<>Q-{)03t9L|4*=4!_0qx&G6gpZ5EYWUA-DO&G>TK-_@eDG-|sqIIB__dK5{~Ls!7e#Gj^#!6!(A{O9I-qKy)1c(x<*%z(%|UU+Dhl=IHu>h$=2orXU1>4 zKeCH)(%@~;jU$OyM3oabz@6{anvG99lTf?g-&7Vv8lwIV?=+akqiuBNGDf|ZEB3!O zKXhA64{ymmX%>A^cLw2L@;mN)ba9$nfZz^fN*pKr^$oHrDWTmtNkf|XD8d`lax24g`Tq8^6*FlZ^ZZ+cYZUWiQR6U{emCXe;Da+jk zzN#kV0XV3J-XKzi0W*q8P}5V(ya@Iy%9Q4rg6uRTsxC$~oPu%je zKb#zOA6s>{kX`lNy2-jgS%_L=6O(kmzv~s_q*6;V%wxU1M3E@cwa$`Tt$XAIpmpaZ zoT?em%Z9snTA&_mhhM|L=tyTQC}U>uImrDCv zK5yOw*D_bN__3%J`r*@l{T~R>=#$u_Txk2u*GEN8G=1Aw*oWY2$>l0n22o#;%h6)V zJA>|2{qJ>)M6@FUGF5J`>YZ0P_4^ECNTMEA*=>Z#4NQO8Pj*{IXX6Dt-uk?xEI3_h z7vz%=?vV^>W)h#?EN2aGOZ~`b!#7r^I7J{Q8cpZ&#Nm=8d=@WkbZ6S@mkMnez%f#0 zaFa}63JHo)9LS)lC+?pWiMWuVUtpKQUcf7F`5=k<^zOm zfzIvZY$!y%5c^_&K{-1bw)=Jt!e-JNI_pe1NFGr*1}tiN){d*`JznWIV0z$6&|uJU z4foD2l#VxZO8W(B^t`64IqYB8iJiYxU6qr9DFd&Qm*e1UYAk^j*9ZTlxKUOUF7Sh8woliyL2=mmg)FqRw9L#Tv40-Wcuu4pRsQpoUjWe1SjroE=#HN<5tV zKlh&|nFIYD8N>s7d-)3Q^S@J2P^`W@gC_T@6@1ZsTY-SRwbKWwEfyPsdISG)K#dkE>7m<$!9eTTp+ z&?9ixvr$Uuc1aMEqe0X5Yr-_mpq-NOK%^t8lNX?$8L!EqO$gqaNKjc<>D!JC7I7lU z7ieO}9vCUXGo7dlR?c6upXWsmENu#&nqtl=+`9mI=i5#*DOL)QqscD~@XH>eOr@cN z{hHr7UFnE%Wl?t1$}js3D)4MK`M#1vU1TgQ>{>_jTPX8f;aha4!pYaIsOziAr~Uew zJ*d8Bu0kUdSAG*MEiQ87J^TV4f1wbe;@|dPg<%^Z9N*MbuIggZkEW)_T4g)^hSDd& zr_U*6zV7Pmzb`Uk2_tPplbn|R1V%kNt}6 zhx5_tH}h;-Xf_Mt`ePs1YOve{UUT0r|9g9!0q;zTPkBlQy!fP?-@C#L7RZZo+6UoE zlq-KjVOWddRFvvqVMqzO)kMC9zdH#u13KjQZJZ1Y1jsSQ%~6A~jvC}ucx z-n2({okYzBeN}r+jvAz;qd`5j?lz-<4yCWMN|=3It7B_LO6kbX_OB{4Wvz=Zp*-@Dn6UJmnO7spJz|z6~|2aIP-ll)hL{!{Ak@@ zCk0j%@{X?+f2Vn zSTJfT)otS?E}5u)oh~b63L_;qXS@o}x3Hq*(afw26f0yFr>DZFD=iy00(z$}E_c+c zzAvPwE35*Xe)R_YkcVSw{i#?Z-dexugRz|oPhna3)_WK0&<;VTrhYpVel!`kiS&ap zwU0y++->W|Z!Hwm}*JfSm-C%aa}Ezhkb2{Uq^U_~oMk9~0@*6Xfl^?0idj!bZ6 ztt|J03egQ69fq&o2$5qEXOW-o5nqAX_4@~l&0GkC;_wzv^zmmK1b2KO)fxDv>t{7D zxpz;F<5PC7Tt09$H@~jA5PyRy*u$m&=~LJ+Fqv^SvEO#%v<+|F__vNc+K*t!a(ed3 zaej~0xrEqh5g%(!4hAL8`oU_^cfpOn)^#pf6ZadEpd!{)3Y?GqZbfe+l=35AO zBG^0l$tRs1}O5HMwoOA%(!SX+ITMokME9+=ArezK`r z6$bZvI{W%{d|uY4JrQU`IQNd{js?k{1-Y#SWhEYE({D(M*2^0)9fEjF;>SX zQHUABCU8|{7+4tz*^-@EWYWgKr0ju~Wi&E@bl~iG5Wf;$3{wIa*rh^_yFpR+)un1` z6UxMee=L*e@}?#2hO4l>*h4=YhiH(O;*GR^vxYhelI+b3bj#K4;H<~*TbH&K50njE=Jq5l4?akW2q?W5>8QxWI`uG zPM)a7ZBNJFzA*Fsp}uW&jAd_>Yyqbnfo|ndqKk+uiI6*IR6_O)xN`Akh|k4>>Rp6K zyND`HO{yK4++9?W*vcEQ9l!bor89Tgkq&LI&P~0phgg=yu;_(P5RT2R-1P#LjKpbit7od^nKa78Z~s93Hd1%AJ&Sx*vL_0&nlK;TGS56Cq)z!R4gH>ZML%QkLH zCaFZ7(}Pp7%A-e(9HyuiZbpq7sbFC~4$mPM?%%1W4t*$AP^>38Wvf)$QkA9I_^lNe z@j4GP_Jtx%^*;i_L@TV@Q1HizYp`xpAV%W9*xj}Ac<8@>@UvdsbEwO?;zhK}<7(0o zVFY?z(xdCgl{8d|rMjOHkUrQEc36FivMT>fie!w!$w1oJy{c)QGw|29o8+$+XFBF@ z8mz!!bzlFKp|ypj=ulrVipl~x*%E|>kuBf}r*xN!?PEq=0cU9#eo>oR+4C@|d`G3w z!tKf>)$ZQ1G$|Mp$!;@CPkM_de+hQAA zE3#4reN71{O%UH>>g3mC#GaLkk_pe-XjXN!k1~+JuaI(S#2-2-N_GUN$VxIt_t?vU zt*{2Jy1PMv-@15A;WPK!?6;>1sQUEFH9N!uQ)3y3^$j}w!WALMA}rS?#6G0EuFJLa z1FT?ek89>)**tjT6BANT9wUJ??5@WbBf!L()zpi^g$1l4+*|H1Ob8w#O7aF&A|+x+ z3?>i~Ww?f>_LB~>vdq?Agy}q}bFg-D;EBs^RsyH9#M&v;VVnR_xD!!v6584aNvnAk zwwj!%lI&tVIAq};eCA5K)SB?4H!}K%$FQ?Hd!L(ha(hR1GL2`9_ya_-7m@tV2b^;eoqERtn&yl?s7B;=*;AqM63Ncvy@CEXVHVgZbU`*bfyrK2NTqR`dTRTSk3Nn!Yp&y2 zk2NA_cX2=<+H5w7rzQT-DejD1Uay#gJ5^KK)bcEr4UbZc>2i{Au*+sZ8eT66>6t?Q z$PVndmXyKbcdPT9aw0P}5L3V-{oZX|B37%5kVscqR04S!U)@W51ineBtE)qY0q$Sm z1}x0W{th67hV1$G&TGKZ_HgB;?h}>~UcJK`&dFYR z=KKINP)C`f^@&Z$jY|ic72XW=Ii?VqWtLXV!S=giLf!G@%qOwW?w58-ytCdzoFfg&l8We;89)0~h z1%-U1?T(xw{-f!>vmBuzYFhr`04s&2=V5buTgZTmG82Yt>h5ei4Db<$Y^&R6PAI>Z zy>lGS3q8;Q!+uln4rPEm1dig&XVP%(#VD-m*so zB5lw4tT?~e&9djEKKAo)P|YmdEuZ@tR;gpL{4?5b3Jzu)8>Pdj$ew0>d%w@z^4ZG| z3f;Rq*e@PT7cDX#vziA=%bPXfd7$&ZxoQzTjxMiV?yRn>+@@@4ftfM6*ki*qQ<}De zUd`Prjn!ddbK8ior_)2*jwLYPg|ARyMkeoH6OySX8*u&^^QiX*tnP>Ln{+!8emg1R zbbfF)9bDEh8G0Q_`Ny*aoYK$G&m}EouCh#QH-qx0j+LiGyBBxX?%Y%-$IMQ}#cgDq zPG>p4yNwmyY;Lz4yhN#0UCmoG@>6W?EEf~^p*e#F1E7;Ox zGJ!!#g{BbF*JSzb%D7lV2(eo1WR#T-rqPl}TjEos403BUKAp`Z#c^xvK{g25Fm2TQ z*i`mdIg`{)Q_>$cED}gj;!NEC(B@mlF0~{g_KQDgR+vA6Wg>rEbg*IN@diJsl))ET z$sHqU;#I|Y!44sZ zX+}32g;fzwtoABFe*3bhWETci257AUhLz~5o5T+SvgWr{^s7)*FYUCfEzVvQ8;%); z>S|Ir1G{-*Au1bXrmS4aX^$dJq)pgqSI$^ACVO2Do#3BXY_sB69i#C1?e>)~LJ6vu zT7(bByY3_XLqxM2wlhm_z|@T>gZ;RA-bWPHRvE8u`u2DV<2n8&D4CV2pet^UhjFHE z83cV@v8H^d7MZLv<&9M*(d7d>m+9cFwnjgoRPL1b-RZaj^U7WjO?G6HF(|E!Y;hkN z%qX^XrlYHihcF5rTAoXFS9#S0`IEmyM@WeB{S%*RJ7`TT#OyxmL_hH$2m)x%XE^m+ ziy-T+(@G$)n#Qq5MPRmcAw`AaJyB38YtG9^x3k1U8c1o7`NAx;UXaS4N$$L~(DuPc zDNbl#=Aadnmr`_h=oyK@v{4^Esj*EFVK(G^7R4oaH~&q>C$ScrVJ1JNdnRSuL=H3T z?w*k@U|TUAsJ%TF^wzdm5V~wG9CIFLqIfxUV({L@1tDfw)SWz|80As4kZ9o)6XWy0 z9)s(fFPW_;Xy>jTH{2*xuwrlygU0%Pp|qUO*(r}e-qWfqRX+* zO%gD~Ix$M}5TM#J)i0yNjbJy^N@cMLOi#e}E8{IXt&qg>)fsZDi&r29IezM~7v8Xy ziq7UEJ3!rLinprBc!_sIQgTTjwV>Q#i_2Z^2frBhlT|(aGT1I5(O#4sXC5o?rRb=j za9STMo;%QetRMAyFc*<5x7Dfn;0(0ExL%eDM`1OezJ={+@*`u;_FD6LN&Fc&5nP>~ zhJ}qMHgrjy*FwGlNmyxs^ZGH`thlSzIefSWeKIs9PmhphD66-|;4MAqDsq3>LVrF7 zjczN#EDUie@YbL8kC9j>nv2f(bw+VhJ&Hl2PVq4MH=<>kf+ z;TR0XtJF@u?A7&}0B18>HW=tGm=*W`O^1yrm<$#8J7SAH+399_a6|N7@@U+?Xh*zB zUEuu_tNo+roB$$m5`+^?byST}MJlv5^VySx({lmQ^?vp`WaAd7V_fx<)KDA_d%O`M z7Oq3<2eLfw;@fpvcf(#AqMyu9k257yk1SU;HL9b}7Cx?cDR7oo*Z4#icW0H2pI&ZH zWHmciG1af)d&VK3AjK*_4C8K~de|b`LIh>8K(y-Gv#%IDH3r6~y zQpB_?jPle&=;t$HE-}BIQHn(uB!!t^g>S41QB9_qQ=633y<{~k%x*{eQsh+|l3b6_}_*luqV?%~I+Yzk|gr@api)UEa7%nReu z_7&c-8WPex&w+1g$lrP|rVv%YE9?~wjaU{yE{nqz@RGgyTaplzcFFm<{>bEDYw}6) z2I;Alm~_EvmS-L*%85Jl=0)&6OCo>WXn*E2Ns1`sk<%R?6p9~+ZR+$HL;VIT3l9ED0A9WDMEU8h@X8|Z=j&nLLy@)7@4Ge`p_L#+xK!)>!#Qw|9MX(`s(I$ zOw*E`u6{4gJ5ME9CuRv_`DcNg!erE`ciA%^T)Dewz`Hs$DO->rzoZ?SP&` z%4qX_U~`GKTXnN&na>9r*PJa7oA(x*sNb?#yx>AU&J|a9Nh9l(F%4aOa=j?Mcjh{UlYw`szBWR zk8O+R&yB+q)iKA}O`4xX+DxRh+wN>eMJg3&7cRh4Nmvva=~j8#Q|!2cgybF&dU`pK z$}sCOH?A(j*o}58^4y*SaxfjrvQbC2%vBb3(LpLSsHmZ>rHo%Xu77h@T|O&1@+C1W zP1;#~2`t%$1I@g_zVMNq4Re-52kxImwtq4$j8ti^J54t$J`qT?mU!oBM4)&pMjBKqiz&eOejOxDi zMg03S^W=2L`w22SBtJ7$}xyi(-w|yalw}-M4o2ma^9ivQIy7*>;bx|Q( z7TNyVWU}#Ip=_1~IXcc{ASJ>~ae^A73imvr&LNHgN{&Q;LfgVLCt5zzTPY93Hc#}j z=?W6{Ob&{yR{oh;ZvpL`2wXpWlH`$h)g-{ce%?MD7nkH!87{kVA)=}_H2TYn2_3gn zxlt&d`b!aR-o9}oSh4u2gmE~;ieJGdw3&1l^1-p~*Ym0T=y910gYfvt9FJK8A34oM zeZC1EZ_;`{f6cQ}cc>tO^=R6`cn^jDEP6e@3W0$<9)?Jk+S7H_4U?Hi{z2d{=W@$U z&shR$Eywkxc$X4qB3=+TVjSfdiQ8!>Vk!4e6Ovt!BzN0$M`7@_amMH2v$lmsY?pKr zOldK%h{7cHz^ z7_3+UZ@;VZfxc=8zTd4GU?!nMjlGDHy%4lhit-!?5R;=X!UX%(NUCaB#3XB~+~5;J zH1pu$uZH6jdKxqv7a!znTQ0T;+`StS&L;mbHU*gAECZGW((e(;NW73{J{`on-^Z9+x@B2N!bH4BQ=kxmRN#FLCaVymdGyHA#_Okora$LpJay|H9y6xI*q8nDAbD>h zST9r}&FW%8rd!00c({G-z=NdNmjl617@@97I%^D=(JWQeX@pE}J$DfLn@ZraOs^PD zI_Gz^!=XSX7gLz)Hpu!jT&;mplMSckPumBDrzh63l_Gq;rr;3v z7E+2$9=e=&o!U5cUEbnf5LPW}nbQ8)D59+1EAlP@EW@5rarbywph6<)@3HASjtK}> z+^MwJjW8=&oAnZAt)YA(R0owvU^W~1vZ5A%-(~p&R3Lo%y1`U-raX|%@&7p?Oq1lFXd-?J~d#ihA(}ez>n1vkV@^gW(D|fiKcq@aYCrch|b*9sKeP0Kvkyx ztMHTYj&{l)Q9<2-oIsLnnQj{`D?w{P!phggk7Z_Y>rVW5%E9IM6gRTAdpl`HE%wnTNbrHe#^i>T38*hcI8551vS~<2ZAY3FQ}I5= zqEyt3~I$>t$jpQ~y3MDg(8az#8}`f6di&tkmy0V#+~#;>lnCm|!MG*C9h z#03^OHM~01pZ0l0N2Xj#!RqMnT(FITybQ$MF6$PV=Oiq{8p-0bYh0&NlqP|DJ-KnU zV28}th&}r%oh7*o9h5Kzr(M}ZZITwhaE}wD*u`tP*x^m6S;=+L!|pbRfk4Q4T8U(W zId^o_60z(6mwP^YM$=g1`<%mpBiVKld+!{gf{{-;Yq=$7DxK?AMC52~w5d!iu}p_p zj`d-r*89td0AD1x+Iz zxM~+no8)z}73v|1Nkz1jDHR=`AFwSVwW#G-S&PaJcsicHj6QL8wb7QsnuItzb_84w zr#ROr*^P{?o9cPXY`|6x4cRudFPki94%HB@sHb!D6{-th)ajC2<}q4DsX!{=_vPA8 zePkJN8lyH($Kj==9pcWu69HA-TOn?X##0nh(uY1GNM3idOndU^+(2f7L5xrzmC&Ll zjwDor&&mch$6a~P>~Xwp!c(z@b;l@yzI;ff3cv{fV>w3R+ z%*ic#=;uuhn|7AC31D`C-hg3zKGFe zK>>~D^_>lGf9QoT(B6tgcZRGH>7_F%oF(0g)>bjuj~~?6(}A9UO0KIz@E9q4FU#V5 z`9H2SnJu-d`>taT02Rt|6`}Wr%TU^x-qs)-0PKDX?GU>c+*_Bhlb$rbKRT*~wX7PMmU=Jz&@##|W}knSNP9R5uuGz5^y+ zK@vc?Kp=>SsG=a-FN^iFf+2r{x9~qT5dWE)etO%QnESOCvn!Sn==Frw7WU?EO)tg% E3(3YjKmY&$ literal 0 HcmV?d00001 diff --git a/docs/images/creating-a-new-detector/ruby-component-detector.png b/docs/images/creating-a-new-detector/ruby-component-detector.png new file mode 100644 index 0000000000000000000000000000000000000000..532b0956aa40d4704356f28c350dbb12443b4855 GIT binary patch literal 64849 zcmd?QcT`i`*EWjB0*DmF4hSlONE46%(gX`4O#!8YNE1lt9YT?I5P_pKF+dOnL|UY5 z2vtEvYNRG)1Cbgb1f(QLLXsQQ^M3Dl$M3%P-ZAcfcaM>gwfo$2&$ZTk=JV{N+s1ml z`%muY;NalZzj@7+gJTcK!Li$eYae?{t+-Jd`)`-Osh%!JWxwzOdu5NCj*$)r$GbG1 zZO6Urb#9-VR{k6uN6+p2cIlgn|KQ-*p4Y#oV;*9^G|^Xf?o&RCP7qzA=M4S5p#grAP6kDb0|V;V#au*JjN=x)Jdd0+SxaTG{RM(_VcFox z4h~?G5J@R>;Qe=154YnE_7b4`-&#Qr^0(GZIw+D+C+!(C-otYxGb$ba1UwMNIWSL% zawe^lR}vgNdp3=!Lv9`Z-X3#&aSPflsYHJ-71f6I0~~QGQ7X+I`Rby)n1?;PI5>=t zJAz4SX;<1gZx-#^CQlWWeNVi~r?X#Y^kf@z#;pmVaS*6G4c*O`#*=MNYpI6n1%+w% zKb(|w6yo4eb&7*=_U*gTb%ip{iK=AGeRZLu`i#%72}%kvg$ESg#KYD@=N4S5EUC@+(II38Ta_UqsOKe!BWkLnJ4qu{jH6gsytMIT>VeFv9MW7WPlG<=t+rK9c1+YrFhvo1R|h0sEGQs9212##g)lYyK~@Fnv8T8dMi!a+?jgxf zhKI2-n$uIMidhWwlvTQwYBr@Ijq0q1O{@i07h;~QX;zU{(U)`JpFxAl9(^+Y68#-R zByR>yLe1f-E^Dtfij<&JTHCIL#{w@f8BA>D=9{(~)hsNuY}8G*({@yEjjlYGG2@BN z#@a97Gl(@Ar_@7phy4LduTNUn+uSe*+jy@pw%7e1(mwpq$NTx$zI;GClnC7XnRNZq%j5N;+roT6dJNUJ-(*jF%sXj8 z-bZs`%SsBjsq_OR5tA$9LDjRf?O7P3^VaT^7r4eE!50q)^JUh-9D5xpXR5GK8Z9Di5l?jWyOPi}N%ba4Cs zKtBv`wEww9TkqYx8zGxYim+;hjD$77GBGXt@ImJIt?!E`YfI3W3`Q|_<;9G-oRc`? zRc+UZr*&|tO_-sCmh<0Kg=Lj(H8MZkCa{8}EG#+n8Ixd9;vmrC-ebb@24FN;X_dFr zPbx`IQd={}RzIg>xVR4zimL`u(>=et)UFl*p+y@Po;oHP&ZU24bl9stKfaO^@i$6X-~lR}-E=jZ!o#a~;BV)$}rVpwaTri2Oropmp95a@y1z6c4ST`PX4 ziPrztY^dhCUj5EX-^Oet>E8U$k1))JS!wFnOg_nfGqkA9Z27s+R?s3tj5HltDPn>J ze*T=t-?ahyLW`mG0fwXJ^AIn`nX|MMc=2X@b3Q&bJ<@yp2R?w3-FdOpo7&9pvWV(> zM+(g?ggn+dN>?Bo10|F;jT_8$7rQ_keQkg;_SVT>?cNy4G?%9jJAWQ4S!0;@jN;d6 zc4xv(44hvIbkatGQnb%K-p4pNzFn-1hbgM!#RDFX9@Mct4xD@W;?>eng5xXQZ^fJr zFr7U3ZsXPgi*R+>V{hxoN)Pt6d0kQGRc&KHg#-3!d+wg;ec#w9bDwi?PNzPv_hBqd zEjnm7aWL}JYw6c~ZxHyWLSL$~YwJ{{1|+ZLlWmHOcEQgK;qpxh=8|TSic9XD^^S#^ zrB?%F{3$Xc3iYXUTM|M!UDsXfo^dl_+cy*p9ma}_{Z)e}4r5+EX=kVo2jmtqlG*;! zPc1rls-#`v5ZS!`9KG-z=AOvdlAL7M$yCR568t zitxtbR(6b8ymY|LYt=Z69VkBlLJQ6T2mhw3J)3j+GI9z)SG|?U@-ZT0{X^pP9zyl; zz9)Wwd);<8xkQvW_DJMrcF!DVS-^%iXZMlGd{6uf`Q*Mto!aTjy*m{fQgSTK7JB3IJlD=eCkx3_Es_IFC{_~%zV4b&gYCsO<;MRy0<6t>yWp?wM%5i@zv%}W!6 zgjR4rfVV{Q)R@536WW6I!^_@~l`>#84MA6delL2CR-e~SNL|_B1|!~!KYIOeg54+S zt~QzAd_&$vA5R|8_NzoU1zrQ&?RxRWiLdkfC`BJ4UCw+PiwRIG_`)BclrDfVxzJ9= z?wjz_HX^?ncFMfJW;wCksGpo?{&CJmGC0U?z`LkbYNMvVE_`lQ&LZ)<<@}=6TPLqP zT?E!}zvnd5OS8YycIfp*jn6?Xef>3fwaZq)`8RqMUeF%6v^CDW9+B2iDO?vXtlJs= z;R^2R#&%VGy`j<|A%L%{72vUL@gUw9@_mdyPs8QuwK8bb_Vrqy{>QC50n_vjW1WV6 zs#a_dUGXfbub^H2N*`8GIXwy0b*eM5T!sg# z+1lIlnP{O)<&e)~7y&YG$u=r?UVPDy4I7_uhAt%czLTLC^}I`JfPdjOf~k9m%mTGe zB$`}384CKr?NyH+OY2i;+hz{_40#uTcI zMKoQT_)OE=WjiQ}=rWQi@4Zr|7=r>cNGafci3C`z7tGItbl6C-QwLrC3V@T9l%9{` z399qXQ|^90ZC1WY2&ktQu3;6^L3Xk{{x>wlpRS)!35K-tpZ+ zY@?q+dni#_@Be}MX`4J^e3|i-G#MhC>lLmU^v#AtF64D;-EV1*P}FEVA|F0);387d zHxl{$Ayz+TY~F2#qB`2*(u7uI+WwuDMv7ydbT?zbw7a!1_K5mZye8X}jggj+v5zZe z8;P1!rcwnagY>bCl0!Kn8Z`({VMDyIceQ*+_j-nQzFMx&qLF= zx?+YX-xZXVYBqf1j=gQoSWq^g?MvrM^KPst8pmsSs?X>9E$!ZNlJ4J4U}bs^y~&cr zx=+#WWehQu2bC#iY4NWnUXc~J7Yl^z(YCHl?AMT301}|-QR*=3qZLhOphJM0^Wwp* zB;BfkLofE$*7w_rG)tZDfx|A16$6?@UY>2I%J|n>d{8T#T_;@__oquT2ll;hbSeDi zE@<7#u#r z>VY=uIy6JrjQpZyIw#Tr>Bx*lTH&_aRb zz6Nu?(1dt5TLHL5s@ofsNu7xZy5%)=#@8%OfrI1yuF?cpr!g94raH$|2lqW+yCR1 zvkNZBJ0v1h&DOZJu|$S*=VT6!e(YludH%=uaNV6dvlp=vO=uJ4VTj+Bb4Y-J)gN$n z|2i&cHKalOUrVZg*~UIR+W0@UUUUHA4t9Lm1nrYv7kYkOmxJR{98AKwsag1x!}lFT zwzF6^kNu1O+6n(&`|>Zx?Ck!JEq8vyD0PyD;R%A!8YMD1pUOus>YFX;vyOnc+ z5^_Fg1OB+~kaWkN2jozTgY8_LgX4rtqoOU^}~@ zwcgOM;D$YOYte4;8)jJr8{E1B}87?~}MW+bEaY1A%V`_?_-l6O= z#?+h>j^ho<*%XVDk~6GnlcaYy5T*Sa)B(r*>r@mRm)20m%n}$ z2Us4wd6@p*OV3{Yj%c_4L+74g4SohXumh`1;4k*NB_=d7#)^2eDNeC|o2S$&R}y9N ztn<*$vsvf5;~>W0+ZE;ZSV}wzYKIuTpA}ck$LRxxK@X&_9;hcgM0ox6eDpd}Ad#+^ z-cumI?d(6+3T7P=u=GQ~XTVZr|lcCk1|DVDM&CQ~;WmO5%jI1<}2%S?H=E zr-R0{p}Cpr&0B&$t=mdE!`$ikb%WEGp+Zz1e(e)wvmMs$KfshBWhRJhwWflZ>Xn)6 zUpJj=41E@n;_jJi-#?C)5; zXub1D93&_KnmopB4g2f-W@dGIqkG}87n1M?)2^+*L`HZ{CqyO&v3)ZPs31TVr`V>niPqFdB0jWc&e#)X)X8RvCWo= zRE>KJs{{RrRN~78a81q0h_ujYKB}gsA~E8xOZ7AmQU>5-;m_Xafof)N42tqx)vH?* zBTU|&lH9THW}YuD%UE|V&S)Q3g&??eP4k609{$Fq#BV)vqw%?Z*_*bzIjU|FKrw7F zF1ROB5njcqV#b(lG`eRP?b#_~!CA2a6^(}rZ=sgP48B zKlS^DznI%Hr%5icx^-c7sPbJZ(QzHEJaScDk|cxKdvyYng?(|0RCUEXZPfopm4P4B z%|N}5bRoDk2Po}?_C$JU9(%IwyE6bq=hhqmm&E0A&AZuMMgkCuKfRSyv2Ey)k8}Mi zL5@-D@c=w;=3qpAKDQ{anTHrp1odeO8u>#kPfCr8;T5qog97}@z}7+B_ifXM#G6@f zY*N)+eVb;SUSU- z`U^L1n32%;XVa8+JUGi93M3@G7iWhI=cl<{G+yHGw->)PgnoK$ninF0^tR3(O{!>R zL^3i+>9-s*FL9pP(xUdRy%vQv%~NVGifxY7_M5j2z8e_LQ{HH@(mr;rT(j-)Cz?J@ zP0XF>xt-IRxAF5KF{O3+#xKWJlw_wf%IY+=5_mhCOH*ES2TI_L)-NjatVN37>@w7-3oK!9 zl09;8FBEeiltsxVdkse;s`^M7u0CdbPssR<;mzWT_!)&ew1IkmukdQ^DC4dx9<=Rc zA2(a=#dKdr(-da<_{|E&DN43yCvDx}PN{>8f8;GcL~CnT=%EbXPI)c)T1KD-f4Wni z(`HV-O}FOzy1=boq;^kan4PCok=E(4$zp#A|K8uGJR@KFh<+-Zex)kXnNYs+9htSA5XqeYtO8hG7B28QT~|Z+u=re)yowXncH+@>}l)4V8C!;-m}dor51o zx1$+rt`%sq<+Cs_5uepqU{Dc2_g4GiRCg_WP4b2B40!?ItgXB)7G1yr~iL5%e zH!nu}=lYM&oypkAPy;Cyw3-Hu=@+urW#n?g$M7U(&L%BEGTS6UuRg;i27%N_T0#Cw zHs*HBSr)Vu)Q=?2SZR`p) zBet_+qc=P1WMve+X$sq2Jz*J|7vQ^>eo2Qm_v5Fo)ltjvNn~fs;hpOTt;}`+4y@Ih zDVpjDV>m=(v$(Z?%1Y^Fn~FyMaC2=LBqEKU93LtOB<;gXE0w)t4;5=8Dn80wg!F}{!n@#>u4ivN#h6@iqvG!-fB0? zn&(cQ`IT2iYjJ@$)WB@@g+xwSC-Qc0ccX!{%j7uOA4hiE=RJ{Sc|7u?f2^u^?!JEX`j< zpIA~VP5ZV<>OSYqNu$gOc}<_>&aP@Ro{sEG4Bzte+|0?yCVMuBieHY(%lI8=G%=_@ zRN!ESnFM1hE;OH`Dalv$G7V^Ae}u8}V!zY7+fEFwGU8`+GBMw_{Jlqbexd?rQV%L~m^&`@DXs zdW=OU5*a=eZ@X+u3d`R+h4o`L_{;B)7{m+$oWv#D{t-Lw6=-o@CLwJ`tb~28-~<+f1rM4QIwKG?|n1D-%PXkoTB_vc!yeG_q-Go>}bNi zu;_w#q4ltJaSfHNpmg^;a@=Q!1S(2R5RAo~kzfV({ydCWr+n?v^*J)TM^tqb` zSVY5ZFTQU}z+l^ltbKtudJwQ>l7C(Fhc5}REitLlpx#$vz42;6c_G?%7^QQU^1q1n zE}QK!&Vest<86aMRG(;?q56nZ7AZa6UJ{)T4c2!-qWybBSVfF5LKe+^FsQE{p7-L9 z=T&E-#7^^a&fjmpDfz|drLJO9f5v(`>6BpDD~3_!A_1xtV|CoM%Ln?5`%9sip$+Eo z;y+9BX8oSr;7P)ol3&bCPuyro?>aH*C=hrVqv&W9AvfPq)Q}r_ipmErQO8%mhzJET z@PW^4PiyZ(>!2}~H`W<=_P|kHTazv;f z^S`DRdI~aPabb@&zyx5-uwJVp02=NADs0zTeh1A%ky~0ma4iSc8WHk)Eb|HB0(G@G zwiFNO9<|dIIN>Agj5`{|7}}09jcif@&(xvF-ILJdSWz^T{j5XlACXS#Twnyc7$wVc zwK3G@rzgP9e3PkFy4z%IUHR(HX)eRe`ka;B=zK=bZ$GDsI)XB}OO4!ZTml=nb&!`z z(pO*-SeWBYlCu>{-QyYWkbUIDK38;GiG+4G=*3N~{$OWTkUcQx4Q#n=UHs&sKJRV` zNI-`$NW2!<4&1s38QRR@0sW9d=qm=)&9~pmg7RwZ^jh=mD*MLKnGvn7$Yj7XzwI{| zqhG_hHgNkNKhU5bsk$}NzFW}rZ%i$`cy83Fl1$4Cs%`C?W8Sbn>Gf&JLRHfGV#yvy zxjjQ#Wi1n~R~vtlAr$G?=~*b)Cq(4fm5=Y>$OJ=71X>uA<>VDPc@b)WL4IB@SAY)n zZw@!2WV!owty^qEPte97cl+Tb)_{3QG%Q_41#nnEu4&CUd1?<;GK|_GOl3BI%4XuO zxoz`{%cMK0TlL3t%_yqux>tYhQH=8h6kV6@Das``_`ZP5o0Y^{AMqSAIH~3TGWS%* z7)loZ`6mB+d^cpoeef_a()xf%b9g0UCc0;@bM#&H`8)6L^0J}*d2sD5(aIC&`W;?f z?wL)(CLIF$dLEIt;4~lkXZ&4w`s=C7?i0a&y%fmk=9nMZO8`(#AXS_``MW1?__A@9 zNzv`8+qme`RqxD^!_3UD+ZNjV`ZEY^&iUj0O15>MRa7h@{;NlcL2af~bp?)H4E&zH zMzB#&K=mc2r9?H6uLuAyi)GD&V{B(yqW^2)vzdCRE3gUafp`bFM_rW-5_SYX=ALbt zq=LKKA%`orgrFl)r2#ts&K@^#ki7Y72=8*St^lj_m)9=I?v5r{+KSI`Pr(`~PpHhS$gc-Tm-C6S)7v`u|Sq z{?FTh&nZnE9f|yUn~Cc3=l@g)d`68WpiMz^rq@-}vE>7s++#aYz`=3b=ZqsrnOd$S zs498aO^8%_{=xmx1Nw0=EFoNVJZfdxi=c7#PgUW{r`1H58CMXi-=|{1GylIZxC$$e z$;3|xdF5CUB3+w<;r8D9*XJ>kL(FkSI%ULVthptrYKXUue6HNndSx%`bABL;T37Vs zY`x%U)3AgSQz#(kch13KIoHquT)fZ2a*+1Kp=7rj-K#65k0qFNBVZEW_dik*^*Xo9 z-qLlr*}g%$hSe|$*>ivt0B<0XY(34YH7wbs@ts9TZ5q|rN;$%5e&(9N`P29>5xjR> zl_fgr#Gwc(av8Jd{+)tzlh$B}ChJ~)nOjlI2t;b?^zU;4|5`_z!x~2S;$Xg9bAPF> zOXQeyfwp4BmD%ucXfnllqJ;Lh%Dl5xps2r4mZJXo-$fvH(E*0bO1Qaebsm346!_%* zRB9MivF2(Cfr9s2c-sLF!~uH3w_)!%uiy6p)tFIYAIx@N(B}a^TB!OztFn7s_0D7X zGORKGO?Xj@5p2JBl-{qrl7pk{AYf$pkHlXn-|*lLKYiT%U$U!XbzpGzW#`4(u`u_E zA+oIl^J%O#;`7{;;_uO&Bbs~=2$jpV{^@dqloAWnF6I zcS7ga{K@ft)L8Q0+EL~4-(8V|<16=nXz43J!{;(B5=xMf)^>lwQS48)^?yKe7X)po9P(Byk0S`)}YzE zF>BIM7PXyHec-+EPR=*7N^4@yzM4AWvZdr;+;dc(p(XpPHkJ4o)%WlK_!p)%>WQPx z`{5K+3X1#~{ne$)vHQrF=oSvr^mu*uXpXI9?5VGSq3*$Pq0M>-71SAVmB%{2}h#1l~y3!9|vyhBE1N*TC^_u=MX;sPao>#E; zqbuHr_Z==y83IphiJ`n7_j*6w6R3bRA~i;?fC6I`mQvdz(?=h?&7H55(~sS{Cx>1; z*Q#B3IfWjjZPsaugTSQU#`3RDzwg@zTZI&3vLf?dcYchxM^=x&yc(2VaNiVd7&hqV zS4~;0k6crO&r9fknEm1Q{>8nu8%O?LjY8gyF)oC_K<%m7h9lk51iu8O6zl;?6APHIme%5%bi$4AnYn zU#Kmj0#vl7*{etLc8XPa{tR8X%+9|BqcmpV+q5*%3PK49=|gW79bzJ>=&mjHcol8t zzwL?ZQr+|d{4y|O0MJD{-`A$84<9!gd{@+eIrJfcSSt2xnB zv|d=UvX2*-Gw)wnT1}=&VT~CPU6ii!x#uQ3Ec>L}a&r)plZ>SYfu4fw9@4q^y#o;c zc<}V#3-+vmfK}#F>&F2;Ff8FVnwDM83ue&z9Zt(qV|f(AYe!6`&!D^BZa4e-EmGe_ zI;M+vrG9YJM+odIWFo8-^NDJp#!*1}5oP0jlD#kE4zQt&u;-73R? zIq~WuOhgp1my9tS9HF^(hlRCBRAPFm<1AwNcu8r=_-81mAZ?^`K|pdN9^!n7;xJ{Y zEFxKH)PXH?v8l--T_Y*g=fl88&zgw5OQ+T|i4S~{@meU7EVgj_1l!-l6jWcty~r9P zo_EG<{!pkk$%U7ax4UuB!f7H1vVZ(pQX0ZMDajO!^XuEAB;(1_wmxMf7mJC<$4x@D z@vLRZjYu0KxGR-#F~^*K(QL=7>IZ{JV~rIETkmF*mv!T>ZG*_Zmp^hdx}7d|JMhP} zzw=m(s*kL&C;Gu4upa3RhB)YU$Dv$Z-lMa2;d$DNe%X?Sz9U5B9}F;tRC|pl-Pzb*I%UdD@gjoNZ)tAK4USX&y^~?0fNWsWroyO8I!Aw0o|1L&IBPK~ zWq3qwJ6n$d=oui5=BA_a%y}0-2V&!%Utf$f%`r?)s$zv!8wzJ8y#`w^qXT-TVGnyg zT?^XYE@PG!o@8Xwg=jb}!LCHexVJ-i{Z(rkGrxaz!Gv;nstL9L_1lSXard=v0 zZb`lwa_%u{SQ3}b%l-TehS?Ucx|Cxrfv9L{&F+e@`ko?i{u8KJrZi> z_xt59bPn9Qngz^!tV7FqgZCO_#wTeI;|-?^poe)?dS4oP^79=Sk~a|FSI5mBVVwfQ zqH1_?+zMzd4%hb3E0kULePL{Q+fL8nefTuIYPjZ81wt z;nwtd-mo*RMX$5ds@aD6v0lP=HBx|or`Hg>csJLuFF3^iFt@Yp3 z@%_lZP~kt*j%rL4dGys(*W9mS;GgVpX<#)0Cg;r%Z-%KM?R!{M@* z|ICyAFYeS>3`!sIS&{ye|8m&2q_gddTyt*tr6DMi|DCFHP^ZM(fh68>Iqr$y7xr;s zBcwkhXXPIt%Fce-neLyBUsM0}_Wxt5_&<}&WT>ss@_3Z0MuckEN@KO~@B4#ipkNZI zV?t{9#f0c`vsm|dgOH5qM4OT=Pj+w+qs3SB-}j(?9S88pkDpi`w~TBJqDLNTuxo<5 zH7qluB1urU7{mmV0b+mCplqxekAlfBA-sZPIpiEC1Db^P_-G#d&WmoBh_`~JFLv%AF_=*2$6`XC^|Jws+{h-%o;KKh1+Z}tpmg!cbIh41O${g`%@@j z(eIb>(TuZDaJS&eBW-6qu9hq!+_anpqr&?VZG6!eBL*j-2D`6VnTQ5`I_*dg>tWhn zJUTiMAMKb|Ink<)MUP6LHEe%_wr_6*0P@YB_p(nF#(TRew`OL>Jv=LD-BthDxgje7 zRvdYzwgx;Dlf}iYV&@$xjbY@*xxe&43{ro)Q24KQK=a238>Z)Lt&pKsj}rnhBWq)| zUT+TrIVc0Gk_{>jLi!X~PwArS86f5rGqrxdTHIOoc~Y%iF0UOhhO_O!w1v53817!U zlFTsxRz10hs=!NlhQ|K$bw^>q&crp@hmTp|BwE``4LTr#rg#66t>{NoN|NlC(uyJR zCuzR}_&z@u81#)6-p!t(jqU3G=g;RX&x5Ovo0Na?gv6eI3-%}d_Gvx&(7O2w~4Y}Fe{15BV zWKs^C4o#=jHujLO!Ebw*H9jf$Z7bU)f9U!G!T+(9{s2eqmaaNrqRpsnT)H6Q(em4; zJHyiN*8f${;~O_y*}mAkx{B?n>8}1-v%(6MsSnu-Y{{jJ{@9Mq^0S0~b*qZf%wp|; zI+wAh>qL;PmoAdiz97oH6tTT7-_hhP-Sc8X_!K;ft-(rdjk+{$-jmB#KA@=%TBq1j zgsOfAUKZ74g_pHu5~aTFxV_b96Q#mr{ak0OZcKqR?QKQ_BN>~ZlIcRyRNr3zWX@_6 zVs34%65F-VyJV1y+_v_AiCH}%~RioAtQVBvf%x!9Mwug=!7d#N}IQK#cNfUExG zb%%tsC}#3vEI%dC)g}?Pnb4V?-7&)h8yi8tMUgFbv`Bxl_sTe)vWTBW2{5gH1R)1% zX$df|a4nhNf)Kt_Y!lgx3!Cn-`1T-Vv$O4hXw-UqwTu(H#2jZ32djxC@gD@xs6_%( zLdK~BI3K<4fE^bb#=GN+M~<*9d#`gE3hokwvX$zb4ntV-vno+nU4;{mWd!Gwr{?b4 zj8(0$`K7tCf9|PqRnxt2Zg*`ro!cZb21(Gi%Hy2-M#A=<&bA}))X&g#)r?oE_cn5H z#z3>@3iR#OhKLkrq>j)CZBP4;ic-vMb#JIEwG3*&Lw9!Qon`=@T;L?rE99T%UBO(A z2o3DfRW+%q5>(Z!qW*$~1&b{z`GoXdywcIBDEofRfv|Bpn-%WegFy!pQGIRk^D?`T z;pxUav$kK3CB~4Kup#^leY)&N`nJs;6y6`YD5I0i`}d=7pZ1Vr>oQr~{CBYXK9a6!?sIx3CoRSJ8T}ECQ$-Hgb0xbgMKh&A*$8>TCWa z(P|wg;0+YP^Xw(h&OMjJ#yMfD60=maLkBlSA*yvKS+0+x@vtbB?L?Nk=Zss>#5QK2 zu}jN(tm=|U=X#fnEq9O?ViiQ++!F#<#_T06ALg^wAhYiSmC)^Af$q<9Gpab1sh! zn)myW>pYduf}|*3i4rZuqRA^H?_gBPq8Tp0Z(actXJVp&LP_|vHNc-wdt z@6q-^Bf=Bpg7cfZq}`fJQr1FQNozn~^A1m?fxksF2wnJEy z9xVr~0j6{Iq%WgU*mLTBJEXJiWzgZ52(Dg5mTG&DBVrBBh3)lYX5QH`;XB(q@3q!fMD!&Ru}*HWtZDBQipbvfH6i#&OIGvwY|#Id*j;cva~ zlm6MfNq`#Y4kc+=_;~{hFRHSa#}P0@q%7r1>y5YYBeiV$>CW?`m7c7qp{V0Kx!C(D zF?O$3#{IJ^?&Qq|?%+c=zi|6c@fMhJ9lEebI|!+%dwL77Ci{bxrOUhtrQ&etXZeWkHKNL7*iX?o@vqpJD=go4HdBUxYo}t`Homj$9i{W+jQI5 z=+P&0m53*!Bp2o@+V*a4$NTfFV(a6nrH+Vjp30LzY1FGenEwS&;B0I&h1TlJPIEc# z3&z3J|4D#Zqt5PD+FXBq2*@E5CFVuifg-t8Eno|KvxAn=t|3$n*Iah6S>9cK>-@lB zVafq;(CGmtX2IcUN)PCVj&yKa)KmUCNb98NL&iN8Yu=AKg5BTVumhT%(9IyEEa~nI z2btS5Y;(()oQL)k@4XHZzPhYxPFD>{FE_a2e}Vl@L!0{cdai?ROj7X(%4mxY&F80N zFxYjO3zs@;%tCg)A-k{K4v3&)5(|t%7|ytyc5cCI@w@&x?95;+ikn%vKPBw{r>c5OOnllvlUGit7CiEmn!uesVFfWIOrwPAJ@E5AZSK z<{Sd+F?SB>(!W*%M*hjeR&o@kE zOZM5{YT12g$c6&=L1UH~I1#mich76Ik+&vT`A%MkE0y(Hp+f_%HCIvxS6azv-V~ zLX*LtZw`I-u}}FD(c-`VgBM$;I?vlc@5mEcK=`<-&nuCZdD|u_Q(ix|W|?HqbqKL- z#`Osp`JqZ>El`mqlFjLG6=v-n>4dRfoEgE04mqMb*te8+d_O7- zXc{u9)@z$M+due1ODT}+k&)?F?=;Ws4WNPL2-P@-qv zI(TMp_i6JUgsYS?w5Qc-o{cYzMr}lZ5zzTL+=y}G`)^;Tva@`xH@h+m;$e6jzsKTG zv-)!n1MvZxcQ7>UO%lxd@2Yn@r33aCSQpy?I8GJnuk*rk6HjisgBFLbvktxCYOZWM z2Gy$=DkgPt^f6@!01qKTx=so&`iH<#Jrf6`qpss1@21Sf_K%MHt&R!j?j>E1xeFL0 z%1c@*=KrdOw^3zD5f;R-PJtS}i?$B*sgZbjQ9x7O_;9|lO^ zZM&BxyQ704TP`PUj{O)%Sfd43r%O6K=UeZzVZQyC_&G1%(W%XH3Hh-V-zcmeKpOZl zyXM!Bt9jzo1iqkGLhcj>jEHzC(`nVT)s;TAM%U{wav9Tqj&!njUkdaN=?$#b_GV|a zfu~GT-a4T)6*p&$H}Bh4)VB$pdg6q1-*(#mIpJd@9n5Fz5b@(>_0q?2x`*&4TZ3f> zWQ}E=(9_%jFDh)w_c!YfAV6!l#5nbMK&6^xKFe}=+g`FabuJEc1XnMG_o{3Gy~X#(?Z7E-*)f0Z*3@!`gkZubR_$6FV>^9{h-4}MNbAr(KWBZo z1E^o&W4QpVt{?JuSlg5z^4h7$sCDpLjkI~*-@;Ki*Um1OQ^mOWEm0HL=8`r7Wz>p> zge`2|LJyoyfYA@!XUKPiO*&yDpf(DlY?96>YM*zA*OEAt{@~}8;g$Q5r8e8~gZp~@ zYY0MBY+RO#BL7V$v+L1sT4rRp>vn)eTNON5E^-fdPg{kp z9`igdd92a|2E89!HjaC{EZh#*ifXPG*(Y`!0B?-kZseEU)1BgEZTG3cg+TA6-0><8 zZg#Z6q}Q8PmPbY1w%4d z+oT|KMh}%4GO=YLQ43-N3`T7!bGp7nKRlgKS$uj-T@y<{?Vlel;Vw^k4b+;bzHM?9^-Er69e>qI_Dpp zkMIidI=b9BKaR0I=5doP4nefsEB2SxN}TrZkV_{f+XZW3dqXv|axgWe5k9aa9e4`& zXKTU(`VY|w8*k&|Px`#eU4H)PON1c_B1}zu)hS?%w)Yv___U+&@qUtc3{sxxg11u@ zom0J4O{{Axk=DryKh?`F9$1|SF2$w^3ohzdvGsYDU0Fl*2QS!3q6NOBI?ejGY>a0+ zos{Bz8F7E^oD;$WT}RKD8}jW@tQ3Ut(!Zp<@}rClw?nkOa$P=GR_9tLIvYNx!rzdb zHP0UVYm8Xm%*y(F(z8=G1;cpo`&?qrTDbuG+=qRZm@bNcZA2yZ{Ce}#xgfQBrbvXB z@QGy$7^190W0@y16)#X(0tIVG4UOfCtnQy7bN+3mtd{(k&>qYF79(u6HapF=+;`I8 z>jTc+IzOiOUHIQ&^Ub0j28Z;2gUy16w4<#Tzwfi}x^f|}7uU+W@2=l_6>dqBb%-5e zzLhsNrXa7VRhE(i zfAkrszdq!9Pkep+W~Iocts-_pBmd54mG0-VaO#$u<dLC}J@ zD_yEZ476xkNiNmeD%Q^dHp~3ZtO4F+yltSzva*GEg;!3T8;d+;1SUtW zE_5g=L>ke0BP14IQf4-+++~(Np93|*J%$UMpK16{v;dIN=i2fFc|<;)X)j0hC9fk# zSIoHmmM*X2K1LnQP66{hgY^NV-Vn8L^(iU&;kfdxUzZ#3Eq_gQSIp|I?Hh5DbBoN+ zuX1dfRm&98`#e`0`Bgc$MmnEv#1LKh8TcdNT`n&Xr0TITL(n$?zHgEbuB7K&?9s2% zax{_zY8J1J-+5F}>{8YcCJ~!o%UrncDNcIoZlieGFZQBZ$dexTShT+5w;Xb|p?dmS z4$OOkaOR_FR$T&YRmk42i59uk82N36GoDHeY4Ww((8(UxYi@~9i&w-|$6iuXeL^}S z#n$XJX<9hElVw3Hm?~mu&+B(uK4X#KT5GyYI=|Ut-G#Evll`)@;eRnczNKy z#NvI)0{P%T?tRDc_iyrD_M<I|oV*)q#XnE$-!^Wb)AdO|yQEBF&{JqH%d(75)0V7D=eIhIJ z7S{bKnRr0ml?P1sj71e1{J@i%yHNi2^obsnT?UJ~(~?b#RfV_bg4tctiD`ZcmC zuhz>z8wMHeq59uAxipIx3lNfahcD(Q7a~W1E+?6ONzAfSY1Q`F-}vE47ESFm?cEld zfMe;lY3mLCAV%7%Vh}KkYodk^cT)nsiedYB&n)e+JYnk$AkOd62xF*xTP)P8b`qold;dE9ap$2bJ}(Fd zqK;4N>{eow#?JxLQSYYPQC2D{s!s#^x@%$2U`XMMw3rvdapVDrgUTrKNQxls$2f$w z($nzugSUkZ>sC9UC3r2|%dpdSKjzrM=hsPca~tc1JmB`$+`frrd^M>a8O{{FZrKi5 zRW=YccF|Q94eCNws&F{ujz;=iYQ5Bjznzp! zu&xv8q$T0ImBX)ZvDd3&7iE6!^z9ISsEDWK7_l+kPA;F$mLxo8e?-T*mHYe)WpR_% z_c_cHuQ-5r0uE|qn~QYb0{Fa;_A_e#JfZu|Tv5wsOG%m;ssf0to-jO2IsbQ;mJg)u zb;Z`t-e6aF3QTO-lH0Kl*S+4Kos&3Ooe!S>x8x>b9#6vWvNT4?22RUY@Qc5YaGN3) zHp6PlHw(jhtNlKVRF|usKe4d;650-~z1p~X|I#Vc-oPAA-4p(BkTW32;eq-qHsf-f z!?go~%@S^_*dkW|+zlFf!`!ziCmo`^NSo#D)vx&$w1zxwXF7no-Q$6~QClx%zr_3v z`DWb^qhoA&Os(Kx?Bl`r;wQGn!M9&{t8btqQM32&04iAdCWS1OW>wy-7C-RX|EW1;GZWG$B+AMOtVZ0z#w+LWD>Su&L67 z&>;a5Lhg#b?>Xn5bH2IfyLY~C=H8nb97cB9-h1u6*7JX!-`^R9#WixVHqucuBQ?#={FUx{8#C|%Bc6k;4YyyY zDcxLi*VY_r-?I|$8|AV)va>D4uwonR!Nfw^5i8F}22D1Mjg1aK_z9?ztI09{P?v0^ znQ02+iTty7Wu%$|7YAU<3f&?Ejo;9RZ7HSEt$)?s_g}NUv3n&8S=y#@54KC0hJnspS$QS_eOaB3J@=& z#eU`7%6!q4_e;_KNV`I+!uRqL&FS!p${ha@ZZlKJ2-u1CisThS-E%A z?eH5f$^j?_HX5431@>pJ3uOh|d3qfji_bZVXwTEYq*TpHG~cfc4Bj(-g~21iJh0vT z-lu;R0ew_3y9OcfMmoq7&0PiX22lG-GeW-sCoM$iWw{2*Tq2tgt8__6!7wTqt_O3H z&8r1p%O|Qd^X^&A#%%s%?2hfkp-o5|=AhnbY%Fth4E|XkcGsjbvQK`OZDojh#2DJb zrhA{p1l%AL)$3Ua^%1-DZ}g2nm%l=+tn$j)-&Fcl16fz-C$=`9y?YgP=FZKjA^MGE zu0K2f+;ZP-O+}U%xt~U8OLh_qpFkC}lu69`jX9=+S%&{6P{dQxH|TuyO!P_!C03kw zvDE*v*OA#h;zDfwHqGr}*CHMrm-6En(X95QVO|#(W%IN3jgn2Ne-zxDJKOVW9$u9( z`?XmJZ-LedOdTPIbG=`9D4lbalG+&~#i!(H-$wtmSwn^6HbYhz$`$4i&JMb8v&vvm zAeK7e9q7^St%Z!AzUa3OedE2?M3{UoHeRH>`Sn@Y`c}wxL438vA|9E6aJxO$i^wX@ zcfxvMrH9Qutv{a7P??~lPX?m$KavOV2UwKZv35`E&j*u}XovHgrMO`lO9dVUYXB2! zUo07UU$bOK`PGTgZJB@Fu-^B?)a%9}amh`Cw>AixEVKw@KTr;z0-y#<9g5xImBd}& zR~pzP7oVlwUzM{wLEhZ=Q z8TU8EYSKFF86@;o&PP-n`VBJf`plf}*~&55Z4&eYHC)_8jUsf;a7S`;IB6Dgd}!F` zAoD;+%e6Nnz+34XEd`Dh5z8Xw)QLa3nX~-jVlE%z*YnbkCCz!l#ZroV#cCR`Tk%Gfg=4^^_LWkF~>rJ*-fx5rUeU3~8o&8UXDK8WBWCJ5hm6R@tyYG-r73*NCt+FMmUsDa zCy2^D)x8gz8$DYsh|ZaJD)_)XkQUHM%qEUv(jRC&%MBvRlK4d0&UJ^ou3Oh=B-U0x z5mt@n+Q0e4BsL`8Y>eF+=&b( z@h@M8R6sEGroYheFg zkYM1)9kF48n2MV! zzQ2w=3h9f@MjC5T%M&lIR5iIqsO-`kt$ZLi%=M~enS;!E#IjIB&1pC6S$yv4sVF|X zHuQ0?lRIAR^GnLP|D$w=%;vifqoZ3n^F+NE;iH+#D6^*Lo(R=J=Hh6=0j;Zfr1$)A zoxDnoXIHW#xBzc_ceOQjkO&YCL+iV0l&3arG2s!^%L=N{54qn{Sd}OQZ9QkYN{i?D z_Sre9c>v`Z@NoZ)zv{-93ecb9H0C6xj3otzlw;6h!Bb@aPxq5DMX&2}ImE9Y@J#O#W&z@YNzqIprw;D~cdB=NKyh6CRjQOp9IQc^f>EN6I5i zS35g>_FR3;Z+4;~17z_&Ra8Vu6Ux9JN?r#&7WDR6d=I&GGe{^;24~^qJ zNpIB)1cjQJ0l!HcwH38qE1p(}_4dEC{v4fUP|=ZS#oCJpM7b8uDD3>lI@rwxELJ+T z(6TgQ$=hr)J6UD6w{-A((1^-Np_Pu8Yo|1^Mr9hruxstBY2)Y!>H=(8Qw~w5VZW+G zT32#+#I{34XoXQDFCT`o`S1672x=MrHWxmsjoutYc@DqXmUjfW)E*`Bm;Uq^rj@9` z7n3e~ur#NiSM9F2TWKF8bAX5P>--m-0oi3R2}Fyc%n#=7mAXLxqc2RC#hP<0k2t9?xw!8v4781TtbXZ~XnQ^2eAD zA*6lm69|_R0`{RKt^?h<`(uuwpFavq{aFsv>s-HYHSJZ3d!6>j9QdUZ6n*#Z=d|B^ z{Og{Zdk4Fo|CK#d)CRkS9)$L;C^tPpV{`VCD-WZxp+?gU8Id&M-ygKhF9cm-{rLET z7fER0wqDFWs3a!?z+*mq+G1+z1Wo!C&fXKu&(cp@p8NKvUSke;C!$V%42t&w;~rcl z6d?rR59xDSlLvW-04u_Jj{Hn2ICzdDo%1f+@0N#hT+`;ehZc$>#L@mKGnL&7LHFZR zf2Cbsw6K|hE~MFNr?;izd@^{Ms}J#-rXG2=kpEtQtK=gf2Z%46)puWmDUDrwEKg9 zm{#FhT5KcRDr5}Ez;BvOO@#{7zRPf(v9g5GD^8)HULH<@4? z;7U9+;%$3=K*ar@u z&CurC%94y&tHSm@5lZ3sJD@By^K)VRFn0e|S8-^sr`H2?Y%3OKSL0=f$ZQK2#J zdif`4nD=>vI!X~er|_C^V1VhqJ^7;3^6)k7-8*x+;XuIwj9DN8Vm8c$B5itrHgY~v z<(Bu5ZY@M?sMW9+Jm0Vf>QUJzX5~Z_5~02&LX~r7T1f+;d#hjYmI7+0Q7s|fO`lU? zWI~OG5jl|@;6!;77#L(ssFk#znF zjiMC@MPow*PU1x+D=bvblJxvW<@p(>OvFPPJn_kO?&0FI=!QNNF~^927+I3rd62Q@ z`VyrU@AIggOY9_Zx8~cWix0+*yS@gcf^Df6O-c@W__vz*5Fym+81dt9+8rx`LGj>S z=FG;=vBPGFysIa92S>eCD2hc^0tf5i_L`~>5pY#=w-m~7E?j_VBFB>-IgJpJ4p<`J z$|x1$lR_1(Bu(N42RBo#S{fOn9x2GWib8zK*k1gpuK zlQu9o$HFP#Iv*b*i00FM8a_R`m9>Edps|hY!cKtYOD}GwSvg~w#UbeQ z@0&L6a(`I>^R7qsJ0ZPe(`QxQ8ryN=94rJOjn%_4X4^6nxa!Xxiji3fE`G3PI2Vv4 zX*Dz@#5cR18kF^(N9s*=74wYf5N8xOdBEw1kU{r2x*XAr5$3xX>>W#oIvjibbk|1D ze1kCIRjyFXz?Gt?gjM9Hc1I9&lu>a8o*ULC4@_zlEuMKw6E-^pf ziNs0cwHf}dBHMHEDoqo1EsNT{j4gIy2cUTW>ohgT&SdP@4JCW3zx6<`3j7;aQT3U& z!+)H(s%Do9D9wrRn$CkZ$8p7vVxMgDsJ7v43pna?O01>&r0-CQ(LD_*OhHRBkX85H zdVLeYH)RgMX+G&MxgP`eFA@D}PnfG<@1UzEW8fs<3kN9zM@E>9P34T{B~eVuMYI^> z)VfJ=vunjI){$fb>YrODTdp2Jty9A zr|OAcb!L`4Lf?$kgHd8zDGoI+%M!&nUSOI=_B7G1rYGsFeTPbC=a^5>Gn-6pO;`@+ zY!2N@cu=>!p`}q@wUBW_L>Obnc4KZQ=idk<=F0?_>2tpq(J;PwURr-Qt!(H@Ag29^ z!HxOj=ifzF<$kD&$LaGV?bbC`Ze?8j6owWzzFO6El8mG%nn{?Di}~o9r5%S+S7%8A zK9QDrNnR`UL4UNoZ^7if;FRW53t#Z!of--$hKz1;Rll>F9KtSx_*9Mdc2Jt9Fi+4jqt0t_C#lyZx3!K3 zt+j<3x&Ig)=-quTLArgXx7Rj!ipsC$8pjWNz_FNNx;~HK1sk}}9X9%bkSD8ym_`}! zmZ+!lN7oHWg9{erlR(UCQ8&rWLsMT!k|EnZJ{mZgC(~`d!tm(9TlKcxnU2YeTr+ll}8tKA}QWV$)eL`Q1!Eo8{Ua zE6QQS6=>k%(SBfy(!Us4MGu5hT7s=&63aU2)=Umn*b(pj{2wDLvDd2GL1`l?Y0*2j zv@{`b^K8VVo`x2`97?!n32}maKTsyBW@b=n+rWKn@PYlyqffuo2T=`8g`tv4%*U|X ze%<@X5-XR@DmU)L;hMMl_HMiseS!|1j%tEDO*cOipWP@k7nrQ{nW?-HR6k4VI`?2d z+QzKivt8sUgVjz4Q-;YhbOmowWhbUz1%>@aAf{{00SD$f?G6m2z^l}ro1&h%zt1-> zlbd0E%6I%}(pDK}OoxOySjOrS8mF)`;ezVqk6I|5{=!QrrWPT+>PcDk-rsbouVC>L zra%A=v<2?NKRZb$L??bzg1Hf2qUV^ypYf`@x$^dQuh!nZ7>6k%W6QQZ4%1pFDe9Ux zO$6FH4QV$W%W_s14)FS#m|`20@dyTHlG!uZDd7_-*b2ctbd?m;myT7h*4MnBElv92@IKan3wfOiKkc&2uG z@OaURG-IiooE@~*rG+vpE1Xiy-wmlPxjXSi$1)x)24qFxyZ)Ryr?P@IR6wN01B0GF zbqDPmuH<<*nihLYBF8;6&_+aVjL4I`TBnALuXD5 z`l0Srlmm%W%60IfQ_mbdKkrx6>ptHSV~mSYew1!^mtRZE>40#FqkJ%csm5+W2!j#O zEG6jib@`BU5iYl6!=N33JZF!5)zjsO1(SKm)*MGY_AyXjNNO%zf76@c{0#IUUkU0; zyQ3N0o~*7rsMH_4UgSg1w;-b&&$zT#BDmjv@)m^jJ;sbs4J^UnaXI`Mm&TP=dO_$$ zfYd1g@-+r^IA`|78x3YNLhO~K%?*(wB?0}5uK*8;-Vl&_o99=djTvGnDusfHdfGeQ z8^yikoi-MCnMxSh#(`y9V(-g+;Dx3+(1K<76p3?c=pS}aitrV1>546{V|0&O9p$Ci z!XM_~;u&Us0~1NK(CXrR-K1$q;rjCN-^;LB;ELFO@v=*XT$YEGN^YgS_1M68LPXS= zfyG{v4tnYR*{tmi<|#j(J|idWOl!w(HB^lXHIeD&4&Kl5YPj1yLU?@gLf&K%KtA~T zANe6vDp}eofkNgI+B+oTOUKT;h>gkRICdjS3}p?q#J7qLY{RY8&@tYfm970F*@s{8 z?>epjEq1wA~6-soj&h4 zonHiWi!;6y9=7arFf-^v=UnKv(t!g+h3Wahe%;VZE&hoOJ~qr?=3$c3HeI=$d=3)r zXB&4!*FK5{mg!(Qu?XkKn^GZZ*&YYERqsy%SP>O@(>>3wO)tU@iAo0U?!VO)`_!sifkK`@^4w+Tl4VXdfC~_PcNLf;jn!gfhlU%ncAN#5qu0; zghRL8WS!kvZ=z6_u)5Rr^>RM{>=7FbOwR#lR%?sh8Yh?sf1HT=teuZn5x4L3s6O_o zc9f-Bi^!(+EfeExf6jdj-{f=*WAOd)S!ouE-ag+p7d(S^ldd@P^KnHgYHhzJZKh!q z8QJ7{NGtscY2kyI6lrzZ^X{I5Bk8#2I00DxT|p+=f3Vb-b^sFG(9bg7t+!?^^K*0R zfQ=dOn?3gGIIX6zMXF2djvr`NVJl>!DT(EoGe1zallgAzGf)3z#Bzrcln^A}&NAD| z(E?&ie3`+AIUF0VrM}TgNcrY`_>9{LkZU*JmUf08eE_HPswg7JGoxYf)#)4@FW_C| zcBz~%Eu6#gobbILb8oXJ_7W$+4lGWw?7I z&pgM0FWh0GB{>RceGJSc7r2Upb|I(;jm_VhZyU^06%n?1XhX)9rY0AJiMD#j(G7-$ zC;;FMO$NQlnD^m%6#VmidU$a9@|=mjH`7~R>l|b4`L2i%))ND}$dQ|@^qpzKd3EcN zDJXj1Uu^!`Z*k-Q8fEW4`@a8+YM;bUx<7vy8xb_OgB<-gcBdP}8ZG>XTijT~{U9}! z6NN0D6Da(n3yE-mzv11%mQuD?Y)%hi&eg^6Q&FN)a#Ztxg702buEpTHcQa4>=*rE7!j$v81rmlmH1pyAJO z=ug*Hy}GX0Vt;SU5wXh#&O-xlH~DrgG>x@4kU;^NtJB8pLeQTLL=JR+=F0S-+o40|n1Yyg)4U-=SrN-o>MBCZ8%uHEBF<#OMil~(0Lou$8 zMXpuXiJ}$R+u%IR#?`)3EtXQ>PWzoF!_dw_x*24GgU5Zh;rhK#&_@Mm_kAx=$}IHR z4D&vzRWF+>516}OM(<6r5!={9wV#R`?k|m@@;wa<8pI8#bHme1k~xD5%)6N*$}f|E zW!w!wjZm)O7I1c^Q)@%KVp=3vx!EQJP2b$cbzxCeb7{-owOvGz7E4`q6#iJSaq z#)y*jo=sm>owUBHyr2m~YgI7m8#*mUB_ogax3dgDqo-7^+05b_~n($4l?iVjMwx) zOox?0lU~v^#0=#1dS^r_qRSgG6jMv8T~#c`r`~toPFzCLPx*QtQ+ypxM}8nzT9mr! zL>zXQu|1C0YpeH9-w-80_pbvp4ncCH-ug_{iI+StZ0Ftd2m#*3HrR^65{vzd1G@(e znTn28K~Ej4*QZT9wWJ0jC8&&^ny^cjbiH}>D@Hh_Dj@m#Tq9sOk+rk3;Ycl+?Ah=} z_cr>w7Fvcex!)&_*^6oPJ4Pv|=`r7`k8i9tV>CROqr~S`lG!4j?a2knOYTWiA~{DR zOy(wZ%(}-}G9$6JdMLm~;ELsjARaL&--v-kgmiifQwogA}VuVT`3Gor7x1fyW` zwhExNex?2enTzl>dAFbwr^cqVL?b5#?qepW zD5ZPRvXf*86AHkpXG%QtKa$MjGZV8bcE9VP-5XA@E0rQyU2tBx*p*vC>TN-j_})SZ zb>hL@FRgzF1-fuPs{Z07xJM@Vs`^`AE^KvbfTht6l!R`7+aq_?OkUb>S8k_D+Kx;# z_Gfhvt>~t|LUDCff0b)we^;^>lze|!c{F@JT%R3 zB$K#9iE85ZeZtEVv!^uclodi_?vzO>q}dK-EPuVCrPAh?(|X)ow_eC)$Kms7=>>0I zM-<)P(CnPke_qQ2Prr4W(thSwtCVUc3h&U4G{QvVCnQE%=Q1q`YaD1+8@;In-7&C8M2YZdNnl zj}t=^V9jd@HPG7raxl3$$rkNJN55UaeC8oJ1g)bcD6$ol-ah_zIiI^}%CfUoeNFua zROB1DJmHs3)duD*Vd%RlA0OR>tQ2PSnjE1@ub})Xtp+%p z@-FO{hu8vbacob|m!mtp#oa59Ji$r3lYB*YW z&x{S3^R3T(x$dIP_p;ymmcfBOj*dJm_QW^t@O*;E79z&9un6^oKy1o)cE|26LESGFU?b&w%d2wzKV4P1g*d~ zRnZh`>rwMJwskrpw3nt0x|h{Nw#UOt2CeRV=Fr(eWbrqu#89oXTQdG%Z$5P@eGpy{-*wG(-Vt<*DF8|ozh~9 z8^Qtj*a^KGc`Wflji}-PxUzsM_(Oa~(a2~_5kF(s|5M(I4%31H5`Utfd1f8TWnEl0 zxu0A=anroCO};O*a{s&oJj^y&pyi^sC;plqm!g8)wrh`#ug14maOa9ZzAzWRcRYW#xyAQF^CWw>VSooS{BU2)Z`;joZ3N(> z?7KEwYj9mRxVIkRFi@2c!gxs&%N3Z*kot+EP_zNc#+<9U?3K`1h#Fgb1c z^~@k{2A*12@mh9&AtJ0C8S|2c%T-^2R?dBPe=QjHb>zBy1SwjPHP_T_JIpY$Vx6o( zFnGS8#fQRIrt2Ng`X$jRH>M>#wO-`Sq%#(lJ&8=cfzA6Dpth9obcU-=AWNVqX}&6B zI)8GYdwqwK?TT8b^efc+1>c)z#;Ye9-Q&DU89WS&uJ%0m!S`Cv7FRgewzT35a@xJE z6wj;faO{fueYXA_b{hlc-PWI!%Nn<8J*lp{_oTd=9>$1clx+UuX3?%P;&@u{UMKy_ zjI5f$R%EThjRF30k53J!viBV6w*Sbzagl%5-^TMtk1asCdP6@m5nmO4uH)P6Rg~kN zE|vY??KWdk-Q&G)MoK%ExBJ)%7kNDkewQh2Q3KK0h~RonA?8pc@4k#`3)NItkyT=fG-fDaM@ubcl(X^u1Gy=?31oZD zGbtxlv&jCjH+NyA-5La4*`UNFiC=77t&fPQkvCg1mNW)#c7(0(g&ExorRnl`3Pr@I z)Oco0>YqRWW1WDcb_D~FU7`eO`4%BG}jYOdiX1F&lB;xTKJhKD^LGEcHjH2 zc|N9TqvuU$36~b}Qcq{F)IAv#+Jc z$t=9#41iUfb{T|t-Th}AiQWkR`lgfJvn0WwS8!6Ldbn2>zPknL7-0Nyr19P*T>Pr zY2=rwoW0d54_pC!M4>egeH$}mN=c&Qiw9!IOFU(AQkbmfsULnWVcv?v%fS`Cpha47 z(<6fBu2;cm^Sn!AX0@Tk@ekZ?ov?WJu#p(?BnvQUzXDbrPuvsgu>lHt5#h zCiFm)Gx{$U&l6#-W5qHSY~yXwwF7=vyS#+4QmeA3g{l$a76J?r2+Hw&z@JX z|D-@?VyRtl(MW`Nbf$%B4Vc7sTcYpVmHJ}L_Aka%JSBN zS{u@vb*<_cPG`njH_&G5r=f#*WG&jtnPEzq6)ii-9x$P|7tiHscK5qBKlY-Yi;~LF znD?xXytOTGTGKz|JpPf+?2`SVyY%ZauDscLcvG?aJ7Dw5k?K$Xc#;$PVEdAX7udt5 zgjx3Ru2*-L(C0FbuX4wY#B=k7zL%w5nIUxHvMvpaU71gN8@NLwm(@;s@0ZI5k81YP zdVnK^7GFg&{}IDD6N9|^Wtm(i-JZN$2KFfhKeMJ{MQ2JMBk`?1KKJRglUHz^_4Qta zbGO60BU(OMD*BqEnjiEzn<7$neyjS%mr&Vv@%Z|gJfq)-rx0^j4A1>>{fy#`XOZr< zDaxXr`Ciy4bC&dX&2)MK?PEh#7D)lq>Y(p`AHMdoxFGayr!1}auMawe(swz*2`3!t z9X-wrE)$*FStDf_dj6yl+#pNP6eTQ|nP2Me8WFpKTS-{-!V+A5lHN03!+Dhk7HWi? z$-vl(ly4+(D5sXLr^OOGj0jfFLbK`{$xgHwO@V~jaB0S_@4i^Pm=i;ZDbRV15@ZzV z*54*iDy9VU&v?J_JaoGBue)OUp#Gk{yu&D&R77Pa-D7Nx&@$j!@M0 zJL~blxxMq|V_fR6A4SX~1ff&4w{9uF8wMpjfsk*~;?CQf9=ZcJJ#gC#<&(Ehc z{&&EQ!CR6SaYT9uTA>59UV~G};lZ307Se3T@%Fc)nqJ~{+_kTVvIB-`?YN9W=J{^K z+&+C|4V?)*xFfox+UtzqQ!cXmTA7S@JmLno7Evt9iNh)(GlT-N z3nrJd=jyufO2hNZGndWPJ-pb{oYUdd*Awr&UGj@DWgS8yZ_d)*} z#-}6qg5&TksNYp$!SlkzEF=CjG$0I9loklbI!G4i6lbj_$G420;^Q-{UU{&3ji0tU z6$zZePHN-nd>IVoF`ec#B!h3O4^3 z=wSw33s!h%aO2Au1PZ>tY3}zJC_eb5X^Lb6`a{b5;X-Twd+N(Gx*O$f5yJ_Z*(oqa z0!!rZOye6}Khze_8RuYqU(=cMIV$cljBUen%+TLkz0wtxHb} z46W*aI~c1PYT%!u9DAF7UZd3Lmq(>z!?8)@_r9lkPY*1)I{NtWtemJtToCh=rJN+- zXbt*;6j9G^hhr-~W2RYWHos%s*z@73;{_t7Ung&AG!Qo)cqeEeW;Uz0^1UTNXT9&+ z0x2V%X}6+m+DaJzEJ{l2Qh~*x&u^fihqRC;DG~XL#28x(&SdG#!Ud**@HN$_^CBw6g_yk<*|L*V+t-_v{4f<4#EOWYo0nFU6(MYQ*r(;am)3AM8?#V1-`H ze+aPn2WcYHNhm1rJxVTZ2uin~ek@{EUcMw@$Zs^x$ezB52zH zmCWyL7q}a=_L{$SJU?=gCtQjh4h1$pK2aO&{&imb`5%bGC?2qe+HQjt*hs=Q4W2rk zpotC6=Dkq*eGnzZ3h&+glhCF#jMNXsEFDW-zIrvD3LKQ^Ib-qB5Of4%Yu$&%9L=za zXH21)(dA%h%oQg(jLmZV(-IIUo&Sr##y=>%|2<#j|APht-beBXD{c=~-R=W#ufEA2 z*x|!|uu}TXuU!4juS|`hjP%Yh{$W;fJ;b5+Tb3#I;S1JLH8@dDm+@R^B`^E$ZUZi%#MG%AY(-66iQx(xyfuLUIsnz#4P3FSKxj))B-xeGA;M<{0no4KD zgX2xdgCd(_cxokPnxRoefj3*L>(27T?5E7HCeA&C0e!S44PAH@SB zOkBOX@3=rYWXOn?Z6q(&8%tX^ZLtu) zA~4(qdI`MuaDFwhas2wO2bZWfFBIvMqc$ZlO{&lnapc}TEB`orRS$xj*3ONQZOvA} zr#|0ysLgu#;DQWbQp%|EbiMdXN?8euDomz92|Rrc3#UIK8tu3I;TH`V|5u?!02}hJ zO|Xn6uT!@EK4`ybxPK1c{r|iphWAji-$dAOcqHLq^6l6s$)HV@E&eMppE8zH8ag3X z99?@iSks?LDOl&PU6MUX^iyC6QfTZR1ZcIJe|VH^mgBlhS-LdgE};7qV?}iUTnhdbdz}xW^7=7 zb)g5Q|b9bZ}#kwNBX28f3|@=j*Dm_r=5uV59|Ai2}tPa3GcDQyV-PUH~cr zu|e_yuPitz?3zan^n@2?lU_`qAaSP`H6??5to@IE+r-&CLI54{XWdOsO~_wue0I-a z@m?2i6<=8h8nJBwZsI)hRcB%G_LXdrEj<{O>7tW){)JX6%2#q*Xs)eaZEQ;{&wbm= zlCE%5&y$TF+2iVZDdpLW9g&fb@nZ|;xu%fq)Q9Cu9uZrKAzls4yMSx?9Y)S(V;9U^ zWgcQ1seQEchb>&bEx{yv|1pSduT?kKTm%gawpXWMcw7&!I1c<&w7L+!g6%?v2ilkg z%yPY3`VbK4SUUWHoRiHO8yU7-UP}kI(e`eY{te$%E!UI8W~0aa6&vjRL;r2od|a+) zj!xaBX9pBI{=lcieKlDg-TSuvm31GV^00xJ;x|FjTl>q1EQebdZuu?r+T5V0$IQTo zlz`OxSDIW%Lg{3$(hB;3!j_p+RCE@pm>Rv<==%!I5XzJ8!8JpklkaI80~cqrKQtO$ zv3RgA)Nqolhk2mFT~l!?5|_69~APtpXDXuo*%N+2l&XS z>Km!r-FHkOU70Pql$4OofMm^fFLplOYVt>C{37`;-DGKofx83wZ&cp?2|d^BKn4?wrZAyS*)5K+f!S*UQI`mU9yuJ+r7) zZNN@ZQQ2IJ5bo*;>bccA%vIt<{hCbciv<>NMO1caO$Wsw`RJPGWepWbQ(MJxkdLx= zz>5|%P|CVe!&)-Kum{V-B2X&b?Dwiw1x4h%mK*iceBN-oOV`PNlI*;J{}vuuO3(;J z|4oi0o)jsnU-~|^A8O1u6n8CnMj*r*i$eP5J-+Hqprf~4EsOxR+k^0!w}x4jmp1`ym` zotwptTrQQpIq?S~nbsGg`Nv7t9_HbTG`4R7T3KhBcK7&HZXjCr6rt$LyPb)4v@D>} z%%H3{PC75{ZPPj2@f>vk(!Z#>k2q(=-`Ym<*$q)wvFLDeWBK(;ji*%Bi7$;Co4JvX zp%&Lu?3<@5AGm+KGPqk}tI8y(f<7Zju<(3 zD<7BZ$?P-I;gvq+NhDU^vh7B=Ty<#(wLuP|upzEvcv79w6$vh2dK}nnj)&Yz7J7LG zk5P8Zi5ho9i{TJxco;wUjv}D>`(uG>th)7Pp0u4Is|xL--CJ(hY&+6)Kfm%C7YUA;=c2^do0=xIDv6$R<_;9pNcx%>^&bS7%JRr=3q-_EvCf;l(Bz9wG!@kvJgdm1g%UlHA4V>akE z`K=bpt3Jve9=7NH*^Us3_cf032c7@i@cB<&TsP=W4M97s;i|IJz~{4O#=fI}S!1to zK>iRm-k9xiI2W~{0^ehmRSz5YUNdIjv4>Xwt2g}rJGKLjD#2c1KW6?jThD*}{|xG= zD+l@b#Uf@01@k0{kx9;j{^U-3ZAyLr!do^!KJ`@XCOo1K&tlJy6AGtyyj@zJb+a(@ ze$}%603Gbl?&thtD_AgAx;d5+MxITTz$@1fc1r+M7rcG!dx6hkCu=@n7NfncsPEVq z95p?LLe7E0Ouv*?M*i%Bc>jW5*}>&np4WPJFu~w?7y~Z#Uw5=H?Ho#NV4W)JaS_+< z`YmY3dt_so5VLSxt{?x4^NE<`CBw26H;@rrpMD-;H14PM85i!~Cf+g}$L_}b3pgYs z9&=k0FM0vkHBSxX+3FVVRYBVZH!%+^lLkm@wcN1Yf%kp>+hBty-g#_q&uQSOK_<;J zbbf?S5MGIoFk%gNI(PE8{oTi7fM}~O!!I8E$6D0J{WNJ_27X2y zg+IVbC_{-aO(BXdMVTaX&DpsryDCJ%Z4$4q-WXl;N}1-$oVk&^-cN7YO&2@5MJ5g^4|J=GP2V8&c*%1(bRmf>1SxCIun9SR?9acRGAc z$>ct>aydNdj5x!9HrCXPUZFgJ3mYVp6FH;l)L#;wFW)VF?bMIwPp}tZplJKbc62_q!b4;N3Y(}xnyrCGy>z3!B}F;iVY7}V z`J)4`m&fYRQuH%3@6(rlPysYTX>3{Zjh9Qggg*6lgvI?9rDFLT>Y9tj70g(Ypw<3@ zemkqYMAiE2-dBk)F!U^Ha&$7=PxTOJ6=YM1PmRZH6!f>ZJ$t<)dX(gC=8@vPD0fXx zud=wYm6L*Re39?(d%mqpJPYcJ8tWB@ZuT8yc0UblEu*GC8#vOPud3pKeyQaXb@qnK z$*7Lwy;W6~G~+CEEaF#&`QU`6YwLsX_`sa#_Q-Qg;|8V^F^su0DGV;|bgCGzGh2qn zv{IC5`30BYdziPe@lQQ}IH{7&NEenoO(W|%w43DLemjj#u@5q0=qCsBqC3iHiL)Bc z!GTTGO4?cil6?j{f;b-8lxw2*i)GRQwd&Es@jK%&&lg+HaJNpCO#0oe$u=nbT#wMf z>!chbDqlmz_%%nJ&J9q=GR&lGeOFZs8%n$}#QQF0&5-p^Tw>-vU=&u->$=d_D066r z{{xxRmBx+e`}uk=TAYZP1f!79b5w@&0;=;~S!# zExv~b!~8JqOUh!dnk5kvQFzJMr?)eujzh z%B2baHi+5bC!?pwQnqAgWmS>Dt+!9#a(l7P^^q@{$+sf63lQS(Xx{-4Suc6i6Nb%-cHHZM|ld+WSG$!4W_-X(vqWu zD)~nji}~(qv>S|_;eqX&%1-OEtelFtw{4&B2Ff>(otSpRR#W^NQci z#O36f4 zB)tR#MM`GF<&?Th3Jv$g8Eu`)z3GwyE=P;33=Kl~Mrg5GIr;hfuc6R`ztB_UYm*D5 z_BA8OsoUxU$$ka~jAvTyEfpbckyS~a!U_%gmMZCe{yz24GtFlaFQnpfjgW)}dp(xE z*LKErNyilnRc(o{bnYp&RHt7T;Ly~UCfc+jZ%brKqi5yJ8I((;WCr#1Zq9N@CZ*B; zO*Z!P!Fq+O*xuf^nI#gQ=1mhXE%yg~wu2e{QFN+dJ6`S+O_qHqfvr6u@$={&@pFM5 z8Ug|wYQwlbyTit^32t_5-(*vWNAg*}h0`Sn9Y0}dt?H&HoW#XOWBwkp%lS!SU*+N6 z_XmYByZ1|q7+#=Uy%ATO&%^MFGQ(J4D%}>}iU^Rd$*9w!`iwsJ1b)@KO{7YubszMqaOes_JHGVq$g1e2 zu-key0Jf|4qi55C@dF>!p4ehO9=_dR{ScR{zhvg7dbzWd6I`<%0+@HiU*PBJaJES%w>>1i zWB5ue!JC7Js_UN(I5^Hq{O7-^nuB&Cpy9|L1*fH&ov6mFPZaQ19Zc?wMH=Lheq(`C zLMc`QcVb23f>y_^!cu}Z$ioCp7Sevril4Rk-ogc=uug;vzd!S1%+6ccVp+@!_VGTT zZTjbcGkxu`o4$bli`unRiIJiOHf*>ncJ7O!RRlwLWo=3<=)FusMEpX(s(1i96Ag)E z8BP_kvB2ykcyOCBci1!eX|UWC(TB&`%$6*`ZtVqrF&mF)#-tUui6<9 z&#n@2Y|x{x98ef4HfMn6kEHw-f+8;ePFUZx1#QeBgO!B^!4UMaNp-9Z{EzRIx$%+y zNj8+fp02sNe%{2}h24jVARAm~u3~2g2+_Jc@ zM~jb@8k4O96`iR1c)R^DYN^`tXG2A;Ys{k2rvp&G@IkAH&C&U@ABpRBhN`4zsY!AY zO!vPImxSVSwG?*=OI$8E%Kt$Dm^tn9ni~W79Gj-cL|pk{T+8j3jgg|5J7>GQ>IMe+ z7uIecApQLC_Xo)(>HSz>gPNNP^FZGdfC?56IQ^tLf;Szpt<6mey=XI{{<@J8O7$YHM!KR2Z zv$KaC+n4A+4juq<9E`!2shO8DA58@~dLkU*>6TM-;cYb}qNJTwa!}EB&##}!mBsto z27+}aoR_6`x_JRgb5`)1m5U(=(2J?5N&pCSA>C{7 zA)}ALS$5d5RCCKEQ0z+fb12G+g(Sub)Ti${lif3xS8}(!kk2#~mtIXCPnf=MKPE_~O>UeQWE!mL7VD#%mTv8xpwj0H|U6?qb4vZfgn} z$aKz68aD`krm__uBP`u~$p2cVmCd|8mn)oKlkOP!SiN?pwl%T5%OL8NBZsg<5^`L( z<6hUOsPuUfXnQ^;r}t({y_ooQ?9E6@TXccH>gG=g9D~v(uKTv$uvjE)jPJ%fIey ztq65)fIT)sPAK7X9_(Sw%Vg`N+`H^^gPuGP>&{P}7_GKdpET*JU##=4TnKFJ3|b#= zF6;8myXIk$8^)z@XJZDa8NV=Xg{pFW7Y@3JzHno*k%Yc#C0>=~;y%cm*W5-?9ZxRW zSg-$?KAZNAthOp?h)l}57@D~*2;BTnIDLMkmp8v93Q414H7*l{`0SK_2p^S8tb$8$ zy}W)>uKvhv!IsT0m%?7CQ~NOkXj&B(WTO^�JZI9@D>WcNtmZvLFu)rw~^`R@=Rv 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 WGSa+iaR1lC-@I;pt;+0=`2PaU6#s1i literal 0 HcmV?d00001 diff --git a/docs/images/creating-a-new-detector/single-root-dependency.png b/docs/images/creating-a-new-detector/single-root-dependency.png new file mode 100644 index 0000000000000000000000000000000000000000..efec424cdff5042fea48cf04e802597c40f42108 GIT binary patch literal 2603 zcmd5;`!~~%8=q3jCrNHmnF_fiQX|&rg3y?(*=jQBGPhY;Zn3GNbqt^8*3lA0bQgY9Oo;ip97K+_IYzxF1w(iY>~y88MKkbWY~J8sR< zO{uCkF$Q2znTtKh|8Xqn<8AcfreaXbc zKU$#m*2zM?cKCwgE)f43f-`Exa$8kUd!y6m!{PSZON!fJmIp$qJIz~!Q`VAVxGe#L zd~#pvZWYxtmL988P;rHpo%k0fAtw?&8rvB~8Af}Itv92PXhLgI!G)6sVPcgW=e5M-(}k)f zC)!@{d+vqc%)?PmLmI=wFRxl zlq#QFy#D8D1+0I$kZ<32;-~9i$&>hoPU%g;UF%d0c%`Yk1p6RWsY^N*;c)~B5k_-J zm<4Cj+RByyl&(xBQSpQe13!H=+vuP>`1(PE9jY+nsXnmPg8<7lW-6F^F3z9e#*9kG z=;!h$S-{q2-w*Cj+&+xu4}ZcvGTmthSmBOg;t|;p$bg$~&Tn|RqdHDvNE_7uYlk>b z7_j?7uA_0!8^oI}NpePO-FiAYKi)`SyqRXLap#|&T~91}()wewcK8sC*8~Uobqsmz zE~guTL#ZkV@#CXmTBwrYb>B8^|9F+l?)hGB$J`Emp{*LC69?0ioQ6*rM{K1xK)@!b z{vB;dt9Rs7UVlHZ(BuTLOniKN1f4h> z`cFkxQm%>f*?Qe~KPabqnomy!)Uw#xcrfis1tWo7+s2DIIf<8jC6FtFJxVqg=<~U# zaN9!f(Yb@YU0Qz#i4_tFLpJ-+fkvC9B?qP<&3hj26)Z0a8&||A{zSG&aXgS*$xb1S zt3L+XP3wf!j50eDa+eEuU8(3HTGb*lvw$H}W=Z&&71<)FgEenyhis@5z%~7Pjys>* zbww7}fhJl7H5H*?wbmYWX%8-6FS9{uYxheUL%|1>dAs)&)WpIzOQxKrbK?Mesd7Q4 zQuFYgq@`!Xx8)}LrS@d3&j7nt6#On`4-_W$B<0kaKJ8Ry)!q6vJM>~;8d#-7LnqtX zMmy$LUM5IxH!=(Set%cBgys_S>%IrCtkBYn`o^EKfDCjj`kim5M;u@t&#;G?-I38| zz4ZjX9F1_~Yfq{> z^kW1b-N6pSWMh5jHPx~3kCA#>Hb-EAHby);(in%I5#@d3-OWs}7)Qv!UHiQp&N;EE ziS<9vQ%q^LtAd&40(aavn_Y7;uv<7)NNvUDLiNlr6xQr_gbh6E{6Xc18zv=BiJK_D zZUiTMZp&`*ZZogS%=UNgds4oy{XplZ$qw3ODfuvVx$^F-CdAy4-*fYf*+1#di)0P} zZUcV-1dZl@Z1*p1^BF+8G1&T5a4TZ{_7-S;bZkuY8#@OY3^rr|GymDIpmv(eudLw!2`=R0a5WHIa zJM!vvVT!`@2>cKIw+s1PS+gP+{Yh9F1Pi-#xW6@}m?^p-ZnEt~lt=nq4BBW2Oq)4~ z44IEMG-Vk9)#xp2!BaCGXTuQKF$rVYWmr>|bt%lKcI?#Q4mIyX%bp*sJa1hxa|s&@ zXOnKmb4cpm-_+#)ds^2)eg$T*(llQDYAej*(LxfteC zQp~+AjCb$*lITJ1F>O*wRh5?0sjjK9-S;36I90AJGU_M?%|XB1|4)R*e^2ZM%3{|3yQ_(cE! literal 0 HcmV?d00001 diff --git a/docs/images/creating-a-new-detector/test-projects.png b/docs/images/creating-a-new-detector/test-projects.png new file mode 100644 index 0000000000000000000000000000000000000000..ae16f529bfebbd20dc9a3c936642aa638abd5458 GIT binary patch literal 31006 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 H>&gECvmB1% literal 0 HcmV?d00001 diff --git a/docs/images/creating-a-new-detector/vs-projects.png b/docs/images/creating-a-new-detector/vs-projects.png new file mode 100644 index 0000000000000000000000000000000000000000..ccb6f0398dc0ace0151425b3526889476048b2fd GIT binary patch literal 33645 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 JRwZH-_setup.py or *requirements.txt* files. + +## Detection strategy +Pip detection is performed by running the following code snippet on every setup.py: + +```python + import distutils.core; + setup = distutils.core.run_setup({setup.py}); + print(setup.install_requires); +``` + +The code above allows Pip detection to detect any runtime dependendies. + +*requirements.txt* files are parsed; a Git component is created for every *git+* url. + +For every top level component, Pip detection makes http calls to Pip in order to determine latest version available, as well as to resolve the dependency tree by parsing the *METADATA* file on a given release's *bdist_wheel* or *bdist_egg*. + +Full dependency graph generation is supported. + +## Known limitations +*Dev dependency* tagging is not supported. + +Pip detection will not run if *python* is unavailable. + +If no *bdist_wheel* or *bdist_egg* are available for a given component, dependencies will not be fetched. + +If no internet connection or a component cannot be found in Pypi, said component and its dependencies will be skipped. \ No newline at end of file diff --git a/docs/renewing-secrets.md b/docs/renewing-secrets.md new file mode 100644 index 000000000..d14793f83 --- /dev/null +++ b/docs/renewing-secrets.md @@ -0,0 +1,25 @@ +# Renewing secrets + +Almost all of our [workflows](../.github/workflows) require secrets and those secrets can be invalidated, deleted or expired so we need to know how to renew them. + +The secrets in use today in the BCDE repo can be found [here](https://github.com/microsoft/componentdetection-bcde/settings/secrets): + +* GH_PRIVATE_REPO_PAT + +## Before Starting + +Verify your account has sufficient permissions. You can do this by building BCDE locally. This will verify you have access to read the packages being created and read the private BCDE repo. + +## Renewing GH_PRIVATE_REPO_PAT + +1. Click this link: https://github.com/settings/tokens/new +1. (Optional) Name the token BCDE_GH_PRIVATE_REPO_PAT. This will make things easier to track in the future +1. Check the following permissions: + * Full `repo` scope + * `read:packages` scope +1. Click **Generate token** +1. Copy and paste that token into notepad once you see it because it will disappear as soon as you leave the page +1. Enable SSO for both GitHub and Microsoft organizations for the token +1. In the [BCDE secrets page](https://github.com/microsoft/componentdetection-bcde/settings/secrets) click update on **GH_PRIVATE_REPO_PAT** +1. Paste in your new token +1. Click **Update Secret** diff --git a/global.json b/global.json new file mode 100644 index 000000000..d43ba408d --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "6.0.100-rc.2.21505.57", + "allowPrerelease": true, + "rollForward": "latestMinor" + } +} diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 000000000..0eb245117 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,20 @@ + + + + + + true + snupkg + + + + true + true + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Common/AsyncExecution.cs b/src/Microsoft.ComponentDetection.Common/AsyncExecution.cs new file mode 100644 index 000000000..f72b85da5 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/AsyncExecution.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.ComponentDetection.Common +{ + public static class AsyncExecution + { + public static async Task ExecuteWithTimeoutAsync(Func> toExecute, TimeSpan timeout, CancellationToken cancellationToken) + { + if (toExecute == null) + { + throw new ArgumentNullException(nameof(toExecute)); + } + + Task work = Task.Run(toExecute); + + bool completedInTime = await Task.Run(() => work.Wait(timeout)); + if (!completedInTime) + { + throw new TimeoutException($"The execution did not complete in the alotted time ({timeout.TotalSeconds} seconds) and has been terminated prior to completion"); + } + + return work.Result; + } + + public static async Task ExecuteVoidWithTimeoutAsync(Action toExecute, TimeSpan timeout, CancellationToken cancellationToken) + { + if (toExecute == null) + { + throw new ArgumentNullException(nameof(toExecute)); + } + + Task work = Task.Run(toExecute); + bool completedInTime = await Task.Run(() => work.Wait(timeout)); + if (!completedInTime) + { + throw new TimeoutException($"The execution did not complete in the alotted time ({timeout.TotalSeconds} seconds) and has been terminated prior to completion"); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Common/Column.cs b/src/Microsoft.ComponentDetection.Common/Column.cs new file mode 100644 index 000000000..5d5ce9863 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Column.cs @@ -0,0 +1,11 @@ +namespace Microsoft.ComponentDetection.Common +{ + public class Column + { + public int Width { get; set; } + + public string Header { get; set; } + + public string Format { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Common/CommandLineInvocationService.cs b/src/Microsoft.ComponentDetection.Common/CommandLineInvocationService.cs new file mode 100644 index 000000000..59646e790 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/CommandLineInvocationService.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel; +using System.Composition; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.ComponentDetection.Common.Telemetry.Records; +using Microsoft.ComponentDetection.Contracts; + +namespace Microsoft.ComponentDetection.Common +{ + [Export(typeof(ICommandLineInvocationService))] + public class CommandLineInvocationService : ICommandLineInvocationService + { + private IDictionary commandLocatableCache = new ConcurrentDictionary(); + + public async Task CanCommandBeLocated(string command, IEnumerable additionalCandidateCommands = null, DirectoryInfo workingDirectory = null, params string[] parameters) + { + additionalCandidateCommands = additionalCandidateCommands ?? Enumerable.Empty(); + parameters = parameters ?? new string[0]; + var allCommands = new[] { command }.Concat(additionalCandidateCommands); + if (!commandLocatableCache.TryGetValue(command, out string validCommand)) + { + foreach (var commandToTry in allCommands) + { + using var record = new CommandLineInvocationTelemetryRecord(); + + var joinedParameters = string.Join(" ", parameters); + try + { + var result = await RunProcessAsync(commandToTry, joinedParameters, workingDirectory); + record.Track(result, commandToTry, joinedParameters); + + if (result.ExitCode == 0) + { + commandLocatableCache[command] = validCommand = commandToTry; + break; + } + } + catch (Exception ex) when (ex is Win32Exception || ex is FileNotFoundException || ex is PlatformNotSupportedException) + { + // When we get an exception indicating the command cannot be found. + record.Track(ex, commandToTry, joinedParameters); + } + } + } + + return !string.IsNullOrWhiteSpace(validCommand); + } + + public async Task ExecuteCommand(string command, IEnumerable additionalCandidateCommands = null, DirectoryInfo workingDirectory = null, params string[] parameters) + { + var isCommandLocatable = await CanCommandBeLocated(command, additionalCandidateCommands); + if (!isCommandLocatable) + { + throw new InvalidOperationException( + $"{nameof(ExecuteCommand)} was called with a command that could not be located: `{command}`!"); + } + + if (workingDirectory != null && !Directory.Exists(workingDirectory.FullName)) + { + throw new InvalidOperationException( + $"{nameof(ExecuteCommand)} was called with a working directory that could not be located: `{workingDirectory.FullName}`"); + } + + using var record = new CommandLineInvocationTelemetryRecord(); + + var pathToRun = commandLocatableCache[command]; + var joinedParameters = string.Join(" ", parameters); + try + { + var result = await RunProcessAsync(pathToRun, joinedParameters, workingDirectory); + record.Track(result, pathToRun, joinedParameters); + return result; + } + catch (Exception ex) + { + record.Track(ex, pathToRun, joinedParameters); + throw; + } + } + + public bool IsCommandLineExecution() + { + return true; + } + + private static Task RunProcessAsync(string fileName, string parameters, DirectoryInfo workingDirectory = null) + { + var tcs = new TaskCompletionSource(); + + if (fileName.EndsWith(".cmd") || fileName.EndsWith(".bat")) + { + // If a script attempts to find its location using "%dp0", that can return the wrong path (current + // working directory) unless the script is run via "cmd /C". An example is "ant.bat". + parameters = $"/C {fileName} {parameters}"; + fileName = "cmd.exe"; + } + + var process = new Process + { + StartInfo = + { + FileName = fileName, + Arguments = parameters, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + }, + EnableRaisingEvents = true, + }; + + if (workingDirectory != null) + { + process.StartInfo.WorkingDirectory = workingDirectory.FullName; + } + + string errorText = string.Empty; + string stdOutText = string.Empty; + + var t1 = new Task(() => + { + errorText = process.StandardError.ReadToEnd(); + }); + var t2 = new Task(() => + { + stdOutText = process.StandardOutput.ReadToEnd(); + }); + + process.Exited += (sender, args) => + { + Task.WaitAll(t1, t2); + tcs.SetResult(new CommandLineExecutionResult { ExitCode = process.ExitCode, StdErr = errorText, StdOut = stdOutText }); + process.Dispose(); + }; + + process.Start(); + t1.Start(); + t2.Start(); + + return tcs.Task; + } + + public async Task CanCommandBeLocated(string command, IEnumerable additionalCandidateCommands = null, params string[] parameters) + { + return await CanCommandBeLocated(command, additionalCandidateCommands, workingDirectory: null, parameters); + } + + public async Task ExecuteCommand(string command, IEnumerable additionalCandidateCommands = null, params string[] parameters) + { + return await ExecuteCommand(command, additionalCandidateCommands, workingDirectory: null, parameters); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Common/ComponentComparer.cs b/src/Microsoft.ComponentDetection.Common/ComponentComparer.cs new file mode 100644 index 000000000..8013fa93b --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/ComponentComparer.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Microsoft.ComponentDetection.Contracts.TypedComponent; + +namespace Microsoft.ComponentDetection.Common +{ + public class ComponentComparer : EqualityComparer + { + public override bool Equals(TypedComponent t0, TypedComponent t1) + { + return t0.Id.Equals(t1.Id); + } + + public override int GetHashCode(TypedComponent typedComponent) + { + return typedComponent.Id.GetHashCode(); + } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/ComponentStream.cs b/src/Microsoft.ComponentDetection.Common/ComponentStream.cs new file mode 100644 index 000000000..397434b65 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/ComponentStream.cs @@ -0,0 +1,14 @@ +using System.IO; +using Microsoft.ComponentDetection.Contracts; + +namespace Microsoft.ComponentDetection.Common +{ + public class ComponentStream : IComponentStream + { + public Stream Stream { get; set; } + + public string Pattern { get; set; } + + public string Location { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/ComponentStreamEnumerable.cs b/src/Microsoft.ComponentDetection.Common/ComponentStreamEnumerable.cs new file mode 100644 index 000000000..d2009f071 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/ComponentStreamEnumerable.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using Microsoft.ComponentDetection.Contracts; + +namespace Microsoft.ComponentDetection.Common +{ + public class ComponentStreamEnumerable : IEnumerable + { + private IEnumerable ToEnumerate { get; } + + private ILogger Logger { get; } + + public ComponentStreamEnumerable(IEnumerable fileEnumerable, ILogger logger) + { + ToEnumerate = fileEnumerable; + Logger = logger; + } + + public IEnumerator GetEnumerator() + { + foreach (var filePairing in ToEnumerate) + { + if (!filePairing.File.Exists) + { + Logger.LogWarning($"File {filePairing.File.FullName} does not exist on disk."); + yield break; + } + + using var stream = SafeOpenFile(filePairing.File); + + if (stream == null) + { + yield break; + } + + yield return new ComponentStream { Stream = stream, Pattern = filePairing.Pattern, Location = filePairing.File.FullName }; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + private Stream SafeOpenFile(FileInfo file) + { + try + { + return file.OpenRead(); + } + catch (UnauthorizedAccessException) + { + Logger.LogWarning($"Unauthorized access exception caught when trying to open {file.FullName}"); + return null; + } + catch (Exception e) + { + Logger.LogWarning($"Unhandled exception caught when trying to open {file.FullName}"); + Logger.LogException(e, isError: false); + return null; + } + } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/ComponentStreamEnumerableFactory.cs b/src/Microsoft.ComponentDetection.Common/ComponentStreamEnumerableFactory.cs new file mode 100644 index 000000000..9f415a0e5 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/ComponentStreamEnumerableFactory.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Composition; +using System.IO; +using Microsoft.ComponentDetection.Contracts; + +namespace Microsoft.ComponentDetection.Common +{ + [Export(typeof(IComponentStreamEnumerableFactory))] + [Shared] + public class ComponentStreamEnumerableFactory : IComponentStreamEnumerableFactory + { + [Import] + public ILogger Logger { get; set; } + + [Import] + public IPathUtilityService PathUtilityService { get; set; } + + public IEnumerable GetComponentStreams(DirectoryInfo directory, IEnumerable searchPatterns, ExcludeDirectoryPredicate directoryExclusionPredicate, bool recursivelyScanDirectories = true) + { + SafeFileEnumerable enumerable = new SafeFileEnumerable(directory, searchPatterns, Logger, PathUtilityService, directoryExclusionPredicate, recursivelyScanDirectories); + return new ComponentStreamEnumerable(enumerable, Logger); + } + + public IEnumerable GetComponentStreams(DirectoryInfo directory, Func fileMatchingPredicate, ExcludeDirectoryPredicate directoryExclusionPredicate, bool recursivelyScanDirectories = true) + { + SafeFileEnumerable enumerable = new SafeFileEnumerable(directory, fileMatchingPredicate, Logger, PathUtilityService, directoryExclusionPredicate, recursivelyScanDirectories); + return new ComponentStreamEnumerable(enumerable, Logger); + } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/ConsoleWritingService.cs b/src/Microsoft.ComponentDetection.Common/ConsoleWritingService.cs new file mode 100644 index 000000000..97c9c9dea --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/ConsoleWritingService.cs @@ -0,0 +1,14 @@ +using System; +using System.Composition; + +namespace Microsoft.ComponentDetection.Common +{ + [Export(typeof(IConsoleWritingService))] + public class ConsoleWritingService : IConsoleWritingService + { + public void Write(string content) + { + Console.Write(content); + } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/DependencyGraph/ComponentRecorder.cs b/src/Microsoft.ComponentDetection.Common/DependencyGraph/ComponentRecorder.cs new file mode 100644 index 000000000..b3bc210fd --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/DependencyGraph/ComponentRecorder.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Runtime.CompilerServices; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.TypedComponent; + +[assembly: InternalsVisibleTo("Microsoft.ComponentDetection.Common.Tests")] + +namespace Microsoft.ComponentDetection.Common.DependencyGraph +{ + public class ComponentRecorder : IComponentRecorder + { + private readonly ILogger log; + + private readonly ConcurrentBag singleFileRecorders = new ConcurrentBag(); + + private readonly bool enableManualTrackingOfExplicitReferences; + + public ComponentRecorder(ILogger log = null, bool enableManualTrackingOfExplicitReferences = true) + { + this.log = log; + this.enableManualTrackingOfExplicitReferences = enableManualTrackingOfExplicitReferences; + } + + public TypedComponent GetComponent(string componentId) + { + return singleFileRecorders.Select(x => x.GetComponent(componentId)?.Component).Where(x => x != null).FirstOrDefault(); + } + + public IEnumerable GetDetectedComponents() + { + IEnumerable detectedComponents; + if (singleFileRecorders == null) + { + return Enumerable.Empty(); + } + + detectedComponents = singleFileRecorders + .Select(singleFileRecorder => singleFileRecorder.GetDetectedComponents().Values) + .SelectMany(x => x) + .GroupBy(x => x.Component.Id) + .Select(grouping => + { + // We pick a winner here -- any stateful props could get lost at this point. Only stateful prop still outstanding is ContainerDetails. + var winningDetectedComponent = grouping.First(); + foreach (var component in grouping) + { + foreach (var containerDetailId in component.ContainerDetailIds) + { + winningDetectedComponent.ContainerDetailIds.Add(containerDetailId); + } + } + + return winningDetectedComponent; + }) + .ToImmutableList(); + + return detectedComponents; + } + + public ISingleFileComponentRecorder CreateSingleFileComponentRecorder(string location) + { + if (string.IsNullOrWhiteSpace(location)) + { + throw new ArgumentNullException(nameof(location)); + } + + var matching = singleFileRecorders.FirstOrDefault(x => x.ManifestFileLocation == location); + if (matching == null) + { + matching = new SingleFileComponentRecorder(location, this, enableManualTrackingOfExplicitReferences, log); + singleFileRecorders.Add(matching); + } + + return matching; + } + + internal DependencyGraph GetDependencyGraphForLocation(string location) + { + return singleFileRecorders.Single(x => x.ManifestFileLocation == location).DependencyGraph; + } + + public IReadOnlyDictionary GetDependencyGraphsByLocation() + { + return singleFileRecorders.Where(x => x.DependencyGraph.HasComponents()) + .ToImmutableDictionary(x => x.ManifestFileLocation, x => x.DependencyGraph as IDependencyGraph); + } + + public class SingleFileComponentRecorder : ISingleFileComponentRecorder + { + private readonly ILogger log; + + public string ManifestFileLocation { get; } + + internal DependencyGraph DependencyGraph { get; } + + IDependencyGraph ISingleFileComponentRecorder.DependencyGraph => DependencyGraph; + + private readonly ConcurrentDictionary detectedComponentsInternal = new ConcurrentDictionary(); + + private readonly ComponentRecorder recorder; + + private object registerUsageLock = new object(); + + public SingleFileComponentRecorder(string location, ComponentRecorder recorder, bool enableManualTrackingOfExplicitReferences, ILogger log) + { + ManifestFileLocation = location; + this.recorder = recorder; + this.log = log; + DependencyGraph = new DependencyGraph(enableManualTrackingOfExplicitReferences); + } + + public DetectedComponent GetComponent(string componentId) + { + if (detectedComponentsInternal.TryGetValue(componentId, out var detectedComponent)) + { + return detectedComponent; + } + + return null; + } + + public IReadOnlyDictionary GetDetectedComponents() + { + // Should this be immutable? + return detectedComponentsInternal; + } + + public void RegisterUsage( + DetectedComponent detectedComponent, + bool isExplicitReferencedDependency = false, + string parentComponentId = null, + bool? isDevelopmentDependency = null) + { + if (detectedComponent == null) + { + throw new ArgumentNullException(paramName: nameof(detectedComponent)); + } + + if (detectedComponent.Component == null) + { + throw new ArgumentException(Resources.MissingComponentId); + } + +#if DEBUG + if (detectedComponent.FilePaths?.Any() ?? false) + { + log?.LogWarning("Detector should not populate DetectedComponent.FilePaths!"); + } + + if (detectedComponent.DependencyRoots?.Any() ?? false) + { + log?.LogWarning("Detector should not populate DetectedComponent.DependencyRoots!"); + } + + if (detectedComponent.DevelopmentDependency.HasValue) + { + log?.LogWarning("Detector should not populate DetectedComponent.DevelopmentDependency!"); + } +#endif + + string componentId = detectedComponent.Component.Id; + DetectedComponent storedComponent = null; + lock (registerUsageLock) + { + storedComponent = detectedComponentsInternal.GetOrAdd(componentId, detectedComponent); + AddComponentToGraph(ManifestFileLocation, detectedComponent, isExplicitReferencedDependency, parentComponentId, isDevelopmentDependency); + } + } + + public void AddAdditionalRelatedFile(string relatedFilePath) + { + DependencyGraph.AddAdditionalRelatedFile(relatedFilePath); + } + + public IList GetAdditionalRelatedFiles() + { + return DependencyGraph.GetAdditionalRelatedFiles().ToImmutableList(); + } + + public IComponentRecorder GetParentComponentRecorder() + { + return recorder; + } + + private void AddComponentToGraph(string location, DetectedComponent detectedComponent, bool isExplicitReferencedDependency, string parentComponentId, bool? isDevelopmentDependency) + { + var componentNode = new DependencyGraph.ComponentRefNode + { + Id = detectedComponent.Component.Id, + IsExplicitReferencedDependency = isExplicitReferencedDependency, + IsDevelopmentDependency = isDevelopmentDependency, + }; + + DependencyGraph.AddComponent(componentNode, parentComponentId); + } + } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/DependencyGraph/DependencyGraph.cs b/src/Microsoft.ComponentDetection.Common/DependencyGraph/DependencyGraph.cs new file mode 100644 index 000000000..67a7be3d0 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/DependencyGraph/DependencyGraph.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Runtime.CompilerServices; +using Microsoft.ComponentDetection.Contracts; + +[assembly: InternalsVisibleTo("Microsoft.ComponentDetection.Common.Tests")] + +namespace Microsoft.ComponentDetection.Common.DependencyGraph +{ + internal class DependencyGraph : IDependencyGraph + { + private ConcurrentDictionary componentNodes; + + internal ConcurrentDictionary AdditionalRelatedFiles { get; } = new ConcurrentDictionary(); + + private bool enableManualTrackingOfExplicitReferences; + + public DependencyGraph(bool enableManualTrackingOfExplicitReferences) + { + componentNodes = new ConcurrentDictionary(); + this.enableManualTrackingOfExplicitReferences = enableManualTrackingOfExplicitReferences; + } + + public void AddComponent(ComponentRefNode componentNode, string parentComponentId = null) + { + if (componentNode == null) + { + throw new ArgumentNullException(nameof(componentNode)); + } + + if (string.IsNullOrWhiteSpace(componentNode.Id)) + { + throw new ArgumentNullException(nameof(componentNode.Id)); + } + + componentNodes.AddOrUpdate(componentNode.Id, componentNode, (key, currentNode) => + { + currentNode.IsExplicitReferencedDependency |= componentNode.IsExplicitReferencedDependency; + + // If incoming component has a dev dependency value, and it with whatever is in storage. Otherwise, leave storage alone. + if (componentNode.IsDevelopmentDependency.HasValue) + { + currentNode.IsDevelopmentDependency = currentNode.IsDevelopmentDependency.GetValueOrDefault(true) && componentNode.IsDevelopmentDependency.Value; + } + + return currentNode; + }); + + AddDependency(componentNode.Id, parentComponentId); + } + + public bool Contains(string componentId) + { + return componentNodes.ContainsKey(componentId); + } + + public ICollection GetDependenciesForComponent(string componentId) + { + return componentNodes[componentId].DependencyIds; + } + + public ICollection GetExplicitReferencedDependencyIds(string componentId) + { + if (string.IsNullOrWhiteSpace(componentId)) + { + throw new ArgumentNullException(nameof(componentId)); + } + + if (!componentNodes.TryGetValue(componentId, out var componentRef)) + { + throw new ArgumentException(string.Format(Resources.MissingNodeInDependencyGraph, componentId), paramName: nameof(componentId)); + } + + IList explicitReferencedDependencyIds = new List(); + + GetExplicitReferencedDependencies(componentRef, explicitReferencedDependencyIds, new HashSet()); + + return explicitReferencedDependencyIds; + } + + public void AddAdditionalRelatedFile(string additionalRelatedFile) + { + AdditionalRelatedFiles.AddOrUpdate(additionalRelatedFile, 0, (notUsed, notUsed2) => 0); + } + + public HashSet GetAdditionalRelatedFiles() + { + return AdditionalRelatedFiles.Keys.ToImmutableHashSet().ToHashSet(); + } + + public bool HasComponents() + { + return componentNodes.Count > 0; + } + + public bool? IsDevelopmentDependency(string componentId) + { + return componentNodes[componentId].IsDevelopmentDependency; + } + + public IEnumerable GetAllExplicitlyReferencedComponents() + { + return componentNodes.Values + .Where(componentRefNode => IsExplicitReferencedDependency(componentRefNode)) + .Select(componentRefNode => componentRefNode.Id); + } + + private void GetExplicitReferencedDependencies(ComponentRefNode component, IList explicitReferencedDependencyIds, ISet visited) + { + if (IsExplicitReferencedDependency(component)) + { + explicitReferencedDependencyIds.Add(component.Id); + } + + visited.Add(component.Id); + + foreach (var parentId in component.DependedOnByIds) + { + if (!visited.Contains(parentId)) + { + GetExplicitReferencedDependencies(componentNodes[parentId], explicitReferencedDependencyIds, visited); + } + } + } + + private bool IsExplicitReferencedDependency(ComponentRefNode component) + { + return (enableManualTrackingOfExplicitReferences && component.IsExplicitReferencedDependency) || + (!enableManualTrackingOfExplicitReferences && !component.DependedOnByIds.Any()); + } + + private void AddDependency(string componentId, string parentComponentId) + { + if (string.IsNullOrWhiteSpace(parentComponentId)) + { + return; + } + + if (!componentNodes.TryGetValue(parentComponentId, out var parentComponentRefNode)) + { + throw new ArgumentException(string.Format(Resources.MissingNodeInDependencyGraph, parentComponentId), nameof(parentComponentId)); + } + + parentComponentRefNode.DependencyIds.Add(componentId); + componentNodes[componentId].DependedOnByIds.Add(parentComponentId); + } + + IEnumerable IDependencyGraph.GetDependenciesForComponent(string componentId) + { + return GetDependenciesForComponent(componentId).ToImmutableList(); + } + + IEnumerable IDependencyGraph.GetComponents() + { + return componentNodes.Keys.ToImmutableList(); + } + + bool IDependencyGraph.IsComponentExplicitlyReferenced(string componentId) + { + return IsExplicitReferencedDependency(componentNodes[componentId]); + } + + internal class ComponentRefNode + { + internal bool IsExplicitReferencedDependency { get; set; } + + internal string Id { get; set; } + + internal ISet DependencyIds { get; private set; } + + internal ISet DependedOnByIds { get; private set; } + + internal bool? IsDevelopmentDependency { get; set; } + + internal ComponentRefNode() + { + DependencyIds = new HashSet(); + DependedOnByIds = new HashSet(); + } + } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/DetectorDependencies.cs b/src/Microsoft.ComponentDetection.Common/DetectorDependencies.cs new file mode 100644 index 000000000..d85d5805d --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/DetectorDependencies.cs @@ -0,0 +1,30 @@ +using System.Composition; +using Microsoft.ComponentDetection.Contracts; + +namespace Microsoft.ComponentDetection.Common +{ + [Export(typeof(IDetectorDependencies))] + public class DetectorDependencies : IDetectorDependencies + { + [Import] + public ILogger Logger { get; set; } + + [Import] + public IComponentStreamEnumerableFactory ComponentStreamEnumerableFactory { get; set; } + + [Import] + public IPathUtilityService PathUtilityService { get; set; } + + [Import] + public ICommandLineInvocationService CommandLineInvocationService { get; set; } + + [Import] + public IFileUtilityService FileUtilityService { get; set; } + + [Import] + public IObservableDirectoryWalkerFactory DirectoryWalkerFactory { get; set; } + + [Import] + public IDockerService DockerService { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/DirectoryItemFacade.cs b/src/Microsoft.ComponentDetection.Common/DirectoryItemFacade.cs new file mode 100644 index 000000000..60fd92bee --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/DirectoryItemFacade.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.ComponentDetection.Contracts; + +namespace Microsoft.ComponentDetection.Common +{ + [DebuggerDisplay("{Name}")] + public class DirectoryItemFacade + { + public string Name { get; set; } + + public List Directories { get; set; } + + public List Files { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/DockerService.cs b/src/Microsoft.ComponentDetection.Common/DockerService.cs new file mode 100644 index 000000000..e1bef86db --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/DockerService.cs @@ -0,0 +1,240 @@ +using System; +using System.Collections.Generic; +using System.Composition; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Docker.DotNet; +using Docker.DotNet.Models; +using Microsoft.ComponentDetection.Common.Telemetry.Records; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.BcdeModels; +using Newtonsoft.Json; + +namespace Microsoft.ComponentDetection.Common +{ + [Export(typeof(IDockerService))] + public class DockerService : IDockerService + { + private static readonly DockerClient Client = new DockerClientConfiguration().CreateClient(); + + private static int incrementingContainerId; + + [Import] + public ILogger Logger { get; set; } + + // Base image annotations from ADO dockerTask + private const string BaseImageRefAnnotation = "image.base.ref.name"; + private const string BaseImageDigestAnnotation = "image.base.digest"; + + public async Task CanPingDockerAsync(CancellationToken cancellationToken = default) + { + try + { + await Client.System.PingAsync(cancellationToken); + return true; + } + catch (Exception e) + { + Logger.LogException(e, false); + return false; + } + } + + public async Task CanRunLinuxContainersAsync(CancellationToken cancellationToken = default) + { + using var record = new DockerServiceSystemInfoTelemetryRecord(); + if (!await CanPingDockerAsync(cancellationToken)) + { + return false; + } + + try + { + var systemInfoResponse = await Client.System.GetSystemInfoAsync(cancellationToken); + record.SystemInfo = JsonConvert.SerializeObject(systemInfoResponse); + return string.Equals(systemInfoResponse.OSType, "linux", StringComparison.OrdinalIgnoreCase); + } + catch (Exception e) + { + record.ExceptionMessage = e.Message; + } + + return false; + } + + public async Task ImageExistsLocallyAsync(string image, CancellationToken cancellationToken = default) + { + using var record = new DockerServiceImageExistsLocallyTelemetryRecord + { + Image = image, + }; + try + { + var imageInspectResponse = await Client.Images.InspectImageAsync(image, cancellationToken); + record.ImageInspectResponse = JsonConvert.SerializeObject(imageInspectResponse); + return true; + } + catch (Exception e) + { + record.ExceptionMessage = e.Message; + return false; + } + } + + public async Task TryPullImageAsync(string image, CancellationToken cancellationToken = default) + { + using var record = new DockerServiceTryPullImageTelemetryRecord + { + Image = image, + }; + var parameters = new ImagesCreateParameters + { + FromImage = image, + }; + try + { + var createImageProgress = new List(); + var progress = new Progress(message => { + createImageProgress.Add(JsonConvert.SerializeObject(message)); + }); + await Client.Images.CreateImageAsync(parameters, null, progress, cancellationToken); + record.CreateImageProgress = JsonConvert.SerializeObject(createImageProgress); + return true; + } + catch (Exception e) + { + record.ExceptionMessage = e.Message; + return false; + } + } + + public async Task InspectImageAsync(string image, CancellationToken cancellationToken = default) + { + using var record = new DockerServiceInspectImageTelemetryRecord + { + Image = image, + }; + try + { + var imageInspectResponse = await Client.Images.InspectImageAsync(image, cancellationToken); + record.ImageInspectResponse = JsonConvert.SerializeObject(imageInspectResponse); + + var baseImageRef = string.Empty; + var baseImageDigest = string.Empty; + + imageInspectResponse.Config.Labels?.TryGetValue(BaseImageRefAnnotation, out baseImageRef); + imageInspectResponse.Config.Labels?.TryGetValue(BaseImageDigestAnnotation, out baseImageDigest); + + record.BaseImageRef = baseImageRef; + record.BaseImageDigest = baseImageDigest; + + var layers = imageInspectResponse.RootFS?.Layers + .Select((diffId, index) => + new DockerLayer + { + DiffId = diffId, + LayerIndex = index, + }); + + return new ContainerDetails + { + Id = GetContainerId(), + ImageId = imageInspectResponse.ID, + Digests = imageInspectResponse.RepoDigests, + Tags = imageInspectResponse.RepoTags, + CreatedAt = imageInspectResponse.Created, + BaseImageDigest = baseImageDigest, + BaseImageRef = baseImageRef, + Layers = layers ?? Enumerable.Empty(), + }; + } + catch (Exception e) + { + record.ExceptionMessage = e.Message; + return null; + } + } + + public async Task<(string stdout, string stderr)> CreateAndRunContainerAsync(string image, IList command, + CancellationToken cancellationToken = default) + { + using var record = new DockerServiceTelemetryRecord + { + Image = image, + Command = JsonConvert.SerializeObject(command), + }; + await TryPullImageAsync(image, cancellationToken); + var container = await CreateContainerAsync(image, command, cancellationToken); + record.Container = JsonConvert.SerializeObject(container); + var stream = await AttachContainerAsync(container.ID, cancellationToken); + await StartContainerAsync(container.ID, cancellationToken); + var (stdout, stderr) = await stream.ReadOutputToEndAsync(cancellationToken); + record.Stdout = stdout; + record.Stderr = stderr; + await RemoveContainerAsync(container.ID, cancellationToken); + return (stdout, stderr); + } + + private static async Task CreateContainerAsync(string image, IList command, + CancellationToken cancellationToken = default) + { + var parameters = new CreateContainerParameters + { + Image = image, + Cmd = command, + NetworkDisabled = true, + HostConfig = new HostConfig + { + CapDrop = new List + { + "all", + }, + SecurityOpt = new List + { + "no-new-privileges", + }, + Binds = new List + { + $"{Path.GetTempPath()}:/tmp", + "/var/run/docker.sock:/var/run/docker.sock", + }, + }, + }; + return await Client.Containers.CreateContainerAsync(parameters, cancellationToken); + } + + private static async Task AttachContainerAsync(string containerId, CancellationToken cancellationToken = default) + { + var parameters = new ContainerAttachParameters + { + Stdout = true, + Stderr = true, + Stream = true, + }; + return await Client.Containers.AttachContainerAsync(containerId, false, parameters, cancellationToken); + } + + private static async Task StartContainerAsync(string containerId, CancellationToken cancellationToken = default) + { + var parameters = new ContainerStartParameters(); + await Client.Containers.StartContainerAsync(containerId, parameters, cancellationToken); + } + + private static async Task RemoveContainerAsync(string containerId, CancellationToken cancellationToken = default) + { + var parameters = new ContainerRemoveParameters + { + Force = true, + RemoveVolumes = true, + }; + await Client.Containers.RemoveContainerAsync(containerId, parameters, cancellationToken); + } + + private static int GetContainerId() + { + return Interlocked.Increment(ref incrementingContainerId); + } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Exceptions/InvalidUserInputException.cs b/src/Microsoft.ComponentDetection.Common/Exceptions/InvalidUserInputException.cs new file mode 100644 index 000000000..cac98953f --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Exceptions/InvalidUserInputException.cs @@ -0,0 +1,12 @@ +using System; + +namespace Microsoft.ComponentDetection.Common.Exceptions +{ + public class InvalidUserInputException : Exception + { + public InvalidUserInputException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/FastDirectoryWalkerFactory.cs b/src/Microsoft.ComponentDetection.Common/FastDirectoryWalkerFactory.cs new file mode 100644 index 000000000..8ce3c554a --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/FastDirectoryWalkerFactory.cs @@ -0,0 +1,292 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Composition; +using System.Diagnostics; +using System.IO; +using System.IO.Enumeration; +using System.Linq; +using System.Reactive.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.Internal; + +namespace Microsoft.ComponentDetection.Common +{ + [Export(typeof(IObservableDirectoryWalkerFactory))] + [Export(typeof(FastDirectoryWalkerFactory))] + [Shared] + public class FastDirectoryWalkerFactory : IObservableDirectoryWalkerFactory + { + private readonly ConcurrentDictionary>> pendingScans = new ConcurrentDictionary>>(); + + [Import] + public ILogger Logger { get; set; } + + [Import] + public IPathUtilityService PathUtilityService { get; set; } + + public IObservable GetDirectoryScanner(DirectoryInfo root, ConcurrentDictionary scannedDirectories, ExcludeDirectoryPredicate directoryExclusionPredicate, IEnumerable filePatterns = null, bool recurse = true) + { + return Observable.Create(s => + { + if (!root.Exists) + { + Logger?.LogError($"Root directory doesn't exist: {root.FullName}"); + s.OnCompleted(); + return Task.CompletedTask; + } + + PatternMatchingUtility.FilePatternMatcher fileIsMatch = null; + + if (filePatterns == null || !filePatterns.Any()) + { + fileIsMatch = span => true; + } + else + { + fileIsMatch = PatternMatchingUtility.GetFilePatternMatcher(filePatterns); + } + + var sw = Stopwatch.StartNew(); + + Logger?.LogInfo($"Starting enumeration of {root.FullName}"); + + var fileCount = 0; + var directoryCount = 0; + + var shouldRecurse = new FileSystemEnumerable.FindPredicate((ref FileSystemEntry entry) => + { + if (!recurse) + { + return false; + } + + if (!(entry.ToFileSystemInfo() is DirectoryInfo di)) + { + return false; + } + + var realDirectory = di; + + if (di.Attributes.HasFlag(FileAttributes.ReparsePoint)) + { + var realPath = PathUtilityService.ResolvePhysicalPath(di.FullName); + + realDirectory = new DirectoryInfo(realPath); + } + + if (!scannedDirectories.TryAdd(realDirectory.FullName, true)) + { + return false; + } + + if (directoryExclusionPredicate != null) + { + return !directoryExclusionPredicate(entry.FileName.ToString(), entry.Directory.ToString()); + } + + return true; + }); + + var initialIterator = new FileSystemEnumerable(root.FullName, Transform, new EnumerationOptions() + { + RecurseSubdirectories = false, + IgnoreInaccessible = true, + ReturnSpecialDirectories = false, + }) + { + ShouldIncludePredicate = (ref FileSystemEntry entry) => + { + if (!entry.IsDirectory && fileIsMatch(entry.FileName)) + { + return true; + } + + return shouldRecurse(ref entry); + }, + }; + + var observables = new List>(); + + var initialFiles = new List(); + var initialDirectories = new List(); + + foreach (var fileSystemInfo in initialIterator) + { + if (fileSystemInfo is FileInfo fi) + { + initialFiles.Add(fi); + } + else if (fileSystemInfo is DirectoryInfo di) + { + initialDirectories.Add(di); + } + } + + observables.Add(Observable.Create(sub => + { + foreach (var fileInfo in initialFiles) + { + sub.OnNext(fileInfo); + } + + sub.OnCompleted(); + + return Task.CompletedTask; + })); + + if (recurse) + { + observables.Add(Observable.Create(async observer => + { + var scan = new ActionBlock( + di => + { + var enumerator = new FileSystemEnumerable(di.FullName, Transform, new EnumerationOptions() + { + RecurseSubdirectories = true, + IgnoreInaccessible = true, + ReturnSpecialDirectories = false, + }) + { + ShouldRecursePredicate = shouldRecurse, + }; + + foreach (var fileSystemInfo in enumerator) + { + observer.OnNext(fileSystemInfo); + } + }, new ExecutionDataflowBlockOptions() { MaxDegreeOfParallelism = Environment.ProcessorCount }); + + foreach (var directoryInfo in initialDirectories) + { + scan.Post(directoryInfo); + } + + scan.Complete(); + await scan.Completion; + observer.OnCompleted(); + })); + } + + return observables.Concat().Subscribe( + info => + { + if (info is FileInfo) + { + Interlocked.Increment(ref fileCount); + } + else + { + Interlocked.Increment(ref directoryCount); + } + + s.OnNext(info); + }, () => + { + sw.Stop(); + Logger?.LogInfo($"Enumerated {fileCount} files and {directoryCount} directories in {sw.Elapsed}"); + s.OnCompleted(); + }); + }); + } + + private FileSystemInfo Transform(ref FileSystemEntry entry) + { + return entry.ToFileSystemInfo(); + } + + private IObservable CreateDirectoryWalker(DirectoryInfo di, ExcludeDirectoryPredicate directoryExclusionPredicate, int minimumConnectionCount, IEnumerable filePatterns) + { + return GetDirectoryScanner(di, new ConcurrentDictionary(), directoryExclusionPredicate, filePatterns, true).Replay() // Returns a replay subject which will republish anything found to new subscribers. + .AutoConnect(minimumConnectionCount); // Specifies that this connectable observable should start when minimumConnectionCount subscribe. + } + + private bool MatchesAnyPattern(FileInfo fi, params string[] searchPatterns) + { + return searchPatterns != null && searchPatterns.Any(sp => PathUtilityService.MatchesPattern(sp, fi.Name)); + } + + ///

+ /// Initialized an observable file enumerator. + /// + /// Root directory to scan. + /// + /// Number of observers that need to subscribe before the observable connects and starts enumerating. + /// Pattern used to filter files. + public void Initialize(DirectoryInfo root, ExcludeDirectoryPredicate directoryExclusionPredicate, int minimumConnectionCount, IEnumerable filePatterns = null) + { + pendingScans.GetOrAdd(root, new Lazy>(() => CreateDirectoryWalker(root, directoryExclusionPredicate, minimumConnectionCount, filePatterns))); + } + + public IObservable Subscribe(DirectoryInfo root, IEnumerable patterns) + { + var patternArray = patterns.ToArray(); + + if (pendingScans.TryGetValue(root, out var scannerObservable)) + { + Logger.LogVerbose(string.Join(":", patterns)); + + var inner = scannerObservable.Value.Where(fsi => + { + if (fsi is FileInfo fi) + { + return MatchesAnyPattern(fi, patternArray); + } + else + { + return true; + } + }); + + return inner; + } + + throw new InvalidOperationException("Subscribe called without initializing scanner"); + } + + public IObservable GetFilteredComponentStreamObservable(DirectoryInfo root, IEnumerable patterns, IComponentRecorder componentRecorder) + { + var observable = Subscribe(root, patterns).OfType().SelectMany(f => patterns.Select(sp => new + { + SearchPattern = sp, + File = f, + })).Where(x => + { + var searchPattern = x.SearchPattern; + var fileName = x.File.Name; + + return PathUtilityService.MatchesPattern(searchPattern, fileName); + }).Where(x => x.File.Exists) + .Select(x => + { + var lazyComponentStream = new LazyComponentStream(x.File, x.SearchPattern, Logger); + return new ProcessRequest + { + ComponentStream = lazyComponentStream, + SingleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder(lazyComponentStream.Location), + }; + }); + + return observable; + } + + public void StartScan(DirectoryInfo root) + { + if (pendingScans.TryRemove(root, out var scannerObservable)) + { + // scannerObservable.Connect(); + } + + throw new InvalidOperationException("StartScan called without initializing scanner"); + } + + public void Reset() + { + pendingScans.Clear(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Common/FileUtilityService.cs b/src/Microsoft.ComponentDetection.Common/FileUtilityService.cs new file mode 100644 index 000000000..f61c9c627 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/FileUtilityService.cs @@ -0,0 +1,35 @@ +using System.Composition; +using System.IO; +using Microsoft.ComponentDetection.Contracts; + +namespace Microsoft.ComponentDetection.Common +{ + /// + /// Wraps some common file operations for easier testability. This interface is *only used by the command line driven app*. + /// + [Export(typeof(IFileUtilityService))] + [Export(typeof(FileUtilityService))] + [Shared] + public class FileUtilityService : IFileUtilityService + { + public string ReadAllText(string filePath) + { + return File.ReadAllText(filePath); + } + + public string ReadAllText(FileInfo file) + { + return File.ReadAllText(file.FullName); + } + + public bool Exists(string fileName) + { + return File.Exists(fileName); + } + + public Stream MakeFileStream(string fileName) + { + return new FileStream(fileName, FileMode.Open, FileAccess.Read); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Common/FileWritingService.cs b/src/Microsoft.ComponentDetection.Common/FileWritingService.cs new file mode 100644 index 000000000..a39b47211 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/FileWritingService.cs @@ -0,0 +1,74 @@ +using System; +using System.Composition; +using System.IO; +using Microsoft.ComponentDetection.Common.Exceptions; + +namespace Microsoft.ComponentDetection.Common +{ + [Export(typeof(IFileWritingService))] + [Export(typeof(FileWritingService))] + [Shared] + public class FileWritingService : IFileWritingService + { + private object lockObject = new object(); + private string timestamp = DateTime.Now.ToString(TimestampFormatString); + public const string TimestampFormatString = "yyyyMMddHHmmss"; + + public string BasePath { get; private set; } + + public void Init(string basePath) + { + if (!string.IsNullOrEmpty(basePath) && !Directory.Exists(basePath)) + { + throw new InvalidUserInputException($"The path {basePath} does not exist.", new DirectoryNotFoundException()); + } + + BasePath = string.IsNullOrEmpty(basePath) ? Path.GetTempPath() : basePath; + } + + public void AppendToFile(string relativeFilePath, string text) + { + relativeFilePath = ResolveFilePath(relativeFilePath); + + lock (lockObject) + { + File.AppendAllText(relativeFilePath, text); + } + } + + public void WriteFile(string relativeFilePath, string text) + { + relativeFilePath = ResolveFilePath(relativeFilePath); + + lock (lockObject) + { + File.WriteAllText(relativeFilePath, text); + } + } + + public void WriteFile(FileInfo absolutePath, string text) + { + File.WriteAllText(absolutePath.FullName, text); + } + + public string ResolveFilePath(string relativeFilePath) + { + EnsureInit(); + if (relativeFilePath.Contains("{timestamp}")) + { + relativeFilePath = relativeFilePath.Replace("{timestamp}", timestamp); + } + + relativeFilePath = Path.Combine(BasePath, relativeFilePath); + return relativeFilePath; + } + + private void EnsureInit() + { + if (string.IsNullOrEmpty(BasePath)) + { + throw new InvalidOperationException("Base path has not yet been initialized in File Writing Service!"); + } + } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/IConsoleWritingService.cs b/src/Microsoft.ComponentDetection.Common/IConsoleWritingService.cs new file mode 100644 index 000000000..c190a61d0 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/IConsoleWritingService.cs @@ -0,0 +1,7 @@ +namespace Microsoft.ComponentDetection.Common +{ + public interface IConsoleWritingService + { + void Write(string content); + } +} diff --git a/src/Microsoft.ComponentDetection.Common/IFileWritingService.cs b/src/Microsoft.ComponentDetection.Common/IFileWritingService.cs new file mode 100644 index 000000000..1a36022a1 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/IFileWritingService.cs @@ -0,0 +1,16 @@ +using System.IO; + +namespace Microsoft.ComponentDetection.Common +{ + // All file paths are relative and will replace occurrences of {timestamp} with the shared file timestamp. + public interface IFileWritingService + { + void AppendToFile(string relativeFilePath, string text); + + void WriteFile(string relativeFilePath, string text); + + void WriteFile(FileInfo relativeFilePath, string text); + + string ResolveFilePath(string relativeFilePath); + } +} diff --git a/src/Microsoft.ComponentDetection.Common/ISafeFileEnumerableFactory.cs b/src/Microsoft.ComponentDetection.Common/ISafeFileEnumerableFactory.cs new file mode 100644 index 000000000..6098a3ed8 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/ISafeFileEnumerableFactory.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.IO; +using Microsoft.ComponentDetection.Contracts; + +namespace Microsoft.ComponentDetection.Common +{ + /// Factory for generating safe file enumerables. + public interface ISafeFileEnumerableFactory + { + /// Creates a "safe" file enumerable, which provides logging while evaluating search patterns on a known directory structure. + /// The directory to search "from", e.g. the top level directory being searched. + /// The patterns to use in the search. + /// Predicate which indicates which directories should be excluded. + /// A FileInfo enumerable that should be iterated over, containing all valid files given the input patterns and directory exclusions. + IEnumerable CreateSafeFileEnumerable(DirectoryInfo directory, IEnumerable searchPatterns, ExcludeDirectoryPredicate directoryExclusionPredicate); + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Common/LazyComponentStream.cs b/src/Microsoft.ComponentDetection.Common/LazyComponentStream.cs new file mode 100644 index 000000000..5b15d112d --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/LazyComponentStream.cs @@ -0,0 +1,52 @@ +using System; +using System.IO; +using Microsoft.ComponentDetection.Contracts; + +namespace Microsoft.ComponentDetection.Common +{ + public class LazyComponentStream : IComponentStream + { + private readonly FileInfo fileInfo; + private readonly Lazy fileBuffer; + private readonly ILogger logger; + + private byte[] SafeOpenFile() + { + try + { + using var fs = fileInfo.OpenRead(); + + var buffer = new byte[fileInfo.Length]; + fs.Read(buffer, 0, (int)fileInfo.Length); + + return buffer; + } + catch (UnauthorizedAccessException) + { + logger?.LogWarning($"Unauthorized access exception caught when trying to open {fileInfo.FullName}"); + } + catch (Exception e) + { + logger?.LogWarning($"Unhandled exception caught when trying to open {fileInfo.FullName}"); + logger?.LogException(e, isError: false); + } + + return new byte[0]; + } + + public LazyComponentStream(FileInfo fileInfo, string pattern, ILogger logger) + { + Pattern = pattern; + Location = fileInfo.FullName; + this.fileInfo = fileInfo; + this.logger = logger; + fileBuffer = new Lazy(SafeOpenFile); + } + + public Stream Stream => new MemoryStream(fileBuffer.Value); + + public string Pattern { get; set; } + + public string Location { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Logger.cs b/src/Microsoft.ComponentDetection.Common/Logger.cs new file mode 100644 index 000000000..fc2977e63 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Logger.cs @@ -0,0 +1,154 @@ +using System; +using System.Composition; +using System.Runtime.CompilerServices; +using Microsoft.ComponentDetection.Common.Telemetry.Records; +using Microsoft.ComponentDetection.Contracts; + +using static System.Environment; + +namespace Microsoft.ComponentDetection.Common +{ + [Export(typeof(ILogger))] + [Export(typeof(Logger))] + [Shared] + public class Logger : ILogger + { + [Import] + public IFileWritingService FileWritingService { get; set; } + + [Import] + public IConsoleWritingService ConsoleWriter { get; set; } + + private VerbosityMode Verbosity { get; set; } + + private bool WriteToFile { get; set; } + + public const string LogRelativePath = "GovCompDisc_Log_{timestamp}.log"; + + public void Init(VerbosityMode verbosity) + { + WriteToFile = true; + Verbosity = verbosity; + try + { + FileWritingService.WriteFile(LogRelativePath, string.Empty); + LogInfo($"Log file: {FileWritingService.ResolveFilePath(LogRelativePath)}"); + } + catch (Exception) + { + WriteToFile = false; + LogError("There was an issue writing to the log file, for the remainder of execution verbose output will be written to the console."); + Verbosity = VerbosityMode.Verbose; + } + } + + public void LogCreateLoggingGroup() + { + PrintToConsole(NewLine, VerbosityMode.Normal); + AppendToFile(NewLine); + } + + public void LogWarning(string message) + { + LogInternal("WARN", message); + } + + public void LogInfo(string message) + { + LogInternal("INFO", message); + } + + public void LogVerbose(string message) + { + LogInternal("VERBOSE", message, VerbosityMode.Verbose); + } + + public void LogError(string message) + { + LogInternal("ERROR", message, VerbosityMode.Quiet); + } + + private void LogInternal(string prefix, string message, VerbosityMode verbosity = VerbosityMode.Normal) + { + var formattedPrefix = string.IsNullOrWhiteSpace(prefix) ? string.Empty : $"[{prefix}] "; + var text = $"{formattedPrefix}{message} {NewLine}"; + + PrintToConsole(text, verbosity); + AppendToFile(text); + } + + public void LogFailedReadingFile(string filePath, Exception e) + { + PrintToConsole(NewLine, VerbosityMode.Verbose); + LogFailedProcessingFile(filePath); + LogException(e, isError: false); + using var record = new FailedReadingFileRecord + { + FilePath = filePath, + ExceptionMessage = e.Message, + StackTrace = e.StackTrace, + }; + } + + public void LogException( + Exception e, + bool isError, + bool printException = false, + [CallerMemberName] string callerMemberName = "", + [CallerLineNumber] int callerLineNumber = 0) + { + string tag = isError ? "[ERROR]" : "[INFO]"; + + var fullExceptionText = $"{tag} Exception encountered." + NewLine + + $"CallerMember: [{callerMemberName} : {callerLineNumber}]" + NewLine + + e.ToString() + NewLine; + + var shortExceptionText = $"{tag} {callerMemberName} logged {e.GetType().Name}: {e.Message}{NewLine}"; + + var consoleText = printException ? fullExceptionText : shortExceptionText; + + if (isError) + { + PrintToConsole(consoleText, VerbosityMode.Quiet); + } + else + { + PrintToConsole(consoleText, VerbosityMode.Verbose); + } + + AppendToFile(fullExceptionText); + } + + // TODO: All these vso specific logs should go away + public void LogBuildWarning(string message) + { + PrintToConsole($"##vso[task.LogIssue type=warning;]{message}{NewLine}", VerbosityMode.Quiet); + } + + public void LogBuildError(string message) + { + PrintToConsole($"##vso[task.LogIssue type=error;]{message}{NewLine}", VerbosityMode.Quiet); + } + + private void LogFailedProcessingFile(string filePath) + { + LogVerbose($"Could not read component details from file {filePath} {NewLine}"); + } + + private void AppendToFile(string text) + { + if (WriteToFile) + { + FileWritingService.AppendToFile(LogRelativePath, text); + } + } + + private void PrintToConsole(string text, VerbosityMode minVerbosity) + { + if (Verbosity >= minVerbosity) + { + ConsoleWriter.Write(text); + } + } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/MatchedFile.cs b/src/Microsoft.ComponentDetection.Common/MatchedFile.cs new file mode 100644 index 000000000..d3af4ba29 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/MatchedFile.cs @@ -0,0 +1,11 @@ +using System.IO; + +namespace Microsoft.ComponentDetection.Common +{ + public class MatchedFile + { + public FileInfo File { get; set; } + + public string Pattern { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Microsoft.ComponentDetection.Common.csproj b/src/Microsoft.ComponentDetection.Common/Microsoft.ComponentDetection.Common.csproj new file mode 100644 index 000000000..8df30202a --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Microsoft.ComponentDetection.Common.csproj @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + Resources.resx + True + True + + + Resources.Designer.cs + ResXFileCodeGenerator + + + + \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Common/PathUtilityService.cs b/src/Microsoft.ComponentDetection.Common/PathUtilityService.cs new file mode 100644 index 000000000..4a93e88ea --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/PathUtilityService.cs @@ -0,0 +1,289 @@ +using System; +using System.Collections.Concurrent; +using System.Composition; +using System.Diagnostics; +using System.IO; +using System.IO.Enumeration; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.Win32.SafeHandles; + +namespace Microsoft.ComponentDetection.Common +{ + // We may want to consider breaking this class into Win/Mac/Linux variants if it gets bigger + [Export(typeof(IPathUtilityService))] + [Export(typeof(PathUtilityService))] + [Shared] + public class PathUtilityService : IPathUtilityService + { + [Import] + public ILogger Logger { get; set; } + + public const uint CreationDispositionRead = 0x3; + + public const uint FileFlagBackupSemantics = 0x02000000; + + public const int InitalPathBufferSize = 512; + + public const string LongPathPrefix = "\\\\?\\"; + + private readonly ConcurrentDictionary resolvedPaths = new ConcurrentDictionary(); + + public bool IsRunningOnWindowsContainer + { + get + { + if (!isRunningOnWindowsContainer.HasValue) + { + lock (isRunningOnWindowsContainerLock) + { + if (!isRunningOnWindowsContainer.HasValue) + { + isRunningOnWindowsContainer = CheckIfRunningOnWindowsContainer(); + } + } + } + + return isRunningOnWindowsContainer.Value; + } + } + + private object isRunningOnWindowsContainerLock = new object(); + private bool? isRunningOnWindowsContainer = null; + + private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + private static readonly bool IsLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + + public string GetParentDirectory(string path) + { + return Path.GetDirectoryName(path); + } + + public bool IsFileBelowAnother(string aboveFilePath, string belowFilePath) + { + var aboveDirectoryPath = Path.GetDirectoryName(aboveFilePath); + var belowDirectoryPath = Path.GetDirectoryName(belowFilePath); + + // Return true if they are not the same path but the second has the first as its base + return (aboveDirectoryPath.Length != belowDirectoryPath.Length) && belowDirectoryPath.StartsWith(aboveDirectoryPath); + } + + public bool MatchesPattern(string searchPattern, string fileName) + { + if (searchPattern.StartsWith("*") && fileName.EndsWith(searchPattern.Substring(1), StringComparison.OrdinalIgnoreCase)) + { + return true; + } + else if (searchPattern.EndsWith("*") && fileName.StartsWith(searchPattern.Substring(0, searchPattern.Length - 1), StringComparison.OrdinalIgnoreCase)) + { + return true; + } + else if (searchPattern.Equals(fileName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + else + { + return false; + } + } + + public static bool MatchesPattern(string searchPattern, ref FileSystemEntry fse) + { + if (searchPattern.StartsWith("*") && fse.FileName.EndsWith(searchPattern.Substring(1), StringComparison.OrdinalIgnoreCase)) + { + return true; + } + else if (searchPattern.EndsWith("*") && fse.FileName.StartsWith(searchPattern.Substring(0, searchPattern.Length - 1), StringComparison.OrdinalIgnoreCase)) + { + return true; + } + else if (fse.FileName.Equals(searchPattern.AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + return true; + } + else + { + return false; + } + } + + public string ResolvePhysicalPath(string path) + { + if (IsWindows) + { + return ResolvePhysicalPathWindows(path); + } + else if (IsLinux) + { + return ResolvePhysicalPathLinux(path); + } + + return path; + } + + public string ResolvePhysicalPathWindows(string path) + { + if (!IsWindows) + { + throw new PlatformNotSupportedException("Attempted to call a function that makes windows-only SDK calls"); + } + + if (IsRunningOnWindowsContainer) + { + return path; + } + + if (resolvedPaths.TryGetValue(path, out string cachedPath)) + { + return cachedPath; + } + + DirectoryInfo symlink = new DirectoryInfo(path); + + using SafeFileHandle directoryHandle = CreateFile(symlink.FullName, 0, 2, IntPtr.Zero, CreationDispositionRead, FileFlagBackupSemantics, IntPtr.Zero); + + if (directoryHandle.IsInvalid) + { + return path; + } + + StringBuilder resultBuilder = new StringBuilder(InitalPathBufferSize); + int mResult = GetFinalPathNameByHandle(directoryHandle.DangerousGetHandle(), resultBuilder, resultBuilder.Capacity, 0); + + // If GetFinalPathNameByHandle needs a bigger buffer, it will tell us the size it needs (including the null terminator) in finalPathNameResultCode + if (mResult > InitalPathBufferSize) + { + resultBuilder = new StringBuilder(mResult); + mResult = GetFinalPathNameByHandle(directoryHandle.DangerousGetHandle(), resultBuilder, resultBuilder.Capacity, 0); + } + + if (mResult < 0) + { + return path; + } + + string result = resultBuilder.ToString(); + + result = result.StartsWith(LongPathPrefix) ? result.Substring(LongPathPrefix.Length) : result; + + resolvedPaths.TryAdd(path, result); + + return result; + } + + public string ResolvePhysicalPathLinux(string path) + { + if (!IsLinux) + { + throw new PlatformNotSupportedException("Attempted to call a function that makes linux-only library calls"); + } + + IntPtr pointer = IntPtr.Zero; + try + { + pointer = RealPathLinux(path, IntPtr.Zero); + + if (pointer != IntPtr.Zero) + { + var toReturn = Marshal.PtrToStringAnsi(pointer); + return toReturn; + } + else + { + return path; + } + } + catch (Exception ex) + { + Logger.LogException(ex, isError: false, printException: true); + return path; + } + finally + { + if (pointer != IntPtr.Zero) + { + FreeMemoryLinux(pointer); + } + } + } + + private bool CheckIfRunningOnWindowsContainer() + { + if (IsLinux) + { + return false; + } + + // This isn't the best way to do this in C#, but netstandard doesn't seem to support the service api calls + // that we need to do this without shelling out + Process process = new Process() + { + StartInfo = new ProcessStartInfo + { + FileName = "cmd.exe", + Arguments = "/c NET START", + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = false, + UseShellExecute = false, + }, + }; + + StringBuilder sb = new StringBuilder(); + process.Start(); + + while (!process.HasExited) + { + sb.Append(process.StandardOutput.ReadToEnd()); + } + + process.WaitForExit(); + sb.Append(process.StandardOutput.ReadToEnd()); + + if (sb.ToString().Contains("Container Execution Agent")) + { + Logger.LogWarning("Detected execution in a Windows container. Currently windows containers < 1809 do not support symlinks well, so disabling symlink resolution/dedupe behavior"); + return true; + } + else + { + return false; + } + } + + [DllImport("kernel32.dll", EntryPoint = "CreateFileW", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern SafeFileHandle CreateFile( + [In] string lpFileName, + [In] uint dwDesiredAccess, + [In] uint dwShareMode, + [In] IntPtr lpSecurityAttributes, + [In] uint dwCreationDisposition, + [In] uint dwFlagsAndAttributes, + [In] IntPtr hTemplateFile); + + [DllImport("kernel32.dll", EntryPoint = "GetFinalPathNameByHandleW", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern int GetFinalPathNameByHandle([In] IntPtr hFile, [Out] StringBuilder lpszFilePath, [In] int cchFilePath, [In] int dwFlags); + + /// + /// This call can be made on a linux system to get the absolute path of a file. It will resolve nested layers. + /// Note: You may pass IntPtr.Zero to the output parameter. You MUST then free the IntPtr that RealPathLinux returns + /// using FreeMemoryLinux otherwise things will get very leaky. + /// + /// + /// + /// + [DllImport("libc", EntryPoint = "realpath")] + public static extern IntPtr RealPathLinux([MarshalAs(UnmanagedType.LPStr)] string path, IntPtr output); + + /// + /// Use this function to free memory and prevent memory leaks. + /// However, beware.... Improper usage of this function will cause segfaults and other nasty double-free errors. + /// THIS WILL CRASH THE CLR IF YOU USE IT WRONG. + /// + /// + [DllImport("libc", EntryPoint = "free")] + public static extern void FreeMemoryLinux([In] IntPtr toFree); + } +} diff --git a/src/Microsoft.ComponentDetection.Common/PatternMatchingUtility.cs b/src/Microsoft.ComponentDetection.Common/PatternMatchingUtility.cs new file mode 100644 index 000000000..e299f3e00 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/PatternMatchingUtility.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace Microsoft.ComponentDetection.Common +{ + public static class PatternMatchingUtility + { + public delegate bool FilePatternMatcher(ReadOnlySpan span); + + public static FilePatternMatcher GetFilePatternMatcher(IEnumerable patterns) + { + var ordinalComparison = Expression.Constant(System.StringComparison.Ordinal, typeof(System.StringComparison)); + var asSpan = typeof(System.MemoryExtensions).GetMethod("AsSpan", BindingFlags.Public | BindingFlags.Static, null, CallingConventions.Standard, new[] { typeof(string) }, new ParameterModifier[0]); + var equals = typeof(System.MemoryExtensions).GetMethod("Equals", BindingFlags.Public | BindingFlags.Static, null, CallingConventions.Standard, new[] { typeof(ReadOnlySpan), typeof(ReadOnlySpan), typeof(System.StringComparison) }, new ParameterModifier[0]); + var startsWith = typeof(System.MemoryExtensions).GetMethod("StartsWith", BindingFlags.Public | BindingFlags.Static, null, CallingConventions.Standard, new[] { typeof(ReadOnlySpan), typeof(ReadOnlySpan), typeof(System.StringComparison) }, new ParameterModifier[0]); + var endsWith = typeof(System.MemoryExtensions).GetMethod("EndsWith", BindingFlags.Public | BindingFlags.Static, null, CallingConventions.Standard, new[] { typeof(ReadOnlySpan), typeof(ReadOnlySpan), typeof(System.StringComparison) }, new ParameterModifier[0]); + + var predicates = new List(); + var left = Expression.Parameter(typeof(ReadOnlySpan), "fileName"); + + foreach (var pattern in patterns) + { + if (pattern.StartsWith("*")) + { + var match = Expression.Constant(pattern.Substring(1), typeof(string)); + var right = Expression.Call(null, asSpan, match); + var combine = Expression.Call(null, endsWith, left, right, ordinalComparison); + predicates.Add(combine); + } + else if (pattern.EndsWith("*")) + { + var match = Expression.Constant(pattern.Substring(0, pattern.Length - 1), typeof(string)); + var right = Expression.Call(null, asSpan, match); + var combine = Expression.Call(null, startsWith, left, right, ordinalComparison); + predicates.Add(combine); + } + else + { + var match = Expression.Constant(pattern, typeof(string)); + var right = Expression.Call(null, asSpan, match); + var combine = Expression.Call(null, equals, left, right, ordinalComparison); + predicates.Add(combine); + } + } + + var aggregateExpression = predicates.Aggregate(Expression.OrElse); + + var func = Expression.Lambda(aggregateExpression, left).Compile(); + + return func; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Common/Resources.Designer.cs b/src/Microsoft.ComponentDetection.Common/Resources.Designer.cs new file mode 100644 index 000000000..08908f0d6 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Resources.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.ComponentDetection.Common { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.ComponentDetection.Common.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The component object does not have a componentId specified. + /// + internal static string MissingComponentId { + get { + return ResourceManager.GetString("MissingComponentId", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Node with id {0} has not be inserted in the dependency graph. + /// + internal static string MissingNodeInDependencyGraph { + get { + return ResourceManager.GetString("MissingNodeInDependencyGraph", resourceCulture); + } + } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Resources.resx b/src/Microsoft.ComponentDetection.Common/Resources.resx new file mode 100644 index 000000000..d0a1c20e8 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Resources.resx @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The component object does not have a componentId specified + + + Node with id {0} has not be inserted in the dependency graph + {0} - Component id + + \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Common/SafeFileEnumerable.cs b/src/Microsoft.ComponentDetection.Common/SafeFileEnumerable.cs new file mode 100644 index 000000000..3cee65b90 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/SafeFileEnumerable.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.IO.Enumeration; +using Microsoft.ComponentDetection.Contracts; + +namespace Microsoft.ComponentDetection.Common +{ + public class SafeFileEnumerable : IEnumerable + { + private HashSet enumeratedDirectories; + + private readonly IEnumerable searchPatterns; + private readonly ExcludeDirectoryPredicate directoryExclusionPredicate; + private readonly DirectoryInfo directory; + private readonly ILogger logger; + private readonly IPathUtilityService pathUtilityService; + private readonly bool recursivelyScanDirectories; + private readonly Func fileMatchingPredicate; + + private readonly EnumerationOptions enumerationOptions; + + public SafeFileEnumerable(DirectoryInfo directory, IEnumerable searchPatterns, ILogger logger, IPathUtilityService pathUtilityService, ExcludeDirectoryPredicate directoryExclusionPredicate, bool recursivelyScanDirectories = true, HashSet previouslyEnumeratedDirectories = null) + { + this.directory = directory; + this.logger = logger; + this.searchPatterns = searchPatterns; + this.directoryExclusionPredicate = directoryExclusionPredicate; + this.recursivelyScanDirectories = recursivelyScanDirectories; + this.pathUtilityService = pathUtilityService; + enumeratedDirectories = previouslyEnumeratedDirectories; + + enumerationOptions = new EnumerationOptions() + { + IgnoreInaccessible = true, + RecurseSubdirectories = this.recursivelyScanDirectories, + ReturnSpecialDirectories = false, + MatchType = MatchType.Simple, + }; + } + + public SafeFileEnumerable(DirectoryInfo directory, Func fileMatchingPredicate, ILogger logger, IPathUtilityService pathUtilityService, ExcludeDirectoryPredicate directoryExclusionPredicate, bool recursivelyScanDirectories = true, HashSet previouslyEnumeratedDirectories = null) + : this(directory, new List { "*" }, logger, pathUtilityService, directoryExclusionPredicate, recursivelyScanDirectories, previouslyEnumeratedDirectories) + { + this.fileMatchingPredicate = fileMatchingPredicate; + } + + public IEnumerator GetEnumerator() + { + var previouslyEnumeratedDirectories = enumeratedDirectories ?? new HashSet(); + + var fse = new FileSystemEnumerable(directory.FullName, (ref FileSystemEntry entry) => + { + if (!(entry.ToFileSystemInfo() is FileInfo fi)) + { + throw new InvalidOperationException("Encountered directory when expecting a file"); + } + + var foundPattern = entry.FileName.ToString(); + foreach (var searchPattern in searchPatterns) + { + if (PathUtilityService.MatchesPattern(searchPattern, ref entry)) + { + foundPattern = searchPattern; + } + } + + return new MatchedFile() { File = fi, Pattern = foundPattern }; + }, enumerationOptions) + { + ShouldIncludePredicate = (ref FileSystemEntry entry) => + { + if (entry.IsDirectory) + { + return false; + } + + foreach (var searchPattern in searchPatterns) + { + if (PathUtilityService.MatchesPattern(searchPattern, ref entry)) + { + return true; + } + } + + return false; + }, + ShouldRecursePredicate = (ref FileSystemEntry entry) => + { + if (!recursivelyScanDirectories) + { + return false; + } + + var targetPath = entry.ToFullPath(); + + var seenPreviously = false; + + if (entry.Attributes.HasFlag(FileAttributes.ReparsePoint)) + { + var realPath = pathUtilityService.ResolvePhysicalPath(targetPath); + + seenPreviously = previouslyEnumeratedDirectories.Contains(realPath); + previouslyEnumeratedDirectories.Add(realPath); + + if (realPath.StartsWith(targetPath)) + { + return false; + } + } + else if (previouslyEnumeratedDirectories.Contains(targetPath)) + { + seenPreviously = true; + } + + previouslyEnumeratedDirectories.Add(targetPath); + + if (seenPreviously) + { + logger.LogVerbose($"Encountered real path {targetPath} before. Short-Circuiting directory traversal"); + return false; + } + + // This is actually a *directory* name (not FileName) and the directory containing that directory. + if (entry.IsDirectory && directoryExclusionPredicate != null && directoryExclusionPredicate(entry.FileName, entry.Directory)) + { + return false; + } + + return true; + }, + }; + + foreach (var file in fse) + { + if (fileMatchingPredicate == null || fileMatchingPredicate(file.File)) + { + yield return file; + } + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Common/SafeFileEnumerableFactory.cs b/src/Microsoft.ComponentDetection.Common/SafeFileEnumerableFactory.cs new file mode 100644 index 000000000..541793b08 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/SafeFileEnumerableFactory.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Composition; +using System.IO; +using Microsoft.ComponentDetection.Contracts; + +namespace Microsoft.ComponentDetection.Common +{ + [Export(typeof(ISafeFileEnumerableFactory))] + [Shared] + public class SafeFileEnumerableFactory : ISafeFileEnumerableFactory + { + [Import] + public ILogger Logger { get; set; } + + [Import] + public IPathUtilityService PathUtilityService { get; set; } + + public IEnumerable CreateSafeFileEnumerable(DirectoryInfo directory, IEnumerable searchPatterns, ExcludeDirectoryPredicate directoryExclusionPredicate) + { + return new SafeFileEnumerable(directory, searchPatterns, Logger, PathUtilityService, directoryExclusionPredicate); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Common/ScanType.cs b/src/Microsoft.ComponentDetection.Common/ScanType.cs new file mode 100644 index 000000000..2fa4cf84f --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/ScanType.cs @@ -0,0 +1,9 @@ +namespace Microsoft.ComponentDetection.Common +{ + public enum ScanType + { + Invalid = 0, + Register = 1, + LogOnly = 2, + } +} diff --git a/src/Microsoft.ComponentDetection.Common/TabularStringFormat.cs b/src/Microsoft.ComponentDetection.Common/TabularStringFormat.cs new file mode 100644 index 000000000..e64270f17 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/TabularStringFormat.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.ComponentDetection.Common +{ + public class TabularStringFormat + { + private IList columns; + private int totalWidth; + private char horizontalLineChar; + private char verticalLineChar; + private string tableTitle; + + public const char DefaultVerticalLineChar = '|'; + public const char DefaultHorizontalLineChar = '_'; + + public TabularStringFormat(IList columns, char horizontalLineChar = DefaultHorizontalLineChar, char verticalLineChar = DefaultVerticalLineChar, string tableTitle = null) + { + this.columns = columns; + totalWidth = columns.Count + 1 + columns.Sum(x => x.Width); + this.horizontalLineChar = horizontalLineChar; + this.verticalLineChar = verticalLineChar; + this.tableTitle = tableTitle; + } + + public string GenerateString(IEnumerable> rows) + { + StringBuilder sb = new StringBuilder(); + if (!string.IsNullOrWhiteSpace(tableTitle)) + { + PrintTitleSection(sb); + } + else + { + WriteFlatLine(sb, false); + } + + sb.Append(verticalLineChar); + foreach (var column in columns) + { + sb.Append(column.Header.PadRight(column.Width)); + sb.Append(verticalLineChar); + } + + WriteFlatLine(sb); + foreach (var row in rows) + { + sb.Append(verticalLineChar); + if (row.Count() != columns.Count) + { + throw new InvalidOperationException("All rows must have length equal to the number of columns present."); + } + + for (var i = 0; i < columns.Count; i++) + { + var dataString = columns[i].Format != null ? string.Format(columns[i].Format, row[i]) : row[i].ToString(); + sb.Append(dataString.PadRight(columns[i].Width)); + sb.Append(verticalLineChar); + } + + WriteFlatLine(sb); + } + + return sb.ToString(); + } + + private void PrintTitleSection(StringBuilder sb) + { + WriteFlatLine(sb, false); + var tableWidth = columns.Sum(column => column.Width); + sb.Append(verticalLineChar); + sb.Append(tableTitle.PadRight(tableWidth + columns.Count() - 1)); + sb.Append(verticalLineChar); + + sb.AppendLine(); + sb.Append(verticalLineChar); + for (var i = 0; i < columns.Count - 1; i++) + { + sb.Append(string.Empty.PadRight(columns[i].Width, horizontalLineChar)); + sb.Append(horizontalLineChar); + } + + sb.Append(string.Empty.PadRight(columns[columns.Count - 1].Width, horizontalLineChar)); + sb.Append(verticalLineChar); + sb.AppendLine(); + } + + private void WriteFlatLine(StringBuilder sb, bool withPipes = true) + { + var splitCharacter = withPipes ? verticalLineChar : horizontalLineChar; + sb.AppendLine(); + sb.Append(splitCharacter); + for (var i = 0; i < columns.Count; i++) + { + sb.Append(string.Empty.PadRight(columns[i].Width, horizontalLineChar)); + sb.Append(splitCharacter); + } + + sb.AppendLine(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Attributes/MetricAttribute.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Attributes/MetricAttribute.cs new file mode 100644 index 000000000..c0c171545 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Attributes/MetricAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace Microsoft.ComponentDetection.Common.Telemetry.Attributes +{ + /// + /// Denotes that a telemetry property should be treated as a Metric (numeric) value + /// + /// It is up to the implementing Telemetry Service to interpret this value. + /// + public class MetricAttribute : Attribute + { + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Attributes/TelemetryServiceAttribute.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Attributes/TelemetryServiceAttribute.cs new file mode 100644 index 000000000..3b786a00f --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Attributes/TelemetryServiceAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace Microsoft.ComponentDetection.Common.Telemetry.Attributes +{ + public class TelemetryServiceAttribute : Attribute + { + public string ServiceType { get; } + + public TelemetryServiceAttribute(string serviceType) + { + ServiceType = serviceType; + } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/CommandLineTelemetryService.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/CommandLineTelemetryService.cs new file mode 100644 index 000000000..3eec11a5e --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/CommandLineTelemetryService.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Concurrent; +using System.Composition; +using Microsoft.ComponentDetection.Common.Telemetry.Attributes; +using Microsoft.ComponentDetection.Common.Telemetry.Records; +using Microsoft.ComponentDetection.Contracts; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.ComponentDetection.Common.Telemetry +{ + [Export(typeof(ITelemetryService))] + [TelemetryService(nameof(CommandLineTelemetryService))] + internal class CommandLineTelemetryService : ITelemetryService + { + [Import] + public ILogger Logger { get; set; } + + [Import] + public IFileWritingService FileWritingService { get; set; } + + public const string TelemetryRelativePath = "ScanTelemetry_{timestamp}.json"; + + private TelemetryMode telemetryMode = TelemetryMode.Production; + + private static ConcurrentQueue records = new ConcurrentQueue(); + + public void Flush() + { + FileWritingService.WriteFile(TelemetryRelativePath, JsonConvert.SerializeObject(records)); + } + + public void PostRecord(IDetectionTelemetryRecord record) + { + if (telemetryMode != TelemetryMode.Disabled) + { + var jsonRecord = JObject.FromObject(record); + jsonRecord.Add("Timestamp", DateTime.UtcNow); + jsonRecord.Add("CorrelationId", TelemetryConstants.CorrelationId); + + records.Enqueue(jsonRecord); + + if (telemetryMode == TelemetryMode.Debug) + { + Logger.LogInfo(jsonRecord.ToString()); + } + } + } + + public void SetMode(TelemetryMode mode) + { + telemetryMode = mode; + } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/ITelemetryService.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/ITelemetryService.cs new file mode 100644 index 000000000..29b6a26fb --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/ITelemetryService.cs @@ -0,0 +1,23 @@ +using Microsoft.ComponentDetection.Common.Telemetry.Records; + +namespace Microsoft.ComponentDetection.Common.Telemetry +{ + public interface ITelemetryService + { + /// + /// Post a record to the telemetry service. + /// + /// The telemetry record to post. + void PostRecord(IDetectionTelemetryRecord record); + + /// + /// Flush all telemetry events from the queue (usually called on shutdown to clear the queue). + /// + void Flush(); + + /// + /// Sets the telemetry mode for the service. + /// + void SetMode(TelemetryMode mode); + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/BaseDetectionTelemetryRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/BaseDetectionTelemetryRecord.cs new file mode 100644 index 000000000..b303df421 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/BaseDetectionTelemetryRecord.cs @@ -0,0 +1,51 @@ +using System; +using System.Diagnostics; +using Microsoft.ComponentDetection.Common.Telemetry.Attributes; + +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public abstract class BaseDetectionTelemetryRecord : IDetectionTelemetryRecord + { + public abstract string RecordName { get; } + + [Metric] + public TimeSpan? ExecutionTime { get; protected set; } + + private Stopwatch stopwatch = new Stopwatch(); + + protected BaseDetectionTelemetryRecord() + { + stopwatch.Start(); + } + + public void StopExecutionTimer() + { + if (stopwatch.IsRunning) + { + stopwatch.Stop(); + ExecutionTime = stopwatch.Elapsed; + } + } + + private bool disposedValue = false; + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + StopExecutionTimer(); + TelemetryRelay.Instance.PostTelemetryRecord(this); + } + + disposedValue = true; + } + } + + public void Dispose() + { + Dispose(true); + } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/BcdeExecutionTelemetryRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/BcdeExecutionTelemetryRecord.cs new file mode 100644 index 000000000..3cc8f6e90 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/BcdeExecutionTelemetryRecord.cs @@ -0,0 +1,48 @@ +using System; + +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public class BcdeExecutionTelemetryRecord : BaseDetectionTelemetryRecord + { + public override string RecordName => "BcdeExecution"; + + public string Command { get; set; } + + public int? ExitCode { get; set; } + + public int? HiddenExitCode { get; set; } + + public string UnhandledException { get; set; } + + public string Arguments { get; set; } + + public string ErrorMessage { get; set; } + + public string AgentOSMeaningfulDetails { get; set; } + + public string AgentOSDescription { get; set; } + + public static TReturn Track(Func functionToTrack, bool terminalRecord = false) + { + using var record = new BcdeExecutionTelemetryRecord(); + + try + { + return functionToTrack(record); + } + catch (Exception ex) + { + record.UnhandledException = ex.ToString(); + throw; + } + finally + { + record.Dispose(); + if (terminalRecord && !record.Command.Equals("help", StringComparison.InvariantCultureIgnoreCase)) + { + TelemetryRelay.Instance.Shutdown(); + } + } + } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/CommandLineInvocationTelemetryRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/CommandLineInvocationTelemetryRecord.cs new file mode 100644 index 000000000..45650e3cf --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/CommandLineInvocationTelemetryRecord.cs @@ -0,0 +1,41 @@ +using System; +using Microsoft.ComponentDetection.Contracts; + +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public class CommandLineInvocationTelemetryRecord : BaseDetectionTelemetryRecord + { + public override string RecordName => "CommandLineInvocation"; + + public string PathThatWasRan { get; set; } + + public string Parameters { get; set; } + + public int? ExitCode { get; set; } + + public string StandardError { get; set; } + + public string UnhandledException { get; set; } + + internal void Track(CommandLineExecutionResult result, string path, string parameters) + { + ExitCode = result.ExitCode; + StandardError = result.StdErr; + TrackCommon(path, parameters); + } + + internal void Track(Exception ex, string path, string parameters) + { + ExitCode = -1; + UnhandledException = ex.ToString(); + TrackCommon(path, parameters); + } + + private void TrackCommon(string path, string parameters) + { + PathThatWasRan = path; + Parameters = parameters; + StopExecutionTimer(); + } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/DetectorExecutionTelemetryRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/DetectorExecutionTelemetryRecord.cs new file mode 100644 index 000000000..f1792dcdb --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/DetectorExecutionTelemetryRecord.cs @@ -0,0 +1,21 @@ +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public class DetectorExecutionTelemetryRecord : BaseDetectionTelemetryRecord + { + public override string RecordName => "DetectorExecution"; + + public string DetectorId { get; set; } + + public int? DetectedComponentCount { get; set; } + + public int? ExplicitlyReferencedComponentCount { get; set; } + + public int? ReturnCode { get; set; } + + public bool IsExperimental { get; set; } + + public string ExperimentalInformation { get; set; } + + public string AdditionalTelemetryDetails { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/DockerServiceImageExistsLocallyTelemetryRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/DockerServiceImageExistsLocallyTelemetryRecord.cs new file mode 100644 index 000000000..76ed90104 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/DockerServiceImageExistsLocallyTelemetryRecord.cs @@ -0,0 +1,13 @@ +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public class DockerServiceImageExistsLocallyTelemetryRecord : BaseDetectionTelemetryRecord + { + public override string RecordName => "DockerServiceImageExistsLocally"; + + public string Image { get; set; } + + public string ImageInspectResponse { get; set; } + + public string ExceptionMessage { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/DockerServiceInspectImageTelemetryRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/DockerServiceInspectImageTelemetryRecord.cs new file mode 100644 index 000000000..ee5b0ce79 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/DockerServiceInspectImageTelemetryRecord.cs @@ -0,0 +1,17 @@ +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public class DockerServiceInspectImageTelemetryRecord : BaseDetectionTelemetryRecord + { + public override string RecordName => "DockerServiceInspectImage"; + + public string Image { get; set; } + + public string BaseImageDigest { get; set; } + + public string BaseImageRef { get; set; } + + public string ImageInspectResponse { get; set; } + + public string ExceptionMessage { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/DockerServiceSystemInfoTelemetryRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/DockerServiceSystemInfoTelemetryRecord.cs new file mode 100644 index 000000000..4e8e163e7 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/DockerServiceSystemInfoTelemetryRecord.cs @@ -0,0 +1,11 @@ +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public class DockerServiceSystemInfoTelemetryRecord : BaseDetectionTelemetryRecord + { + public override string RecordName => "DockerServiceSystemInfo"; + + public string SystemInfo { get; set; } + + public string ExceptionMessage { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/DockerServiceTelemetryRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/DockerServiceTelemetryRecord.cs new file mode 100644 index 000000000..70bd5d63d --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/DockerServiceTelemetryRecord.cs @@ -0,0 +1,17 @@ +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public class DockerServiceTelemetryRecord : BaseDetectionTelemetryRecord + { + public override string RecordName => "DockerService"; + + public string Image { get; set; } + + public string Command { get; set; } + + public string Container { get; set; } + + public string Stdout { get; set; } + + public string Stderr { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/DockerServiceTryPullImageTelemetryRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/DockerServiceTryPullImageTelemetryRecord.cs new file mode 100644 index 000000000..c73942482 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/DockerServiceTryPullImageTelemetryRecord.cs @@ -0,0 +1,13 @@ +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public class DockerServiceTryPullImageTelemetryRecord : BaseDetectionTelemetryRecord + { + public override string RecordName => "DockerServiceTryPullImage"; + + public string Image { get; set; } + + public string CreateImageProgress { get; set; } + + public string ExceptionMessage { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/FailedReadingFileRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/FailedReadingFileRecord.cs new file mode 100644 index 000000000..0606e463a --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/FailedReadingFileRecord.cs @@ -0,0 +1,13 @@ +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public class FailedReadingFileRecord : BaseDetectionTelemetryRecord + { + public override string RecordName => "FailedReadingFile"; + + public string FilePath { get; set; } + + public string ExceptionMessage { get; set; } + + public string StackTrace { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/GoGraphTelemetryRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/GoGraphTelemetryRecord.cs new file mode 100644 index 000000000..96df47351 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/GoGraphTelemetryRecord.cs @@ -0,0 +1,13 @@ +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public class GoGraphTelemetryRecord : BaseDetectionTelemetryRecord + { + public override string RecordName => "GoGraph"; + + public string ProjectRoot { get; set; } + + public bool IsGoAvailable { get; set; } + + public bool WasGraphSuccessful { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/IDetectionTelemetryRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/IDetectionTelemetryRecord.cs new file mode 100644 index 000000000..099d2ec80 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/IDetectionTelemetryRecord.cs @@ -0,0 +1,12 @@ +using System; + +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public interface IDetectionTelemetryRecord : IDisposable + { + /// + /// Gets the name of the record to be logged. + /// + string RecordName { get; } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxContainerDetectorImageDetectionFailed.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxContainerDetectorImageDetectionFailed.cs new file mode 100644 index 000000000..02498dab3 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxContainerDetectorImageDetectionFailed.cs @@ -0,0 +1,15 @@ +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public class LinuxContainerDetectorImageDetectionFailed : BaseDetectionTelemetryRecord + { + public override string RecordName => "LinuxContainerDetectorImageDetectionFailed"; + + public string ImageId { get; set; } + + public string Message { get; set; } + + public string ExceptionType { get; set; } + + public string StackTrace { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxContainerDetectorLayerAwareness.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxContainerDetectorLayerAwareness.cs new file mode 100644 index 000000000..bef3b31f3 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxContainerDetectorLayerAwareness.cs @@ -0,0 +1,17 @@ +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public class LinuxContainerDetectorLayerAwareness : BaseDetectionTelemetryRecord + { + public override string RecordName => "LinuxContainerDetectorLayerAwareness"; + + public string BaseImageRef { get; set; } + + public string BaseImageDigest { get; set; } + + public int? BaseImageLayerCount { get; set; } + + public int? LayerCount { get; set; } + + public string BaseImageLayerMessage { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxContainerDetectorMissingRepoNameAndTagRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxContainerDetectorMissingRepoNameAndTagRecord.cs new file mode 100644 index 000000000..d64c18f38 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxContainerDetectorMissingRepoNameAndTagRecord.cs @@ -0,0 +1,7 @@ +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public class LinuxContainerDetectorMissingRepoNameAndTagRecord : BaseDetectionTelemetryRecord + { + public override string RecordName => "MissingRepoNameAndTag"; + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxContainerDetectorMissingVersion.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxContainerDetectorMissingVersion.cs new file mode 100644 index 000000000..0f811c343 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxContainerDetectorMissingVersion.cs @@ -0,0 +1,13 @@ +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public class LinuxContainerDetectorMissingVersion : BaseDetectionTelemetryRecord + { + public override string RecordName { get; } = "MissingVersion"; + + public string Distribution { get; set; } + + public string Release { get; set; } + + public string[] PackageNames { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxContainerDetectorTimeout.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxContainerDetectorTimeout.cs new file mode 100644 index 000000000..34baf5456 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxContainerDetectorTimeout.cs @@ -0,0 +1,7 @@ +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public class LinuxContainerDetectorTimeout : BaseDetectionTelemetryRecord + { + public override string RecordName => "LinuxContainerDetectorTimeout"; + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxContainerDetectorUnsupportedOs.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxContainerDetectorUnsupportedOs.cs new file mode 100644 index 000000000..3af20f705 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxContainerDetectorUnsupportedOs.cs @@ -0,0 +1,9 @@ +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public class LinuxContainerDetectorUnsupportedOs : BaseDetectionTelemetryRecord + { + public override string RecordName => "LinuxContainerDetectorUnsupportedOs"; + + public string Os { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxScannerSyftTelemetryRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxScannerSyftTelemetryRecord.cs new file mode 100644 index 000000000..706126097 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxScannerSyftTelemetryRecord.cs @@ -0,0 +1,11 @@ +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public class LinuxScannerSyftTelemetryRecord : BaseDetectionTelemetryRecord + { + public override string RecordName => "LinuxScannerSyftTelemetry"; + + public string LinuxComponents { get; set; } + + public string Exception { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxScannerTelemetryRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxScannerTelemetryRecord.cs new file mode 100644 index 000000000..fef3a5a10 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxScannerTelemetryRecord.cs @@ -0,0 +1,19 @@ +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public class LinuxScannerTelemetryRecord : BaseDetectionTelemetryRecord + { + public override string RecordName => "LinuxScannerTelemetry"; + + public string ImageToScan { get; set; } + + public string ScanStdOut { get; set; } + + public string ScanStdErr { get; set; } + + public string FailedDeserializingScannerOutput { get; set; } + + public bool SemaphoreFailure { get; set; } + + public string ScannerVersion { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LoadComponentDetectorsTelemetryRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LoadComponentDetectorsTelemetryRecord.cs new file mode 100644 index 000000000..ad8018bed --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/LoadComponentDetectorsTelemetryRecord.cs @@ -0,0 +1,9 @@ +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public class LoadComponentDetectorsTelemetryRecord : BaseDetectionTelemetryRecord + { + public override string RecordName => "LoadComponentDetectors"; + + public string DetectorIds { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/NuGetProjectAssetsTelemetryRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/NuGetProjectAssetsTelemetryRecord.cs new file mode 100644 index 000000000..4f691ca43 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/NuGetProjectAssetsTelemetryRecord.cs @@ -0,0 +1,35 @@ +using System; + +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public class NuGetProjectAssetsTelemetryRecord : IDetectionTelemetryRecord, IDisposable + { + public string RecordName => "NuGetProjectAssets"; + + public string FoundTargets { get; set; } + + public string UnhandledException { get; set; } + + public string Frameworks { get; set; } + + private bool disposedValue = false; + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + TelemetryRelay.Instance.PostTelemetryRecord(this); + } + + disposedValue = true; + } + } + + public void Dispose() + { + Dispose(true); + } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/PypiFailureTelemetryRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/PypiFailureTelemetryRecord.cs new file mode 100644 index 000000000..cbb76ca4f --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/PypiFailureTelemetryRecord.cs @@ -0,0 +1,24 @@ +using System.Net; + +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public class PypiFailureTelemetryRecord : BaseDetectionTelemetryRecord + { + public override string RecordName => "PypiFailure"; + + /// + /// Gets or sets the package Name (ex: pyyaml). + /// + public string Name { get; set; } + + /// + /// Gets or sets the set of dependency specifications that constrain the overall dependency request (ex: ==1.0, >=2.0). + /// + public string[] DependencySpecifiers { get; set; } + + /// + /// Gets or sets the status code of the last failed call. + /// + public HttpStatusCode StatusCode { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/PypiMaxRetriesReachedTelemetryRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/PypiMaxRetriesReachedTelemetryRecord.cs new file mode 100644 index 000000000..ec8641aae --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/PypiMaxRetriesReachedTelemetryRecord.cs @@ -0,0 +1,17 @@ +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public class PypiMaxRetriesReachedTelemetryRecord : BaseDetectionTelemetryRecord + { + public override string RecordName => "PypiMaxRetriesReached"; + + /// + /// Gets or sets the package Name (ex: pyyaml). + /// + public string Name { get; set; } + + /// + /// Gets or sets the set of dependency specifications that constrain the overall dependency request (ex: ==1.0, >=2.0). + /// + public string[] DependencySpecifiers { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/PypiRetryTelemetryRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/PypiRetryTelemetryRecord.cs new file mode 100644 index 000000000..14b3cdde4 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/PypiRetryTelemetryRecord.cs @@ -0,0 +1,24 @@ +using System.Net; + +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public class PypiRetryTelemetryRecord : BaseDetectionTelemetryRecord + { + public override string RecordName => "PypiRetry"; + + /// + /// Gets or sets the package Name (ex: pyyaml). + /// + public string Name { get; set; } + + /// + /// Gets or sets the set of dependency specifications that constrain the overall dependency request (ex: ==1.0, >=2.0). + /// + public string[] DependencySpecifiers { get; set; } + + /// + /// Gets or sets the status code of the last failed call that caused the retry. + /// + public HttpStatusCode StatusCode { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/RustCrateV2DetectorTelemetryRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/RustCrateV2DetectorTelemetryRecord.cs new file mode 100644 index 000000000..47b48584f --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/RustCrateV2DetectorTelemetryRecord.cs @@ -0,0 +1,11 @@ +namespace Microsoft.ComponentDetection.Common.Telemetry.Records +{ + public class RustCrateV2DetectorTelemetryRecord : BaseDetectionTelemetryRecord + { + public override string RecordName => "RustCrateV2MalformedDependencies"; + + public string PackageInfo { get; set; } + + public string Dependencies { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/TelemetryConstants.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/TelemetryConstants.cs new file mode 100644 index 000000000..dc129f724 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/TelemetryConstants.cs @@ -0,0 +1,30 @@ +using System; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.ComponentDetection.Orchestrator")] + +namespace Microsoft.ComponentDetection.Common.Telemetry +{ + public static class TelemetryConstants + { + private static Guid correlationId; + + public static Guid CorrelationId + { + get + { + if (correlationId == Guid.Empty) + { + correlationId = Guid.NewGuid(); + } + + return correlationId; + } + + set // This is temporarily public, but once a process boundary exists it will be internal and initialized by Orchestrator in BCDE + { + correlationId = value; + } + } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/TelemetryMode.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/TelemetryMode.cs new file mode 100644 index 000000000..ada2b07b0 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/TelemetryMode.cs @@ -0,0 +1,11 @@ +namespace Microsoft.ComponentDetection.Common.Telemetry +{ + public enum TelemetryMode + { + Disabled = 0, + + Debug = 1, + + Production = 2, + } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/TelemetryRelay.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/TelemetryRelay.cs new file mode 100644 index 000000000..3e4176420 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/TelemetryRelay.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Composition; +using System.Linq; +using System.Threading; +using Microsoft.ComponentDetection.Common.Telemetry.Records; + +namespace Microsoft.ComponentDetection.Common.Telemetry +{ + /// + /// Singleton that is responsible for relaying telemetry records to Telemetry Services. + /// + public sealed class TelemetryRelay + { + [ImportMany] + public static IEnumerable TelemetryServices { get; set; } + + private static readonly TelemetryRelay InternalInstance = new TelemetryRelay(); + + /// + /// Gets a value indicating whether or not the telemetry relay has been shutdown. + /// + public static bool Active { get; private set; } = true; + + private TelemetryRelay() + { + // For things not populating the telemetry services collection, let's not throw. + TelemetryServices = Enumerable.Empty(); + } + + /// + /// Gets the singleton. + /// + public static TelemetryRelay Instance + { + get + { + return InternalInstance; + } + } + + /// + /// Post a given telemetry record to all telemetry services. + /// + /// + public void PostTelemetryRecord(IDetectionTelemetryRecord record) + { + foreach (var service in TelemetryServices) + { + try + { + service.PostRecord(record); + } + catch + { + // Telemetry should never crash the application + } + } + } + + /// + /// Disables the sending of telemetry and flushes any messages out of the queue for each service. + /// + public void Shutdown() + { + Active = false; + + foreach (var service in TelemetryServices) + { + try + { + // Set a timeout for services that flush synchronously. + AsyncExecution.ExecuteVoidWithTimeoutAsync( + () => service.Flush(), + TimeSpan.FromSeconds(20), + CancellationToken.None).Wait(); + } + catch + { + Console.WriteLine("Logging output failed"); + } + } + } + + public void SetTelemetryMode(TelemetryMode mode) + { + foreach (var telemetryService in TelemetryServices ?? Enumerable.Empty()) + { + telemetryService.SetMode(mode); + } + } + } +} diff --git a/src/Microsoft.ComponentDetection.Common/VerbosityMode.cs b/src/Microsoft.ComponentDetection.Common/VerbosityMode.cs new file mode 100644 index 000000000..feb5dfc9c --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/VerbosityMode.cs @@ -0,0 +1,9 @@ +namespace Microsoft.ComponentDetection.Common +{ + public enum VerbosityMode + { + Quiet = 0, + Normal = 1, + Verbose = 2, + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Common/WarnOnAlertSeverity.cs b/src/Microsoft.ComponentDetection.Common/WarnOnAlertSeverity.cs new file mode 100644 index 000000000..e2eb23ad9 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/WarnOnAlertSeverity.cs @@ -0,0 +1,11 @@ +namespace Microsoft.ComponentDetection.Common +{ + public enum WarnOnAlertSeverity + { + Never = 0, + Critical = 1, + High = 2, + Medium = 3, + Low = 4, + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Contracts/BcdeModels/ContainerDetails.cs b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/ContainerDetails.cs new file mode 100644 index 000000000..25866d5e0 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/ContainerDetails.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.ComponentDetection.Contracts.BcdeModels +{ + // Summary: + // Details for a docker container + [JsonObject(MemberSerialization.OptOut, NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class ContainerDetails + { + // Summary: + // ImageId for the docker container. + public string ImageId { get; set; } + + // Summary: + // Unique id for the container. + public int Id { get; set; } + + // Summary: + // Digests for the container + public IEnumerable Digests { get; set; } + + // Summary: + // The Repository:Tag for the base image of the docker container + // ex: alpine:latest || alpine:v3.1 || mcr.microsoft.com/dotnet/sdk:5.0 + public string BaseImageRef { get; set; } + + // Summary: + // The digest of the exact image used as the base image + // This is to avoid errors if there are ref updates between build time and scan time + public string BaseImageDigest { get; set; } + + // Summary: + // The time the container was created + public DateTime CreatedAt { get; set; } + + // Summary: + // Tags for the container + public IEnumerable Tags { get; set; } + + public IEnumerable Layers { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/BcdeModels/DefaultGraphScanResult.cs b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/DefaultGraphScanResult.cs new file mode 100644 index 000000000..ab2883d4b --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/DefaultGraphScanResult.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.ComponentDetection.Contracts.BcdeModels +{ + [JsonObject(MemberSerialization.OptOut, NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class DefaultGraphScanResult : ScanResult + { + public DependencyGraphCollection DependencyGraphs { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/BcdeModels/DependencyGraph.cs b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/DependencyGraph.cs new file mode 100644 index 000000000..e7a82b7ff --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/DependencyGraph.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Microsoft.ComponentDetection.Contracts.BcdeModels +{ + public class DependencyGraph : Dictionary> + { + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/BcdeModels/DependencyGraphCollection.cs b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/DependencyGraphCollection.cs new file mode 100644 index 000000000..4651c776b --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/DependencyGraphCollection.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Microsoft.ComponentDetection.Contracts.BcdeModels +{ + public class DependencyGraphCollection : Dictionary + { + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/BcdeModels/DependencyGraphWithMetadata.cs b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/DependencyGraphWithMetadata.cs new file mode 100644 index 000000000..3b99fe029 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/DependencyGraphWithMetadata.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.ComponentDetection.Contracts.BcdeModels +{ + [JsonObject(MemberSerialization.OptOut, NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class DependencyGraphWithMetadata + { + public DependencyGraph Graph { get; set; } + + public HashSet ExplicitlyReferencedComponentIds { get; set; } + + public HashSet DevelopmentDependencies { get; set; } + + public HashSet Dependencies { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/BcdeModels/Detector.cs b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/Detector.cs new file mode 100644 index 000000000..a90381d8c --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/Detector.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.ComponentDetection.Contracts.BcdeModels +{ + [JsonObject(MemberSerialization.OptOut, NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class Detector + { + public string DetectorId { get; set; } + + public bool IsExperimental { get; set; } + + public int Version { get; set; } + + [JsonProperty(ItemConverterType = typeof(StringEnumConverter))] + public IEnumerable SupportedComponentTypes { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/BcdeModels/DockerLayer.cs b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/DockerLayer.cs new file mode 100644 index 000000000..8dacf105f --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/DockerLayer.cs @@ -0,0 +1,22 @@ +namespace Microsoft.ComponentDetection.Contracts.BcdeModels +{ + public class DockerLayer + { + // Summary: + // the command/script that was executed in order to create the layer. + public string CreatedBy { get; set; } + + // Summary: + // The Layer hash (docker inspect) that represents the changes between this layer and the previous layer + public string DiffId { get; set; } + + // Summary: + // Whether or not this layer was found in the base image of the container + public bool IsBaseImage { get; set; } + + // Summary: + // 0-indexed monotonically increasing ID for the order of the layer in the container. + // Note: only includes non-empty layers + public int LayerIndex { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Contracts/BcdeModels/LayerMappedLinuxComponents.cs b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/LayerMappedLinuxComponents.cs new file mode 100644 index 000000000..760dd725e --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/LayerMappedLinuxComponents.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Microsoft.ComponentDetection.Contracts.TypedComponent; + +namespace Microsoft.ComponentDetection.Contracts.BcdeModels +{ + public class LayerMappedLinuxComponents + { + public IEnumerable LinuxComponents { get; set; } + + public DockerLayer DockerLayer { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Contracts/BcdeModels/ScanResult.cs b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/ScanResult.cs new file mode 100644 index 000000000..c8d515eea --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/ScanResult.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.ComponentDetection.Contracts.BcdeModels +{ + [JsonObject(MemberSerialization.OptOut, NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class ScanResult + { + public IEnumerable ComponentsFound { get; set; } + + public IEnumerable DetectorsInScan { get; set; } + + public Dictionary ContainerDetailsMap { get; set; } + + [JsonConverter(typeof(StringEnumConverter))] + public ProcessingResultCode ResultCode { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/BcdeModels/ScannedComponent.cs b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/ScannedComponent.cs new file mode 100644 index 000000000..d6b985faa --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/ScannedComponent.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.ComponentDetection.Contracts.BcdeModels +{ + [JsonObject(MemberSerialization.OptOut, NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class ScannedComponent + { + public IEnumerable LocationsFoundAt { get; set; } + + public TypedComponent.TypedComponent Component { get; set; } + + public string DetectorId { get; set; } + + public bool? IsDevelopmentDependency { get; set; } + + public IEnumerable TopLevelReferrers { get; set; } + + public IEnumerable ContainerDetailIds { get; set; } + + public IDictionary> ContainerLayerIds { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/BcdeModels/TypedComponentConverter.cs b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/TypedComponentConverter.cs new file mode 100644 index 000000000..71531c293 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/TypedComponentConverter.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.ComponentDetection.Contracts.BcdeModels +{ + public class TypedComponentConverter : JsonConverter + { + private Dictionary componentTypesToTypes = new Dictionary + { + { ComponentType.Other, typeof(OtherComponent) }, + { ComponentType.NuGet, typeof(NuGetComponent) }, + { ComponentType.Npm, typeof(NpmComponent) }, + { ComponentType.Maven, typeof(MavenComponent) }, + { ComponentType.Git, typeof(GitComponent) }, + { ComponentType.RubyGems, typeof(RubyGemsComponent) }, + { ComponentType.Cargo, typeof(CargoComponent) }, + { ComponentType.Pip, typeof(PipComponent) }, + { ComponentType.Go, typeof(GoComponent) }, + { ComponentType.DockerImage, typeof(DockerImageComponent) }, + { ComponentType.Pod, typeof(PodComponent) }, + { ComponentType.Linux, typeof(LinuxComponent) }, + { ComponentType.Conda, typeof(CondaComponent) }, + }; + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(TypedComponent.TypedComponent); + } + + public override object ReadJson( + JsonReader reader, + Type objectType, object existingValue, JsonSerializer serializer) + { + JToken jo = JToken.Load(reader); + + var value = (ComponentType)Enum.Parse(typeof(ComponentType), (string)jo["type"]); + var targetType = componentTypesToTypes[value]; + var instanceOfTypedComponent = Activator.CreateInstance(targetType, true); + serializer.Populate(jo.CreateReader(), instanceOfTypedComponent); + + return instanceOfTypedComponent; + } + + public override bool CanWrite + { + get { return false; } + } + + public override void WriteJson( + JsonWriter writer, + object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/DetectedComponent.cs b/src/Microsoft.ComponentDetection.Contracts/DetectedComponent.cs new file mode 100644 index 000000000..166fa3948 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/DetectedComponent.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.ComponentDetection.Contracts +{ + /// A detected component, found during component detection scans. This is the container for all metadata gathered during detection. + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class DetectedComponent + { + private readonly object hashLock = new object(); + + /// Creates a new DetectedComponent. + /// The typed component instance to base this detection on. + /// The detector that detected this component. + /// Id of the containerDetails, this is only necessary if the component was found inside a container. + /// Id of the layer the component was found, this is only necessary if the component was found inside a container. + public DetectedComponent(TypedComponent.TypedComponent component, IComponentDetector detector = null, int? containerDetailsId = null, int? containerLayerId = null) + { + Component = component; + FilePaths = new HashSet(); + DetectedBy = detector; + ContainerDetailIds = new HashSet(); + ContainerLayerIds = new Dictionary>(); + if (containerDetailsId.HasValue) + { + ContainerDetailIds.Add(containerDetailsId.Value); + if (containerLayerId.HasValue) + { + ContainerLayerIds.Add(containerDetailsId.Value, new List() { containerLayerId.Value }); + } + } + } + + private string DebuggerDisplay => $"{Component.DebuggerDisplay}"; + + /// + /// Gets or sets the detector that detected this component. + /// + public IComponentDetector DetectedBy { get; set; } + + /// Gets the component associated with this detection. + public TypedComponent.TypedComponent Component { get; private set; } + + /// Gets or sets the hashset containing the file paths associated with the component. + public HashSet FilePaths { get; set; } + + /// Gets or sets the dependency roots for this component. + public HashSet DependencyRoots { get; set; } + + /// Gets or sets the flag to mark the component as a development dependency or not. + /// This is used at build or development time not a distributed dependency. + public bool? DevelopmentDependency { get; set; } + + /// Gets or sets the details of the container where this component was found. + public HashSet ContainerDetailIds { get; set; } + + /// Gets or sets the layer within a container where this component was found. + public IDictionary> ContainerLayerIds { get; set; } + + /// Adds a filepath to the FilePaths hashset for this detected component. + /// The file path to add to the hashset. + public void AddComponentFilePath(string filePath) + { + lock (hashLock) + { + FilePaths.Add(filePath); + } + } + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/DetectorClass.cs b/src/Microsoft.ComponentDetection.Contracts/DetectorClass.cs new file mode 100644 index 000000000..db8898b3f --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/DetectorClass.cs @@ -0,0 +1,39 @@ +namespace Microsoft.ComponentDetection.Contracts +{ + /// Class of detector, the names of which are converted into categories for all default detectors. + public enum DetectorClass + { + /// Default value, which indicates all classes should be run. Not used as an actual category. + All, + + /// Indicates a detector applies to NPM packages. + Npm, + + /// Indicates a detector applies to NuGet packages. + NuGet, + + /// Indicates a detector applies to Maven packages. + Maven, + + /// Indicates a detector applies to RubyGems packages. + RubyGems, + + /// Indicates a detector applies to Cargo packages. + Cargo, + + /// Indicates a detector applies to Pip packages. + Pip, + + /// Indicates a detector applies to Go modules + GoMod, + + /// Indicates a detector applies to CocoaPods packages. + CocoaPods, + + /// Indicates a detector applies to Linux packages. + Linux, + + /// Indicates a detector applies to Conda packages. + Conda, + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/FileComponentDetector.cs b/src/Microsoft.ComponentDetection.Contracts/FileComponentDetector.cs new file mode 100644 index 000000000..d6428b456 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/FileComponentDetector.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Composition; +using System.IO; +using System.Reactive.Linq; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using Microsoft.ComponentDetection.Contracts.Internal; +using Microsoft.ComponentDetection.Contracts.TypedComponent; + +namespace Microsoft.ComponentDetection.Contracts +{ + /// Specialized base class for file based component detection. + public abstract class FileComponentDetector : IComponentDetector + { + /// + /// Gets or sets the factory for handing back component streams to File detectors. Injected automatically by MEF composition. + /// + [Import] + public IComponentStreamEnumerableFactory ComponentStreamEnumerableFactory { get; set; } + + /// Gets or sets the logger for writing basic logging message to both console and file. Injected automatically by MEF composition. + [Import] + public ILogger Logger { get; set; } + + public IComponentRecorder ComponentRecorder { get; private set; } + + /// + public abstract string Id { get; } + + /// Gets the search patterns used to produce the list of valid folders to scan. These patterns are evaluated with .Net's Directory.EnumerateFiles function. + public abstract IList SearchPatterns { get; } + + /// Gets the categories this detector is considered a member of. Used by the DetectorCategories arg to include detectors. + public abstract IEnumerable Categories { get; } + + /// + /// Gets the folder names that will be skipped by the Component Detector. + /// + protected virtual IList SkippedFolders => new List { }; + + /// + /// Gets or sets the active scan request -- only populated after a ScanDirectoryAsync is invoked. If ScanDirectoryAsync is overridden, + /// the overrider should ensure this property is populated. + /// + protected ScanRequest CurrentScanRequest { get; set; } + + /// Gets the supported component types. + public abstract IEnumerable SupportedComponentTypes { get; } + + /// Gets the version of this component detector. + public abstract int Version { get; } + + [Import] + public IObservableDirectoryWalkerFactory Scanner { get; set; } + + protected IObservable ComponentStreams { get; private set; } + + public bool NeedsAutomaticRootDependencyCalculation { get; protected set; } + + protected Dictionary Telemetry { get; set; } = new Dictionary(); + + /// + public async virtual Task ExecuteDetectorAsync(ScanRequest request) + { + ComponentRecorder = request.ComponentRecorder; + Scanner.Initialize(request.SourceDirectory, request.DirectoryExclusionPredicate, 1); + return await ScanDirectoryAsync(request); + } + + /// + private Task ScanDirectoryAsync(ScanRequest request) + { + CurrentScanRequest = request; + + var filteredObservable = Scanner.GetFilteredComponentStreamObservable(request.SourceDirectory, SearchPatterns, request.ComponentRecorder); + + Logger?.LogVerbose($"Registered {GetType().FullName}"); + return ProcessAsync(filteredObservable, request.DetectorArgs); + } + + /// + /// Gets the file streams for the Detector's declared as an . + /// + /// The directory to search. + /// The exclusion predicate function. + /// + protected Task> GetFileStreamsAsync(DirectoryInfo sourceDirectory, ExcludeDirectoryPredicate exclusionPredicate) + { + return Task.FromResult(ComponentStreamEnumerableFactory.GetComponentStreams(sourceDirectory, SearchPatterns, exclusionPredicate)); + } + + private async Task ProcessAsync(IObservable processRequests, IDictionary detectorArgs) + { + var processor = new ActionBlock(async processRequest => await OnFileFound(processRequest, detectorArgs)); + + var preprocessedObserbable = await OnPrepareDetection(processRequests, detectorArgs); + + await preprocessedObserbable.ForEachAsync(processRequest => processor.Post(processRequest)); + + processor.Complete(); + + await processor.Completion; + + await OnDetectionFinished(); + + return new IndividualDetectorScanResult + { + ResultCode = ProcessingResultCode.Success, + AdditionalTelemetryDetails = Telemetry, + }; + } + + protected virtual Task> OnPrepareDetection(IObservable processRequests, IDictionary detectorArgs) + { + return Task.FromResult(processRequests); + } + + protected abstract Task OnFileFound(ProcessRequest processRequest, IDictionary detectorArgs); + + protected virtual Task OnDetectionFinished() + { + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Contracts/ICommandLineInvocationService.cs b/src/Microsoft.ComponentDetection.Contracts/ICommandLineInvocationService.cs new file mode 100644 index 000000000..67e901716 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/ICommandLineInvocationService.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace Microsoft.ComponentDetection.Contracts +{ + /// + /// Service for managing execution on a command line. Generally, methods on this service expect a command line environment to be + /// where the detection tool is running, so all logic relying on them should gate on .IsCommandLineExecution(). + /// + public interface ICommandLineInvocationService + { + /// + /// Used to gate logic that requires a command line execution environment. + /// + /// True if it is a command line execution environment, false otherwise. + bool IsCommandLineExecution(); + + /// + /// Checks to see if the given command can be located -- in cases of absolute paths, this is a simple File.Exists. For non-absolute paths, all PATH entries are checked. + /// + /// The command name to execute. Environment variables like PATH on windows will also be considered if the command is not an absolute path. + /// Other commands that could satisfy the need for the first command. Assumption is that they all share similar calling patterns. + /// The directory under which to execute the command. + /// The parameters that should be passed to the command. The parameters will be space-joined. + /// Awaitable task with true if the command can be found in the local environment, false otherwise. + Task CanCommandBeLocated(string command, IEnumerable additionalCandidateCommands = null, DirectoryInfo workingDirectory = null, params string[] parameters); + + /// + /// Checks to see if the given command can be located -- in cases of absolute paths, this is a simple File.Exists. For non-absolute paths, all PATH entries are checked. + /// + /// The command name to execute. Environment variables like PATH on windows will also be considered if the command is not an absolute path. + /// Other commands that could satisfy the need for the first command. Assumption is that they all share similar calling patterns. + /// The parameters that should be passed to the command. The parameters will be space-joined. + /// Awaitable task with true if the command can be found in the local environment, false otherwise. + Task CanCommandBeLocated(string command, IEnumerable additionalCandidateCommands = null, params string[] parameters); + + /// + /// Executes a command line command. If the command has not been located yet, CanCommandBeLocated will be invoked without the submitted parameters. + /// + /// The command name to execute. Environment variables like PATH on windows will also be considered if the command is not an absolute path. + /// Other commands that could satisfy the need for the first command. Assumption is that they all share similar calling patterns. + /// The directory under which to run the command. + /// The parameters that should be passed to the command. The parameters will be space-joined. + /// Awaitable task with the result of executing the command, including exit code. + Task ExecuteCommand(string command, IEnumerable additionalCandidateCommands = null, DirectoryInfo workingDirectory = null, params string[] parameters); + + /// + /// Executes a command line command. If the command has not been located yet, CanCommandBeLocated will be invoked without the submitted parameters. + /// + /// The command name to execute. Environment variables like PATH on windows will also be considered if the command is not an absolute path. + /// Other commands that could satisfy the need for the first command. Assumption is that they all share similar calling patterns. + /// The parameters that should be passed to the command. The parameters will be space-joined. + /// Awaitable task with the result of executing the command, including exit code. + Task ExecuteCommand(string command, IEnumerable additionalCandidateCommands = null, params string[] parameters); + } + + public class CommandLineExecutionResult + { + /// + /// Gets or sets all standard output for the process execution. + /// + public string StdOut { get; set; } + + /// + /// Gets or sets all standard error output for the process execution. + /// + public string StdErr { get; set; } + + /// + /// Gets or sets the process exit code for the executed process. + /// + public int ExitCode { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/IComponentDetector.cs b/src/Microsoft.ComponentDetection.Contracts/IComponentDetector.cs new file mode 100644 index 000000000..7c52a7a4d --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/IComponentDetector.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.ComponentDetection.Contracts.TypedComponent; + +namespace Microsoft.ComponentDetection.Contracts +{ + /// + /// Basic interface required for something to satisfy component detection. + /// If you are writing a File based component detector, you may prefer the class. + /// + public interface IComponentDetector + { + /// Gets the id of the detector. Should be unique to the detector using a "namespace"-like prefix for your detectors is recommended. + string Id { get; } + + /// + /// Gets the set of categories this detector is a member of. + /// Names of the enumeration comprise some of the built in categories. + /// If the category "All" is specified, the detector will always run. + /// + IEnumerable Categories { get; } + + /// + /// Gets the set of supported component type this detector is a member of. Names of the enumeration comprise some of the built in component type. + /// + IEnumerable SupportedComponentTypes { get; } + + /// + /// Gets the version of the component detector. + /// + int Version { get; } + + /// + /// Run the detector and return the result set of components found. + /// + /// + Task ExecuteDetectorAsync(ScanRequest request); + + /// + /// Gets a value indicating whether this detector needs automatic root dependency calculation or is going to be specified as part of RegisterUsage. + /// + bool NeedsAutomaticRootDependencyCalculation { get; } + } + + /// + /// Component detectors implementing this interface are, by default, off. This is used during composition to opt detectors out of being on by default. + /// If opted in, they should behave like a normal detector. + /// + public interface IDefaultOffComponentDetector : IComponentDetector + { + } + + /// + /// Component detectors implementing this interface are in an experimental state. + /// The detector processing service guarantees that: + /// They should NOT return their components as part of the scan result or be allowed to run too long (e.g. 2 min or less). + /// They SHOULD submit telemetry about how they ran. + /// If opted in, they should behave like a normal detector. + /// + public interface IExperimentalDetector : IComponentDetector + { + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/IComponentRecorder.cs b/src/Microsoft.ComponentDetection.Contracts/IComponentRecorder.cs new file mode 100644 index 000000000..64dd3e00b --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/IComponentRecorder.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; + +namespace Microsoft.ComponentDetection.Contracts +{ + public interface IComponentRecorder + { + TypedComponent.TypedComponent GetComponent(string componentId); + + IEnumerable GetDetectedComponents(); + + ISingleFileComponentRecorder CreateSingleFileComponentRecorder(string location); + + IReadOnlyDictionary GetDependencyGraphsByLocation(); + } + + public interface ISingleFileComponentRecorder + { + string ManifestFileLocation { get; } + + /// + /// Add or Update a component. In case that a parent componentId is specified + /// an edge is created between those components in the dependency graph. + /// + /// Component to add. + /// The value define if the component was referenced manually by the user in the location where the scanning is taking place. + /// Id of the parent component. + /// Boolean value indicating whether or not a component is a development-time dependency. Null implies that the value is unknown. + /// DetectedComponent added or updated. + void RegisterUsage( + DetectedComponent detectedComponent, + bool isExplicitReferencedDependency = false, + string parentComponentId = null, + bool? isDevelopmentDependency = null); + + DetectedComponent GetComponent(string componentId); + + void AddAdditionalRelatedFile(string relatedFilePath); + + IReadOnlyDictionary GetDetectedComponents(); + + IComponentRecorder GetParentComponentRecorder(); + + IDependencyGraph DependencyGraph { get; } + } + + public interface IDependencyGraph + { + /// + /// Gets the componentIds that are dependencies for a given componentId. + /// + /// The component id to look up dependencies for. + /// The componentIds that are dependencies for a given componentId. + IEnumerable GetDependenciesForComponent(string componentId); + + /// + /// Gets all componentIds that are in the dependency graph. + /// + /// The componentIds that are part of the dependency graph. + IEnumerable GetComponents(); + + /// + /// Returns true if a componentId is an explicitly referenced dependency. + /// + /// The componentId to check. + /// True if explicitly referenced, false otherwise. + bool IsComponentExplicitlyReferenced(string componentId); + + HashSet GetAdditionalRelatedFiles(); + + /// + /// Returns true if a componentId is registered in the graph. + /// + /// The componentId to check. + /// True if registered in the graph, false otherwise. + bool Contains(string componentId); + + /// + /// Returns true if a componentId is a development dependency, and false if it is not. + /// Null can be returned if a detector doesn't have confidence one way or the other. + /// + /// The componentId to check. + /// True if a development dependency, false if not. Null when unknown. + bool? IsDevelopmentDependency(string componentId); + + /// + /// Gets the component IDs of all explicitly referenced components. + /// + /// An enumerable of the component IDs of all explicilty referenced components. + IEnumerable GetAllExplicitlyReferencedComponents(); + + /// + /// Returns the set of component ids that are explicit references to the given component id. + /// + /// The leaf level component to find explicit references for. + /// A collection fo all explicit references to the given component. + ICollection GetExplicitReferencedDependencyIds(string componentId); + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/IComponentStream.cs b/src/Microsoft.ComponentDetection.Contracts/IComponentStream.cs new file mode 100644 index 000000000..cec022c44 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/IComponentStream.cs @@ -0,0 +1,22 @@ +using System.IO; + +namespace Microsoft.ComponentDetection.Contracts +{ + public interface IComponentStream + { + /// + /// Gets the stream object that was discovered by the provided pattern to />. + /// + Stream Stream { get; } + + /// + /// Gets the pattern that this stream matched. Ex: If *.bar was used to match Foo.bar, this field would contain *.bar. + /// + string Pattern { get; } + + /// + /// Gets the location for this stream. Often a file path if not in test circumstances. + /// + string Location { get; } + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/IComponentStreamEnumerableFactory.cs b/src/Microsoft.ComponentDetection.Contracts/IComponentStreamEnumerableFactory.cs new file mode 100644 index 000000000..c6e0d1f80 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/IComponentStreamEnumerableFactory.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.ComponentDetection.Contracts +{ + public interface IComponentStreamEnumerableFactory + { + /// + /// Returns an enumerable of which are representative of the contents of underlying files that matched the + /// provided search pattern and exclusion function. Each stream is disposed on the end of a single iteration of a foreach loop. + /// + /// The directory to search "from", e.g. the top level directory being searched. + /// The patterns to use in the search. + /// Predicate which indicates which directories should be excluded. + /// Indicates whether the streams should enumerate files from sub directories. + /// + IEnumerable GetComponentStreams(DirectoryInfo directory, IEnumerable searchPatterns, ExcludeDirectoryPredicate directoryExclusionPredicate, bool recursivelyScanDirectories = true); + + /// + /// Returns an enumerable of which are representative of the contents of underlying files that matched the + /// provided search and exclusion functions. Each stream is disposed on the end of a single iteration of a foreach loop. + /// + /// The directory to search "from", e.g. the top level directory being searched. + /// Predicate which indicates what files should be included. + /// Predicate which indicates which directories should be excluded. + /// Indicates whether the streams should enumerate files from sub directories. + /// + IEnumerable GetComponentStreams(DirectoryInfo directory, Func fileMatchingPredicate, ExcludeDirectoryPredicate directoryExclusionPredicate, bool recursivelyScanDirectories = true); + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/IDetectorDependencies.cs b/src/Microsoft.ComponentDetection.Contracts/IDetectorDependencies.cs new file mode 100644 index 000000000..e7a22235b --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/IDetectorDependencies.cs @@ -0,0 +1,19 @@ +namespace Microsoft.ComponentDetection.Contracts +{ + public interface IDetectorDependencies + { + ILogger Logger { get; set; } + + IComponentStreamEnumerableFactory ComponentStreamEnumerableFactory { get; set; } + + IPathUtilityService PathUtilityService { get; set; } + + ICommandLineInvocationService CommandLineInvocationService { get; set; } + + IFileUtilityService FileUtilityService { get; set; } + + IObservableDirectoryWalkerFactory DirectoryWalkerFactory { get; set; } + + IDockerService DockerService { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/IDockerService.cs b/src/Microsoft.ComponentDetection.Contracts/IDockerService.cs new file mode 100644 index 000000000..39572080c --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/IDockerService.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.ComponentDetection.Contracts.BcdeModels; + +namespace Microsoft.ComponentDetection.Contracts +{ + public interface IDockerService + { + Task CanRunLinuxContainersAsync(CancellationToken cancellationToken = default); + + Task CanPingDockerAsync(CancellationToken cancellationToken = default); + + Task ImageExistsLocallyAsync(string image, CancellationToken cancellationToken = default); + + Task TryPullImageAsync(string image, CancellationToken cancellationToken = default); + + Task InspectImageAsync(string image, CancellationToken cancellationToken = default); + + Task<(string stdout, string stderr)> CreateAndRunContainerAsync(string image, IList command, + CancellationToken cancellationToken = default); + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/IFileUtilityService.cs b/src/Microsoft.ComponentDetection.Contracts/IFileUtilityService.cs new file mode 100644 index 000000000..2a0582963 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/IFileUtilityService.cs @@ -0,0 +1,18 @@ +using System.IO; + +namespace Microsoft.ComponentDetection.Contracts +{ + /// + /// Wraps some common file operations for easier testability. This interface is *only used by the command line driven app*. + /// + public interface IFileUtilityService + { + string ReadAllText(string filePath); + + string ReadAllText(FileInfo file); + + bool Exists(string fileName); + + Stream MakeFileStream(string fileName); + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Contracts/ILogger.cs b/src/Microsoft.ComponentDetection.Contracts/ILogger.cs new file mode 100644 index 000000000..5ec5c6399 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/ILogger.cs @@ -0,0 +1,58 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Microsoft.ComponentDetection.Contracts +{ + /// Simple abstraction around console/output file logging for component detection. + public interface ILogger + { + /// Creates a logical separation (e.g. newline) between different log messages. + void LogCreateLoggingGroup(); + + /// Logs a warning message, outputting if configured verbosity is higher than Quiet. + /// The message to output. + void LogWarning(string message); + + /// Logs an informational message, outputting if configured verbosity is higher than Quiet. + /// The message to output. + void LogInfo(string message); + + /// Logs a verbose message, outputting if configured verbosity is at least Verbose. + /// The message to output. + void LogVerbose(string message); + + /// Logs an error message, outputting for all verbosity levels. + /// The message to output. + void LogError(string message); + + /// Logs a specially formatted message if a file read failed, outputting if configured verbosity is at least Verbose. + /// The file path responsible for the file reading failure. + /// The exception encountered when reading a file. + void LogFailedReadingFile(string filePath, Exception e); + + /// Logs a specially formatted message if an exception has occurred. + /// The exception to log the occurance of. + /// Whether or not the exception represents a true error case (e.g. unexpected) vs. expected. + /// Indicate if the exception is going to be fully printed. + /// Implicity populated arg, provides the member name of the calling function to the log message. + /// Implicitly populated arg, provides calling line number. + void LogException( + Exception e, + bool isError, + bool printException = false, + [CallerMemberName] string callerMemberName = "", + [CallerLineNumber] int callerLineNumber = 0); + + /// + /// Log a warning to the build console, adding it to the build summary and turning the build yellow. + /// + /// The message to display alongside the warning. + void LogBuildWarning(string message); + + /// + /// Log an error to the build console, adding it to the build summary and turning the build red. + /// + /// The message to display alongside the warning. + void LogBuildError(string message); + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/IObservableDirectoryWalkerFactory.cs b/src/Microsoft.ComponentDetection.Contracts/IObservableDirectoryWalkerFactory.cs new file mode 100644 index 000000000..45c7c927e --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/IObservableDirectoryWalkerFactory.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.ComponentDetection.Contracts.Internal; + +namespace Microsoft.ComponentDetection.Contracts +{ + public delegate bool ExcludeDirectoryPredicate(ReadOnlySpan nameOfDirectoryToConsider, ReadOnlySpan pathOfParentOfDirectoryToConsider); + + public interface IObservableDirectoryWalkerFactory + { + void Initialize(DirectoryInfo root, ExcludeDirectoryPredicate directoryExclusionPredicate, int count, IEnumerable filePatterns = null); + + IObservable GetFilteredComponentStreamObservable(DirectoryInfo root, IEnumerable patterns, IComponentRecorder componentRecorder); + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Contracts/IPathUtilityService.cs b/src/Microsoft.ComponentDetection.Contracts/IPathUtilityService.cs new file mode 100644 index 000000000..5dc692032 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/IPathUtilityService.cs @@ -0,0 +1,33 @@ +namespace Microsoft.ComponentDetection.Contracts +{ + /// + /// Wraps some common folder operations, shared across command line app and service. + /// + public interface IPathUtilityService + { + string GetParentDirectory(string path); + + /// + /// Given a path, resolve the underlying path, traversing any symlinks (man 2 lstat :D ). + /// + /// + /// + string ResolvePhysicalPath(string path); + + /// + /// Returns true when the below file path exists under the above file path. + /// + /// + /// + /// + bool IsFileBelowAnother(string aboveFilePath, string belowFilePath); + + /// + /// Returns true if file name matches pattern. + /// + /// Search pattern. + /// File name without directory. + /// + bool MatchesPattern(string searchPattern, string fileName); + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/IndividualDetectorScanResult.cs b/src/Microsoft.ComponentDetection.Contracts/IndividualDetectorScanResult.cs new file mode 100644 index 000000000..160f685d6 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/IndividualDetectorScanResult.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.ComponentDetection.Contracts.BcdeModels; + +namespace Microsoft.ComponentDetection.Contracts +{ + /// Results object for a component scan. + public class IndividualDetectorScanResult + { + /// Gets or sets the result code for the scan, indicating Success or Failure. + public ProcessingResultCode ResultCode { get; set; } + + public IEnumerable ContainerDetails { get; set; } = Enumerable.Empty(); + + public Dictionary AdditionalTelemetryDetails { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Contracts/Internal/InjectionParameters.cs b/src/Microsoft.ComponentDetection.Contracts/Internal/InjectionParameters.cs new file mode 100644 index 000000000..981f23a3f --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/Internal/InjectionParameters.cs @@ -0,0 +1,59 @@ +using System.Composition; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.ComponentDetection.Orchestrator")] + +namespace Microsoft.ComponentDetection.Contracts.Internal +{ + // Why do we have this weird garbage code? + // Because https://github.com/dotnet/corefx/issues/11856 + // This ugly file is here mostly because we don't have a way to easily expose instances to our composed detectors (NetStandard MEF hasn't received the love). + // It needs to live in the Contracts project to isolate part discovery to a different assembly. + // It also makes a little bit of sense because everything that IComponentDetectors can inject is in this file :). + internal class InjectionParameters + { + public InjectionParameters() + { + } + + internal InjectionParameters(IDetectorDependencies detectorDependencies) + { + loggerStatic = detectorDependencies.Logger; + factoryStatic = detectorDependencies.ComponentStreamEnumerableFactory; + pathUtilityServiceStatic = detectorDependencies.PathUtilityService; + commandLineInvocationServiceStatic = detectorDependencies.CommandLineInvocationService; + fileUtilityServiceStatic = detectorDependencies.FileUtilityService; + observableDirectoryWalkerFactoryServiceStatic = detectorDependencies.DirectoryWalkerFactory; + dockerServiceStatic = detectorDependencies.DockerService; + } + + private static ILogger loggerStatic; + private static IComponentStreamEnumerableFactory factoryStatic; + private static IPathUtilityService pathUtilityServiceStatic; + private static ICommandLineInvocationService commandLineInvocationServiceStatic; + private static IFileUtilityService fileUtilityServiceStatic; + private static IObservableDirectoryWalkerFactory observableDirectoryWalkerFactoryServiceStatic; + private static IDockerService dockerServiceStatic; + + [Export(typeof(ILogger))] + public ILogger Logger => loggerStatic; + + [Export(typeof(IComponentStreamEnumerableFactory))] + public IComponentStreamEnumerableFactory Factory => factoryStatic; + + [Export(typeof(IPathUtilityService))] + public IPathUtilityService PathUtilityService => pathUtilityServiceStatic; + + [Export(typeof(ICommandLineInvocationService))] + public ICommandLineInvocationService CommandLineInvocationService => commandLineInvocationServiceStatic; + + [Export(typeof(IFileUtilityService))] + public IFileUtilityService FileUtilityService => fileUtilityServiceStatic; + + [Export(typeof(IObservableDirectoryWalkerFactory))] + public IObservableDirectoryWalkerFactory ObservableDirectoryWalkerFactory => observableDirectoryWalkerFactoryServiceStatic; + + [Export(typeof(IDockerService))] + public IDockerService DockerService => dockerServiceStatic; + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/Internal/ProcessRequest.cs b/src/Microsoft.ComponentDetection.Contracts/Internal/ProcessRequest.cs new file mode 100644 index 000000000..66d21349c --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/Internal/ProcessRequest.cs @@ -0,0 +1,9 @@ +namespace Microsoft.ComponentDetection.Contracts.Internal +{ + public class ProcessRequest + { + public IComponentStream ComponentStream { get; set; } + + public ISingleFileComponentRecorder SingleFileComponentRecorder { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/KillSwitchConfiguration.cs b/src/Microsoft.ComponentDetection.Contracts/KillSwitchConfiguration.cs new file mode 100644 index 000000000..8d6c5e515 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/KillSwitchConfiguration.cs @@ -0,0 +1,9 @@ +namespace Microsoft.ComponentDetection.Contracts +{ + public class KillSwitchConfiguration + { + public bool IsDetectionStopped { get; set; } + + public string ReasonForStoppingDetection { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/Microsoft.ComponentDetection.Contracts.csproj b/src/Microsoft.ComponentDetection.Contracts/Microsoft.ComponentDetection.Contracts.csproj new file mode 100644 index 000000000..2b7c925f7 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/Microsoft.ComponentDetection.Contracts.csproj @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/Microsoft.ComponentDetection.Contracts/ProcessingResultCode.cs b/src/Microsoft.ComponentDetection.Contracts/ProcessingResultCode.cs new file mode 100644 index 000000000..b3a87d1c4 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/ProcessingResultCode.cs @@ -0,0 +1,21 @@ +namespace Microsoft.ComponentDetection.Contracts +{ + /// Code used to communicate the state of a scan after completion. + public enum ProcessingResultCode + { + /// The scan was completely successful. + Success = 0, + + /// The scan had some detections complete while others encountered errors. The log file should indicate any issues that happened during the scan. + PartialSuccess = 1, + + /// A critical error occurred during the scan. + Error = 2, + + /// A critical error occurred during the scan, related to user input specifically. + InputError = 3, + + /// The execution exceeded the expected amount of time to run. + TimeoutError = 4, + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/ScanRequest.cs b/src/Microsoft.ComponentDetection.Contracts/ScanRequest.cs new file mode 100644 index 000000000..62d7fc75c --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/ScanRequest.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.ComponentDetection.Contracts +{ + /// Request object for a component scan. + public class ScanRequest + { + /// Creates a new ScanRequest. + /// The source directory to consider the working directory for the detection operation. + /// A predicate which evaluates directories, if the predicate returns true the directory will be excluded. + /// The logger for this detection session. + /// A dictionary of custom detector arguments supplied externally. + /// Container images to scan. + /// Detector component recorder. + public ScanRequest(DirectoryInfo sourceDirectory, ExcludeDirectoryPredicate directoryExclusionPredicate, ILogger logger, IDictionary detectorArgs, IEnumerable imagesToScan, IComponentRecorder componentRecorder) + { + SourceDirectory = sourceDirectory; + DirectoryExclusionPredicate = directoryExclusionPredicate; + DetectorArgs = detectorArgs; + ImagesToScan = imagesToScan; + ComponentRecorder = componentRecorder; + } + + /// Gets the source directory to consider the working directory for the detection operation. + public DirectoryInfo SourceDirectory { get; private set; } + + /// Gets a predicate which evaluates directories, if the predicate returns true the directory will be excluded. + public ExcludeDirectoryPredicate DirectoryExclusionPredicate { get; private set; } + + /// Gets the dictionary of custom detector arguments supplied externally. + public IDictionary DetectorArgs { get; private set; } + + public IEnumerable ImagesToScan { get; private set; } + + public IComponentRecorder ComponentRecorder { get; private set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/CargoComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/CargoComponent.cs new file mode 100644 index 000000000..0859dd0f6 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/CargoComponent.cs @@ -0,0 +1,28 @@ +using PackageUrl; + +namespace Microsoft.ComponentDetection.Contracts.TypedComponent +{ + public class CargoComponent : TypedComponent + { + private CargoComponent() + { + // reserved for deserialization + } + + public CargoComponent(string name, string version) + { + Name = ValidateRequiredInput(name, nameof(Name), nameof(ComponentType.Cargo)); + Version = ValidateRequiredInput(version, nameof(Version), nameof(ComponentType.Cargo)); + } + + public string Name { get; set; } + + public string Version { get; set; } + + public override ComponentType Type => ComponentType.Cargo; + + public override string Id => $"{Name} {Version} - {Type}"; + + public override PackageURL PackageUrl => new PackageURL("cargo", string.Empty, Name, Version, null, string.Empty); + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ComponentType.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ComponentType.cs new file mode 100644 index 000000000..c441c129c --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ComponentType.cs @@ -0,0 +1,48 @@ +using System.Runtime.Serialization; + +namespace Microsoft.ComponentDetection.Contracts.TypedComponent +{ + // This is used in BcdeModels as well + [DataContract] + public enum ComponentType : byte + { + [EnumMember] + Other = 0, + + [EnumMember] + NuGet = 1, + + [EnumMember] + Npm = 2, + + [EnumMember] + Maven = 3, + + [EnumMember] + Git = 4, + + [EnumMember] + RubyGems = 6, + + [EnumMember] + Cargo = 7, + + [EnumMember] + Pip = 8, + + [EnumMember] + Go = 9, + + [EnumMember] + DockerImage = 10, + + [EnumMember] + Pod = 11, + + [EnumMember] + Linux = 12, + + [EnumMember] + Conda = 13, + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/CondaComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/CondaComponent.cs new file mode 100644 index 000000000..b83b3e505 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/CondaComponent.cs @@ -0,0 +1,42 @@ +namespace Microsoft.ComponentDetection.Contracts.TypedComponent +{ + public class CondaComponent : TypedComponent + { + private CondaComponent() + { + /* Reserved for deserialization */ + } + + public CondaComponent(string name, string version, string build, string channel, string subdir, string @namespace, string url, string md5) + { + Name = ValidateRequiredInput(name, nameof(Name), nameof(ComponentType.Conda)); + Version = ValidateRequiredInput(version, nameof(Version), nameof(ComponentType.Conda)); + Build = build; + Channel = channel; + Subdir = subdir; + Namespace = @namespace; + Url = url; + MD5 = md5; + } + + public string Build { get; } + + public string Channel { get; } + + public string Name { get; } + + public string Namespace { get; } + + public string Subdir { get; } + + public string Version { get; } + + public string Url { get; } + + public string MD5 { get; } + + public override ComponentType Type => ComponentType.Conda; + + public override string Id => $"{Name} {Version} {Build} {Channel} {Subdir} {Namespace} {Url} {MD5} - {Type}"; + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/DockerImageComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/DockerImageComponent.cs new file mode 100644 index 000000000..694e13ccd --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/DockerImageComponent.cs @@ -0,0 +1,27 @@ +namespace Microsoft.ComponentDetection.Contracts.TypedComponent +{ + public class DockerImageComponent : TypedComponent + { + private DockerImageComponent() + { + /* Reserved for deserialization */ + } + + public DockerImageComponent(string hash, string name = null, string tag = null) + { + Digest = ValidateRequiredInput(hash, nameof(Digest), nameof(ComponentType.DockerImage)); + Name = name; + Tag = tag; + } + + public string Name { get; set; } + + public string Digest { get; set; } + + public string Tag { get; set; } + + public override ComponentType Type => ComponentType.DockerImage; + + public override string Id => $"{Name} {Tag} {Digest}"; + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/GitComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/GitComponent.cs new file mode 100644 index 000000000..0da30ec3e --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/GitComponent.cs @@ -0,0 +1,34 @@ +using System; + +namespace Microsoft.ComponentDetection.Contracts.TypedComponent +{ + public class GitComponent : TypedComponent + { + private GitComponent() + { + /* Reserved for deserialization */ + } + + public GitComponent(Uri repositoryUrl, string commitHash) + { + RepositoryUrl = ValidateRequiredInput(repositoryUrl, nameof(RepositoryUrl), nameof(ComponentType.Git)); + CommitHash = ValidateRequiredInput(commitHash, nameof(CommitHash), nameof(ComponentType.Git)); + } + + public GitComponent(Uri repositoryUrl, string commitHash, string tag) + : this(repositoryUrl, commitHash) + { + Tag = tag; + } + + public Uri RepositoryUrl { get; set; } + + public string CommitHash { get; set; } + + public string Tag { get; set; } + + public override ComponentType Type => ComponentType.Git; + + public override string Id => $"{RepositoryUrl} : {CommitHash} - {Type}"; + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/GoComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/GoComponent.cs new file mode 100644 index 000000000..d6bb745ba --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/GoComponent.cs @@ -0,0 +1,64 @@ +using System; +using PackageUrl; + +namespace Microsoft.ComponentDetection.Contracts.TypedComponent +{ + public class GoComponent : TypedComponent, IEquatable + { + private GoComponent() + { + /* Reserved for deserialization */ + } + + public GoComponent(string name, string version) + { + Name = ValidateRequiredInput(name, nameof(Name), nameof(ComponentType.Go)); + Version = ValidateRequiredInput(version, nameof(Version), nameof(ComponentType.Go)); + Hash = string.Empty; + } + + public GoComponent(string name, string version, string hash) + { + Name = ValidateRequiredInput(name, nameof(Name), nameof(ComponentType.Go)); + Version = ValidateRequiredInput(version, nameof(Version), nameof(ComponentType.Go)); + Hash = ValidateRequiredInput(hash, nameof(Hash), nameof(ComponentType.Go)); + } + + public string Name { get; set; } + + public string Version { get; set; } + + public string Hash { get; set; } + + public override ComponentType Type => ComponentType.Go; + + public override string Id => $"{Name} {Version} - {Type}"; + + public override bool Equals(object other) + { + GoComponent otherComponent = other as GoComponent; + return otherComponent != null && Equals(otherComponent); + } + + public bool Equals(GoComponent other) + { + if (other == null) + { + return false; + } + + return Name == other.Name && + Version == other.Version && + Hash == other.Hash; + } + + public override int GetHashCode() + { + return Name.GetHashCode() ^ Version.GetHashCode() ^ Hash.GetHashCode(); + } + + // Commit should be used in place of version when available + // https://github.com/package-url/purl-spec/blame/180c46d266c45aa2bd81a2038af3f78e87bb4a25/README.rst#L610 + public override PackageURL PackageUrl => new PackageURL("golang", null, Name, string.IsNullOrWhiteSpace(Hash) ? Version : Hash, null, null); + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/LinuxComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/LinuxComponent.cs new file mode 100644 index 000000000..0b03bb5a3 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/LinuxComponent.cs @@ -0,0 +1,81 @@ +using PackageUrl; + +namespace Microsoft.ComponentDetection.Contracts.TypedComponent +{ + public class LinuxComponent : TypedComponent + { + private LinuxComponent() + { + /* Reserved for deserialization */ + } + + public LinuxComponent(string distribution, string release, string name, string version) + { + Distribution = ValidateRequiredInput(distribution, nameof(Distribution), nameof(ComponentType.Linux)); + Release = ValidateRequiredInput(release, nameof(Release), nameof(ComponentType.Linux)); + Name = ValidateRequiredInput(name, nameof(Name), nameof(ComponentType.Linux)); + Version = ValidateRequiredInput(version, nameof(Version), nameof(ComponentType.Linux)); + } + + public string Distribution { get; set; } + + public string Release { get; set; } + + public string Name { get; set; } + + public string Version { get; set; } + + public override ComponentType Type => ComponentType.Linux; + + public override string Id => $"{Distribution} {Release} {Name} {Version} - {Type}"; + + public override PackageURL PackageUrl + { + get + { + string packageType = null; + + if (IsUbuntu() || IsDebian()) + { + packageType = "deb"; + } + else if (IsCentOS() || IsFedora() || IsRHEL()) + { + packageType = "rpm"; + } + + if (packageType != null) + { + return new PackageURL(packageType, Distribution, Name, Version, null, null); + } + + return null; + } + } + + private bool IsUbuntu() + { + return Distribution.ToLowerInvariant() == "ubuntu"; + } + + private bool IsDebian() + { + return Distribution.ToLowerInvariant() == "debian"; + } + + private bool IsCentOS() + { + return Distribution.ToLowerInvariant() == "centos"; + } + + private bool IsFedora() + { + return Distribution.ToLowerInvariant() == "fedora"; + } + + private bool IsRHEL() + { + return Distribution.ToLowerInvariant() == "red hat enterprise linux"; + } + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/MavenComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/MavenComponent.cs new file mode 100644 index 000000000..b418ead37 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/MavenComponent.cs @@ -0,0 +1,31 @@ +using PackageUrl; + +namespace Microsoft.ComponentDetection.Contracts.TypedComponent +{ + public class MavenComponent : TypedComponent + { + private MavenComponent() + { + /* Reserved for deserialization */ + } + + public MavenComponent(string groupId, string artifactId, string version) + { + GroupId = ValidateRequiredInput(groupId, nameof(GroupId), nameof(ComponentType.Maven)); + ArtifactId = ValidateRequiredInput(artifactId, nameof(ArtifactId), nameof(ComponentType.Maven)); + Version = ValidateRequiredInput(version, nameof(Version), nameof(ComponentType.Maven)); + } + + public string GroupId { get; set; } + + public string ArtifactId { get; set; } + + public string Version { get; set; } + + public override ComponentType Type => ComponentType.Maven; + + public override string Id => $"{GroupId} {ArtifactId} {Version} - {Type}"; + + public override PackageURL PackageUrl => new PackageURL("maven", GroupId, ArtifactId, Version, null, null); + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/NpmComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/NpmComponent.cs new file mode 100644 index 000000000..6a83342ea --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/NpmComponent.cs @@ -0,0 +1,31 @@ +using PackageUrl; + +namespace Microsoft.ComponentDetection.Contracts.TypedComponent +{ + public class NpmComponent : TypedComponent + { + private NpmComponent() + { + /* Reserved for deserialization */ + } + + public NpmComponent(string name, string version, string hash = null) + { + Name = ValidateRequiredInput(name, nameof(Name), nameof(ComponentType.Npm)); + Version = ValidateRequiredInput(version, nameof(Version), nameof(ComponentType.Npm)); + Hash = hash; // Not required; only found in package-lock.json, not package.json + } + + public string Name { get; set; } + + public string Version { get; set; } + + public string Hash { get; set; } + + public override ComponentType Type => ComponentType.Npm; + + public override string Id => $"{Name} {Version} - {Type}"; + + public override PackageURL PackageUrl => new PackageURL("npm", null, Name, Version, null, null); + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/NugetComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/NugetComponent.cs new file mode 100644 index 000000000..65e2d87db --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/NugetComponent.cs @@ -0,0 +1,28 @@ +using PackageUrl; + +namespace Microsoft.ComponentDetection.Contracts.TypedComponent +{ + public class NuGetComponent : TypedComponent + { + private NuGetComponent() + { + /* Reserved for deserialization */ + } + + public NuGetComponent(string name, string version) + { + Name = ValidateRequiredInput(name, nameof(Name), nameof(ComponentType.NuGet)); + Version = ValidateRequiredInput(version, nameof(Version), nameof(ComponentType.NuGet)); + } + + public string Name { get; set; } + + public string Version { get; set; } + + public override ComponentType Type => ComponentType.NuGet; + + public override string Id => $"{Name} {Version} - {Type}"; + + public override PackageURL PackageUrl => new PackageURL("nuget", null, Name, Version, null, null); + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/OtherComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/OtherComponent.cs new file mode 100644 index 000000000..063552227 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/OtherComponent.cs @@ -0,0 +1,32 @@ +using System; + +namespace Microsoft.ComponentDetection.Contracts.TypedComponent +{ + public class OtherComponent : TypedComponent + { + private OtherComponent() + { + /* Reserved for deserialization */ + } + + public OtherComponent(string name, string version, Uri downloadUrl, string hash) + { + Name = ValidateRequiredInput(name, nameof(Name), nameof(ComponentType.Other)); + Version = ValidateRequiredInput(version, nameof(Version), nameof(ComponentType.Other)); + DownloadUrl = ValidateRequiredInput(downloadUrl, nameof(DownloadUrl), nameof(ComponentType.Other)); + Hash = hash; + } + + public string Name { get; set; } + + public string Version { get; set; } + + public Uri DownloadUrl { get; set; } + + public string Hash { get; set; } + + public override ComponentType Type => ComponentType.Other; + + public override string Id => $"{Name} {Version} {DownloadUrl} - {Type}"; + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/PipComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/PipComponent.cs new file mode 100644 index 000000000..b11792f17 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/PipComponent.cs @@ -0,0 +1,28 @@ +using PackageUrl; + +namespace Microsoft.ComponentDetection.Contracts.TypedComponent +{ + public class PipComponent : TypedComponent + { + private PipComponent() + { + /* Reserved for deserialization */ + } + + public PipComponent(string name, string version) + { + Name = ValidateRequiredInput(name, nameof(Name), nameof(ComponentType.Pip)); + Version = ValidateRequiredInput(version, nameof(Version), nameof(ComponentType.Pip)); + } + + public string Name { get; set; } + + public string Version { get; set; } + + public override ComponentType Type => ComponentType.Pip; + + public override string Id => $"{Name} {Version} - {Type}".ToLowerInvariant(); + + public override PackageURL PackageUrl => new PackageURL("pypi", null, Name, Version, null, null); + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/PodComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/PodComponent.cs new file mode 100644 index 000000000..a657fdf4d --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/PodComponent.cs @@ -0,0 +1,44 @@ +using PackageUrl; +using System.Collections.Generic; + +namespace Microsoft.ComponentDetection.Contracts.TypedComponent +{ + public class PodComponent : TypedComponent + { + private PodComponent() + { + /* Reserved for deserialization */ + } + + public PodComponent(string name, string version, string specRepo = "") + { + Name = ValidateRequiredInput(name, nameof(Name), nameof(ComponentType.Pod)); + Version = ValidateRequiredInput(version, nameof(Version), nameof(ComponentType.Pod)); + SpecRepo = specRepo; + } + + public string Name { get; set; } + + public string Version { get; set; } + + public string SpecRepo { get; set; } + + public override ComponentType Type => ComponentType.Pod; + + public override string Id => $"{Name} {Version} - {Type}"; + + public override PackageURL PackageUrl + { + get + { + var qualifiers = new SortedDictionary(); + if (!string.IsNullOrWhiteSpace(SpecRepo)) + { + qualifiers.Add("repository_url", SpecRepo); + } + + return new ("cocoapods", null, Name, Version, qualifiers, null); + } + } + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/RubyGemsComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/RubyGemsComponent.cs new file mode 100644 index 000000000..2b6779a67 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/RubyGemsComponent.cs @@ -0,0 +1,31 @@ +using PackageUrl; + +namespace Microsoft.ComponentDetection.Contracts.TypedComponent +{ + public class RubyGemsComponent : TypedComponent + { + private RubyGemsComponent() + { + /* Reserved for deserialization */ + } + + public RubyGemsComponent(string name, string version, string source = "") + { + Name = ValidateRequiredInput(name, nameof(Name), nameof(ComponentType.RubyGems)); + Version = ValidateRequiredInput(version, nameof(Version), nameof(ComponentType.RubyGems)); + Source = source; + } + + public string Name { get; set; } + + public string Version { get; set; } + + public string Source { get; set; } + + public override ComponentType Type => ComponentType.RubyGems; + + public override string Id => $"{Name} {Version} - {Type}"; + + public override PackageURL PackageUrl => new PackageURL("gem", null, Name, Version, null, null); + } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/TypedComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/TypedComponent.cs new file mode 100644 index 000000000..0bf401458 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/TypedComponent.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.ComponentDetection.Contracts.BcdeModels; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; +using PackageUrl; + +namespace Microsoft.ComponentDetection.Contracts.TypedComponent +{ + [JsonObject(MemberSerialization.OptOut, NamingStrategyType = typeof(CamelCaseNamingStrategy))] + [JsonConverter(typeof(TypedComponentConverter))] + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public abstract class TypedComponent + { + internal TypedComponent() + { + // Reserved for deserialization. + } + + /// Gets the type of the component, must be well known. + [JsonConverter(typeof(StringEnumConverter))] + public abstract ComponentType Type { get; } + + public abstract string Id { get; } + + public virtual PackageURL PackageUrl { get; } + + [JsonIgnore] + internal string DebuggerDisplay => $"{Id}"; + + protected string ValidateRequiredInput(string input, string fieldName, string componentType) + { + return string.IsNullOrWhiteSpace(input) + ? throw new ArgumentNullException(fieldName, NullPropertyExceptionMessage(fieldName, componentType)) + : input; + } + + protected T ValidateRequiredInput(T input, string fieldName, string componentType) + { + // Null coalescing for generic types is not available until C# 8 + return EqualityComparer.Default.Equals(input, default(T)) ? throw new ArgumentNullException(fieldName, NullPropertyExceptionMessage(fieldName, componentType)) : input; + } + + protected string NullPropertyExceptionMessage(string propertyName, string componentType) + { + return $"Property {propertyName} of component type {componentType} is required"; + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/IComponentGovernanceOwnedDetectors.cs b/src/Microsoft.ComponentDetection.Detectors/IComponentGovernanceOwnedDetectors.cs new file mode 100644 index 000000000..206d4406e --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/IComponentGovernanceOwnedDetectors.cs @@ -0,0 +1,9 @@ +namespace Microsoft.ComponentDetection.Detectors +{ + /// + /// This type is used to find this assembly. + /// + public interface IComponentGovernanceOwnedDetectors + { + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/Microsoft.ComponentDetection.Detectors.csproj b/src/Microsoft.ComponentDetection.Detectors/Microsoft.ComponentDetection.Detectors.csproj new file mode 100644 index 000000000..911be7eea --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/Microsoft.ComponentDetection.Detectors.csproj @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Never + + + Never + + + + \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Detectors/cocoapods/PodComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/cocoapods/PodComponentDetector.cs new file mode 100644 index 000000000..81edabc2e --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/cocoapods/PodComponentDetector.cs @@ -0,0 +1,402 @@ +using System; +using System.Collections.Generic; +using System.Composition; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.ComponentDetection.Common; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.Internal; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace Microsoft.ComponentDetection.Detectors.CocoaPods +{ + [Export(typeof(IComponentDetector))] + public class PodComponentDetector : FileComponentDetector + { + public override string Id { get; } = "CocoaPods"; + + public override IEnumerable Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.CocoaPods) }; + + public override IList SearchPatterns { get; } = new List { "Podfile.lock" }; + + public override IEnumerable SupportedComponentTypes { get; } = new[] { ComponentType.Pod, ComponentType.Git }; + + public override int Version { get; } = 1; + + private class Pod : IYamlConvertible + { + public string Name { get; set; } + + public string Version { get; set; } + + public IList Dependencies { get; set; } + + public string Podspec => Name.Split('/', 2)[0]; + + public bool IsSubspec => Name != Podspec; + + public void Read(IParser parser, Type expectedType, ObjectDeserializer nestedObjectDeserializer) + { + var hasDependencies = parser.Accept(out _); + if (hasDependencies) + { + parser.Consume(); + } + + var podInfo = parser.Consume(); + var components = podInfo.Value.Split(new char[] { '(', ')' }, StringSplitOptions.RemoveEmptyEntries); + Name = components[0].Trim(); + Version = components[1].Trim(); + + if (hasDependencies) + { + Dependencies = (IList)nestedObjectDeserializer(typeof(IList)); + + parser.Consume(); + } + else + { + Dependencies = Array.Empty(); + } + } + + public void Write(IEmitter emitter, ObjectSerializer nestedObjectSerializer) + { + throw new NotImplementedException(); + } + } + + private class PodDependency : IYamlConvertible + { + public string PodName { get; set; } + + public string PodVersion { get; set; } + + public string Podspec => PodName.Split('/', 2)[0]; + + public bool IsSubspec => PodName != Podspec; + + public void Read(IParser parser, Type expectedType, ObjectDeserializer nestedObjectDeserializer) + { + var scalar = parser.Consume(); + var components = scalar.Value.Split(new char[] { '(', ')' }, StringSplitOptions.RemoveEmptyEntries); + PodName = components[0].Trim(); + PodVersion = components.Length > 1 ? components[1].Trim() : null; + } + + public void Write(IEmitter emitter, ObjectSerializer nestedObjectSerializer) + { + throw new NotImplementedException(); + } + } + + private class PodfileLock + { + [YamlMember(Alias = "PODFILE CHECKSUM", ApplyNamingConventions = false)] + public string Checksum { get; set; } + + [YamlMember(Alias = "COCOAPODS", ApplyNamingConventions = false)] + public string CocoapodsVersion { get; set; } + + [YamlMember(Alias = "DEPENDENCIES", ApplyNamingConventions = false)] + public IList Dependencies { get; set; } + + [YamlMember(Alias = "SPEC REPOS", ApplyNamingConventions = false)] + public IDictionary> PodspecRepositories { get; set; } + + [YamlMember(Alias = "SPEC CHECKSUMS", ApplyNamingConventions = false)] + public IDictionary PodspecChecksums { get; set; } + + [YamlMember(Alias = "EXTERNAL SOURCES", ApplyNamingConventions = false)] + public IDictionary> ExternalSources { get; set; } + + [YamlMember(Alias = "CHECKOUT OPTIONS", ApplyNamingConventions = false)] + public IDictionary> CheckoutOptions { get; set; } + + [YamlMember(Alias = "PODS", ApplyNamingConventions = false)] + public IList Pods { get; set; } + + public PodfileLock() + { + Dependencies = Array.Empty(); + PodspecRepositories = new Dictionary>(); + PodspecChecksums = new Dictionary(); + ExternalSources = new Dictionary>(); + CheckoutOptions = new Dictionary>(); + Pods = Array.Empty(); + } + + public string GetSpecRepositoryOfSpec(string specName) + { + foreach (var repository in PodspecRepositories) + { + if (repository.Value.Contains(specName)) + { + // CocoaPods specs are stored in a git repo but depending on settings/CocoaPods version + // the repo is shown differently in the Podfile.lock + switch (repository.Key.ToLowerInvariant()) + { + case "trunk": + case "https://github.com/cocoapods/specs.git": + return "trunk"; + + default: + return repository.Key; + } + } + } + + return null; + } + } + + protected override async Task OnFileFound(ProcessRequest processRequest, IDictionary detectorArgs) + { + var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder; + var file = processRequest.ComponentStream; + + Logger.LogVerbose($"Found {file.Pattern}: {file.Location}"); + + try + { + var podfileLock = await ParsePodfileLock(file); + + ProcessPodfileLock(singleFileComponentRecorder, podfileLock); + } + catch (Exception e) + { + Logger.LogFailedReadingFile(file.Location, e); + } + } + + private static async Task ParsePodfileLock(IComponentStream file) + { + var fileContent = await new StreamReader(file.Stream).ReadToEndAsync(); + var input = new StringReader(fileContent); + var deserializer = new DeserializerBuilder() + .IgnoreUnmatchedProperties() + .Build(); + + return deserializer.Deserialize(input); + } + + private void ProcessPodfileLock( + ISingleFileComponentRecorder singleFileComponentRecorder, + PodfileLock podfileLock) + { + if (podfileLock.Pods.Count == 0) + { + return; + } + + // Create a set of root podspecs + var rootPodspecs = new HashSet(podfileLock.Dependencies.Select(p => p.Podspec)); + + var podKeyDetectedComponents = ReadPodfileLock(podfileLock); + var rootComponents = new Dictionary(); + var nonRootComponents = new Dictionary(); + + foreach (var (pod, key, detectedComponent) in podKeyDetectedComponents) + { + // Check if the pod is a root component and add it to the list of discovered components + if (rootPodspecs.Contains(pod.Podspec)) + { + if (nonRootComponents.TryGetValue(key, out DetectedComponent existingComponent)) + { + rootComponents.TryAdd(key, existingComponent); + nonRootComponents.Remove(key); + } + else + { + rootComponents.TryAdd(key, detectedComponent); + } + } + else if (!rootComponents.ContainsKey(key)) + { + nonRootComponents.TryAdd(key, detectedComponent); + } + else + { + // Ignore current element, it will be recovered later via a root or non-root! + } + } + + var dependenciesMap = new Dictionary>(); + + // Map a pod ID to the list of the pod's dependencies + var podDependencies = new Dictionary>(); + + // Map a podspec to the pod ID + var podSpecs = new Dictionary(); + + foreach (var pod in podfileLock.Pods) + { + // Find the spec repository URL for this pod + var specRepository = podfileLock.GetSpecRepositoryOfSpec(pod.Podspec) ?? string.Empty; + + // Check if the Podspec comes from a git repository or not + TypedComponent typedComponent; + string key; + if (podfileLock.CheckoutOptions.TryGetValue(pod.Podspec, out IDictionary checkoutOptions) + && checkoutOptions.TryGetValue(":git", out string gitOption) + && checkoutOptions.TryGetValue(":commit", out string commitOption)) + { + // Create the Git component + typedComponent = new GitComponent(new Uri(gitOption), commitOption); + key = $"{commitOption}@{gitOption}"; + } + else + { + // Create the Pod component + typedComponent = new PodComponent(pod.Podspec, pod.Version, specRepository); + key = $"{pod.Podspec}:{pod.Version}@{specRepository}"; + } + + var detectedComponent = new DetectedComponent(typedComponent) + { + DependencyRoots = new HashSet(new ComponentComparer()), + }; + + // Check if the pod is a root component and add it to the list of discovered components + if (rootPodspecs.Contains(pod.Podspec)) + { + if (nonRootComponents.TryGetValue(key, out DetectedComponent existingComponent)) + { + rootComponents.TryAdd(key, existingComponent); + nonRootComponents.Remove(key); + } + else + { + rootComponents.TryAdd(key, detectedComponent); + } + } + else if (!rootComponents.ContainsKey(key)) + { + nonRootComponents.TryAdd(key, detectedComponent); + } + + // Update the podspec map + podSpecs.TryAdd(pod.Podspec, key); + + // Update the pod dependencies map + if (podDependencies.TryGetValue(key, out List dependencies)) + { + dependencies.AddRange(pod.Dependencies); + } + else + { + podDependencies.TryAdd(key, new List(pod.Dependencies)); + } + } + + foreach (var pod in podDependencies) + { + // Add all the dependencies to the map, without duplicates + dependenciesMap.TryAdd(pod.Key, new HashSet()); + + foreach (var dependency in pod.Value) + { + var dependencyKey = podSpecs[dependency.Podspec]; + if (dependencyKey != pod.Key) + { + dependenciesMap[pod.Key].Add(podSpecs[dependency.Podspec]); + } + } + } + + foreach (var rootComponent in rootComponents) + { + singleFileComponentRecorder.RegisterUsage( + rootComponent.Value, + isExplicitReferencedDependency: true); + + // Check if this component has any dependencies + if (!dependenciesMap.ContainsKey(rootComponent.Key)) + { + continue; + } + + // Traverse the dependencies graph for this component and stop if there is a cycle + // or if we find another root component + var dependencies = new Queue(dependenciesMap[rootComponent.Key]); + while (dependencies.Count > 0) + { + var dependency = dependencies.Dequeue(); + + if (rootComponents.TryGetValue(dependency, out DetectedComponent detectedRootComponent)) + { + // Found another root component + singleFileComponentRecorder.RegisterUsage( + detectedRootComponent, + isExplicitReferencedDependency: true, + parentComponentId: rootComponent.Value.Component.Id); + } + else if (nonRootComponents.TryGetValue(dependency, out DetectedComponent detectedComponent)) + { + singleFileComponentRecorder.RegisterUsage( + detectedComponent, + isExplicitReferencedDependency: true, + parentComponentId: rootComponent.Value.Component.Id); + + // Add the new dependecies to the queue + if (dependenciesMap.TryGetValue(dependency, out HashSet newDependencies)) + { + newDependencies.ToList().ForEach(dependencies.Enqueue); + } + else + { + // Do nothing! + } + } + else + { + // Do nothing! + } + } + } + + foreach (var component in nonRootComponents) + { + singleFileComponentRecorder.RegisterUsage( + component.Value, + isExplicitReferencedDependency: true); + } + } + + private static (Pod pod, string key, DetectedComponent detectedComponent)[] ReadPodfileLock(PodfileLock podfileLock) + { + return podfileLock.Pods.Select(pod => + { + // Find the spec repository URL for this pod + var specRepository = podfileLock.GetSpecRepositoryOfSpec(pod.Podspec) ?? string.Empty; + + // Check if the Podspec comes from a git repository or not + TypedComponent typedComponent; + string key; + if (podfileLock.CheckoutOptions.TryGetValue(pod.Podspec, out IDictionary checkoutOptions) + && checkoutOptions.TryGetValue(":git", out string gitOption) + && checkoutOptions.TryGetValue(":commit", out string commitOption)) + { + // Create the Git component + typedComponent = new GitComponent(new Uri(gitOption), commitOption); + key = $"{commitOption}@{gitOption}"; + } + else + { + // Create the Pod component + typedComponent = new PodComponent(pod.Podspec, pod.Version, specRepository); + key = $"{pod.Podspec}:{pod.Version}@{specRepository}"; + } + + var detectedComponent = new DetectedComponent(typedComponent); + + return (pod, key, detectedComponent); + }) + .ToArray(); + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/go/GoComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/go/GoComponentDetector.cs new file mode 100644 index 000000000..2b323db66 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/go/GoComponentDetector.cs @@ -0,0 +1,227 @@ +using System; +using System.Collections.Generic; +using System.Composition; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.ComponentDetection.Common.Telemetry.Records; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.Internal; +using Microsoft.ComponentDetection.Contracts.TypedComponent; + +namespace Microsoft.ComponentDetection.Detectors.Go +{ + [Export(typeof(IComponentDetector))] + public class GoComponentDetector : FileComponentDetector + { + [Import] + public ICommandLineInvocationService CommandLineInvocationService { get; set; } + + private static readonly Regex GoSumRegex = new Regex( + @"(?.*)\s+(?.*?)(/go\.mod)?\s+(?.*)", + RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase); + + public override string Id { get; } = "Go"; + + public override IEnumerable Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.GoMod) }; + + public override IList SearchPatterns { get; } = new List { "go.mod", "go.sum" }; + + public override IEnumerable SupportedComponentTypes { get; } = new[] { ComponentType.Go }; + + public override int Version => 2; + + private HashSet projectRoots = new HashSet(); + + protected override Task OnFileFound(ProcessRequest processRequest, IDictionary detectorArgs) + { + var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder; + var file = processRequest.ComponentStream; + + var fileExtension = Path.GetExtension(file.Location).ToLowerInvariant(); + switch (fileExtension) + { + case ".mod": + { + Logger.LogVerbose("Found Go.mod: " + file.Location); + ParseGoModFile(singleFileComponentRecorder, file); + break; + } + + case ".sum": + { + Logger.LogVerbose("Found Go.sum: " + file.Location); + ParseGoSumFile(singleFileComponentRecorder, file); + break; + } + + default: + { + throw new Exception("Unexpected file type detected in go detector"); + } + } + + return Task.CompletedTask; + } + + private async Task UseGoCliToScan(string location, ISingleFileComponentRecorder singleFileComponentRecorder) + { + using var record = new GoGraphTelemetryRecord(); + record.WasGraphSuccessful = false; + + var projectRootDirectory = Directory.GetParent(location); + record.ProjectRoot = projectRootDirectory.FullName; + + var isGoAvailable = await CommandLineInvocationService.CanCommandBeLocated("go", null, workingDirectory: projectRootDirectory, new List { "version" }.ToArray()); + record.IsGoAvailable = isGoAvailable; + + if (!isGoAvailable) + { + return false; + } + + var generateGraphProcess = await CommandLineInvocationService.ExecuteCommand("go", null, workingDirectory: projectRootDirectory, new List { "mod", "graph" }.ToArray()); + if (generateGraphProcess.ExitCode == 0) + { + PopulateDependencyGraph(generateGraphProcess.StdOut, singleFileComponentRecorder); + record.WasGraphSuccessful = true; + } + + return record.WasGraphSuccessful; + } + + private void ParseGoModFile( + ISingleFileComponentRecorder singleFileComponentRecorder, + IComponentStream file) + { + using var reader = new StreamReader(file.Stream); + + string line = reader.ReadLine(); + while (line != null && !line.StartsWith("require (")) + { + line = reader.ReadLine(); + } + + // Stopping at the first ) restrict the detection to only the require section. + while ((line = reader.ReadLine()) != null && !line.EndsWith(")")) + { + if (TryToCreateGoComponentFromModLine(line, out var goComponent)) + { + singleFileComponentRecorder.RegisterUsage(new DetectedComponent(goComponent)); + } + else + { + Logger.LogWarning($"Line could not be parsed for component [{line.Trim()}]"); + } + } + } + + private bool TryToCreateGoComponentFromModLine(string line, out GoComponent goComponent) + { + var lineComponents = Regex.Split(line.Trim(), @"\s+"); + + if (lineComponents.Length < 2) + { + goComponent = null; + return false; + } + + var name = lineComponents[0]; + var version = lineComponents[1]; + goComponent = new GoComponent(name, version); + + return true; + } + + //For more information about the format of the go.sum file + //visit https://golang.org/cmd/go/#hdr-Module_authentication_using_go_sum + private void ParseGoSumFile( + ISingleFileComponentRecorder singleFileComponentRecorder, + IComponentStream file) + { + using var reader = new StreamReader(file.Stream); + + string line; + while ((line = reader.ReadLine()) != null) + { + if (TryToCreateGoComponentFromSumLine(line, out var goComponent)) + { + singleFileComponentRecorder.RegisterUsage(new DetectedComponent(goComponent)); + } + else + { + Logger.LogWarning($"Line could not be parsed for component [{line.Trim()}]"); + } + } + } + + private bool TryToCreateGoComponentFromSumLine(string line, out GoComponent goComponent) + { + Match m = GoSumRegex.Match(line); + if (m.Success) + { + goComponent = new GoComponent(m.Groups["name"].Value, m.Groups["version"].Value, m.Groups["hash"].Value); + return true; + } + + goComponent = null; + return false; + } + + private void PopulateDependencyGraph(string goGraphOutput, ISingleFileComponentRecorder singleFileComponentRecorder) + { + // Yes, go always returns \n even on Windows + var graphRelationships = goGraphOutput.Split('\n'); + + foreach (var relationship in graphRelationships) + { + var components = relationship.Split(' '); + if (components.Length != 2) + { + Logger.LogWarning("Unexpected output from go mod graph:"); + Logger.LogWarning(relationship); + continue; + } + + GoComponent parentComponent; + GoComponent childComponent; + + var parentPart = components[0]; + var childPart = components[1]; + + var isParentParsed = TryCreateGoComponentFromRelationshipPart(parentPart, out parentComponent); + var isChildParsed = TryCreateGoComponentFromRelationshipPart(childPart, out childComponent); + + // If the parent component doesn't have a version, it means it's one of the 'main' modules + // The imports of the main modules are explicitly referenced + if (!isParentParsed && isChildParsed) + { + singleFileComponentRecorder.RegisterUsage(new DetectedComponent(childComponent), isExplicitReferencedDependency: true); + } + else if (isParentParsed && isChildParsed) + { + // Go output guarantees that all parents will be output before children + singleFileComponentRecorder.RegisterUsage(new DetectedComponent(childComponent), parentComponentId: parentComponent.Id); + } + else + { + Logger.LogWarning($"Failed to parse components from relationship string {relationship}"); + } + } + } + + private bool TryCreateGoComponentFromRelationshipPart(string relationship, out GoComponent goComponent) + { + var componentParts = relationship.Split('@'); + if (componentParts.Length != 2) + { + goComponent = null; + return false; + } + + goComponent = new GoComponent(componentParts[0], componentParts[1]); + return true; + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/gradle/GradleComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/gradle/GradleComponentDetector.cs new file mode 100644 index 000000000..873f4eba1 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/gradle/GradleComponentDetector.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Composition; +using System.IO; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.Internal; +using Microsoft.ComponentDetection.Contracts.TypedComponent; + +namespace Microsoft.ComponentDetection.Detectors.Gradle +{ + [Export(typeof(IComponentDetector))] + public class GradleComponentDetector : FileComponentDetector, IComponentDetector + { + public override string Id { get; } = "Gradle"; + + public override IEnumerable Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.Maven) }; + + public override IList SearchPatterns { get; } = new List { "*.lockfile" }; + + public override IEnumerable SupportedComponentTypes { get; } = new[] { ComponentType.Maven }; + + public override int Version { get; } = 2; + + /// + private static readonly Regex StartsWithLetterRegex = new Regex("^[A-Za-z]", RegexOptions.Compiled); + + protected override Task OnFileFound(ProcessRequest processRequest, IDictionary detectorArgs) + { + var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder; + var file = processRequest.ComponentStream; + + Logger.LogVerbose("Found Gradle lockfile: " + file.Location); + ParseLockfile(singleFileComponentRecorder, file); + + return Task.CompletedTask; + } + + private void ParseLockfile(ISingleFileComponentRecorder singleFileComponentRecorder, IComponentStream file) + { + string text; + using (var reader = new StreamReader(file.Stream)) + { + text = reader.ReadToEnd(); + } + + var lines = new List(text.Split("\n")); + + while (lines.Count > 0) + { + var line = lines[0].Trim(); + lines.RemoveAt(0); + + if (!StartsWithLetter(line)) + { + continue; + } + + if (line.Split(":").Length == 3) + { + var detectedMavenComponent = new DetectedComponent(CreateMavenComponentFromFileLine(line)); + singleFileComponentRecorder.RegisterUsage(detectedMavenComponent); + } + } + } + + private MavenComponent CreateMavenComponentFromFileLine(string line) + { + var equalsSeparatorIndex = line.IndexOf('='); + var isSingleLockfilePerProjectFormat = equalsSeparatorIndex != -1; + var componentDescriptor = isSingleLockfilePerProjectFormat ? line.Substring(0, equalsSeparatorIndex) : line; + var splits = componentDescriptor.Trim().Split(":"); + var groupId = splits[0]; + var artifactId = splits[1]; + var version = splits[2]; + + return new MavenComponent(groupId, artifactId, version); + } + + private bool StartsWithLetter(string input) => StartsWithLetterRegex.IsMatch(input); + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/ivy/IvyDetector.cs b/src/Microsoft.ComponentDetection.Detectors/ivy/IvyDetector.cs new file mode 100644 index 000000000..cd9136a57 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/ivy/IvyDetector.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using System.Composition; +using System.IO; +using System.Linq; +using System.Reactive.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.Internal; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Newtonsoft.Json.Linq; + +namespace Microsoft.ComponentDetection.Detectors.Ivy +{ + /// + /// Detector for Maven components declared in ivy.xml files for Java projects that are built using Apache Ant + /// and Apache Ivy. + /// + /// + /// Ivy can use artifact repositories that are in the traditional Ivy layout or m2compatible repositories. The + /// m2compatible repositories use the Maven three-coordinate system GAV (group, artifact, version) to identify + /// components, which corresponds directly to Ivy coordinates (org, name, rev). The project has its own + /// assigned organisation, and any dependencies with the same organisation are taken as first party dependencies + /// and ignored by the detector. + /// + /// This detector relies on Apache Ant being available in the PATH, because it needs to run Ivy's resolver to + /// find transitive dependencies (and these in turn require a JDK installed). The ivy.xml file and (if it exists) + /// ivysettings.xml file from the same directory are copied to a temporary directory, along with a synthetic + /// build.xml and the Java source code of a custom Ant task. The detector then invokes Ant in this temporary + /// directory to resolve the dependencies and write out a file for this detector to parse. Note that for this + /// to work, it requires ivy.xml and ivysettings.xml to be self-contained: if they rely on any properties defined + /// in the project's build.xml, or if they use any file inclusion mechanism, it will fail. + /// + /// The file written out by the custom Ant task is a simple JSON file representing a series of calls to be made to + /// the method. + /// + [Export(typeof(IComponentDetector))] + public class IvyDetector : FileComponentDetector, IExperimentalDetector + { + public override string Id => "Ivy"; + + public override IList SearchPatterns => new List { "ivy.xml" }; + + public override IEnumerable SupportedComponentTypes => new[] { ComponentType.Maven }; + + public override int Version => 2; + + public override IEnumerable Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.Maven) }; + + internal const string PrimaryCommand = "ant.bat"; + + internal static readonly string[] AdditionalValidCommands = { "ant" }; + + internal const string AntVersionArgument = "-version"; + + [Import] + public ICommandLineInvocationService CommandLineInvocationService { get; set; } + + protected override async Task> OnPrepareDetection(IObservable processRequests, IDictionary detectorArgs) + { + if (await IsAntLocallyAvailableAsync()) + { + return processRequests; + } + + Logger.LogVerbose("Skipping Ivy detection as ant is not available in the local PATH."); + return Enumerable.Empty().ToObservable(); + } + + protected override async Task OnFileFound(ProcessRequest processRequest, IDictionary detectorArgs) + { + var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder; + var ivyXmlFile = processRequest.ComponentStream; + + var dirName = Path.GetDirectoryName(ivyXmlFile.Location); + var ivySettingsFilePath = Path.Combine(dirName, "ivysettings.xml"); + if (File.Exists(ivyXmlFile.Location)) + { + if (File.Exists(ivySettingsFilePath)) + { + Logger.LogInfo($"Processing {ivyXmlFile.Location} and ivysettings.xml."); + await ProcessIvyAndIvySettingsFilesAsync(singleFileComponentRecorder, ivyXmlFile.Location, ivySettingsFilePath); + } + else + { + Logger.LogInfo($"Processing {ivyXmlFile.Location}."); + await ProcessIvyAndIvySettingsFilesAsync(singleFileComponentRecorder, ivyXmlFile.Location, null); + } + } + else + { + Logger.LogError($"File {ivyXmlFile.Location} passed to OnFileFound, but does not exist!"); + } + } + + private async Task ProcessIvyAndIvySettingsFilesAsync( + ISingleFileComponentRecorder singleFileComponentRecorder, + string ivyXmlFile, + string ivySettingsXmlFile) + { + try + { + string workingDirectory = Path.Combine(Path.GetTempPath(), "ComponentDetection_Ivy"); + Logger.LogVerbose($"Preparing temporary Ivy project in {workingDirectory}"); + if (Directory.Exists(workingDirectory)) + { + Directory.Delete(workingDirectory, recursive: true); + } + + InitTemporaryAntProject(workingDirectory, ivyXmlFile, ivySettingsXmlFile); + if (await RunAntToDetectDependenciesAsync(workingDirectory)) + { + string instructionsFile = Path.Combine(workingDirectory, "target", "RegisterUsage.json"); + RegisterUsagesFromFile(singleFileComponentRecorder, instructionsFile); + } + + Directory.Delete(workingDirectory, recursive: true); + } + catch (Exception e) + { + Logger.LogError("Exception occurred during Ivy file processing: " + e); + + // If something went wrong, just ignore the file + Logger.LogFailedReadingFile(ivyXmlFile, e); + } + } + + private void InitTemporaryAntProject(string workingDirectory, string ivyXmlFile, string ivySettingsXmlFile) + { + Directory.CreateDirectory(workingDirectory); + File.Copy(ivyXmlFile, Path.Combine(workingDirectory, "ivy.xml")); + if (ivySettingsXmlFile != null) + { + File.Copy(ivySettingsXmlFile, Path.Combine(workingDirectory, "ivysettings.xml")); + } + + var assembly = Assembly.GetExecutingAssembly(); + + using (Stream fileIn = assembly.GetManifestResourceStream("Microsoft.ComponentDetection.Detectors.ivy.Resources.build.xml")) + using (FileStream fileOut = File.Create(Path.Combine(workingDirectory, "build.xml"))) + { + fileIn.CopyTo(fileOut); + } + + Directory.CreateDirectory(Path.Combine(workingDirectory, "java-src")); + using (Stream fileIn = assembly.GetManifestResourceStream("Microsoft.ComponentDetection.Detectors.ivy.Resources.java_src.IvyComponentDetectionAntTask.java")) + using (FileStream fileOut = File.Create(Path.Combine(workingDirectory, "java-src", "IvyComponentDetectionAntTask.java"))) + { + fileIn.CopyTo(fileOut); + } + } + + private async Task IsAntLocallyAvailableAsync() + { + // Note: calling CanCommandBeLocated populates a cache of valid commands. If it is not called before ExecuteCommand, + // ExecuteCommand calls CanCommandBeLocated with no arguments, which fails. + return await CommandLineInvocationService.CanCommandBeLocated(PrimaryCommand, AdditionalValidCommands, AntVersionArgument); + } + + private async Task RunAntToDetectDependenciesAsync(string workingDirectory) + { + bool ret = false; + Logger.LogVerbose($"Executing command `ant resolve-dependencies` in directory {workingDirectory}"); + CommandLineExecutionResult result = await CommandLineInvocationService.ExecuteCommand(PrimaryCommand, additionalCandidateCommands: AdditionalValidCommands, "-buildfile", workingDirectory, "resolve-dependencies"); + if (result.ExitCode == 0) + { + Logger.LogVerbose("Ant command succeeded"); + ret = true; + } + else + { + Logger.LogError($"Ant command failed with return code {result.ExitCode}"); + } + + if (string.IsNullOrWhiteSpace(result.StdOut)) + { + Logger.LogVerbose("Ant command wrote nothing to stdout."); + } + else + { + Logger.LogVerbose("Ant command stdout:\n" + result.StdOut); + } + + if (string.IsNullOrWhiteSpace(result.StdErr)) + { + Logger.LogVerbose("Ant command wrote nothing to stderr."); + } + else + { + Logger.LogWarning("Ant command stderr:\n" + result.StdErr); + } + + return ret; + } + + private static MavenComponent JsonGavToComponent(JToken gav) + { + if (gav == null) + { + return null; + } + + return new MavenComponent( + gav.Value("g"), + gav.Value("a"), + gav.Value("v")); + } + + private void RegisterUsagesFromFile(ISingleFileComponentRecorder singleFileComponentRecorder, string instructionsFile) + { + JObject instructionsJson = JObject.Parse(File.ReadAllText(instructionsFile)); + JContainer instructionsList = (JContainer)instructionsJson["RegisterUsage"]; + foreach (JToken dep in instructionsList) + { + MavenComponent component = JsonGavToComponent(dep["gav"]); + bool isDevDependency = dep.Value("DevelopmentDependency"); + MavenComponent parentComponent = JsonGavToComponent(dep["parent_gav"]); + bool isResolved = dep.Value("resolved"); + if (isResolved) + { + singleFileComponentRecorder.RegisterUsage( + detectedComponent: new DetectedComponent(component), + isExplicitReferencedDependency: parentComponent == null, + parentComponentId: parentComponent?.Id, + isDevelopmentDependency: isDevDependency); + } + else + { + Logger.LogWarning($"Dependency \"{component.Id}\" could not be resolved by Ivy, and so has not been recorded by Component Detection."); + } + } + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/ivy/Resources/build.xml b/src/Microsoft.ComponentDetection.Detectors/ivy/Resources/build.xml new file mode 100644 index 000000000..19c6539de --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/ivy/Resources/build.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/src/Microsoft.ComponentDetection.Detectors/ivy/Resources/java-src/IvyComponentDetectionAntTask.java b/src/Microsoft.ComponentDetection.Detectors/ivy/Resources/java-src/IvyComponentDetectionAntTask.java new file mode 100644 index 000000000..f4b2434b0 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/ivy/Resources/java-src/IvyComponentDetectionAntTask.java @@ -0,0 +1,323 @@ +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.PrintStream; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.ivy.ant.IvyPostResolveTask; +import org.apache.ivy.core.module.id.ModuleRevisionId; +import org.apache.ivy.core.report.ResolveReport; +import org.apache.ivy.core.report.ArtifactDownloadReport; +import org.apache.ivy.core.resolve.IvyNode; +import org.apache.ivy.core.resolve.IvyNodeCallers.Caller; +import org.apache.ivy.core.resolve.IvyNodeEviction.EvictionData; +import org.apache.tools.ant.BuildException; + +/** + * An Ant task to write out RegisterUsage.json, which is a list of instructions for passing to RegisterUsage. + * + * Dependencies specified with org+name+rev, with org different from the project org are assumed to be Maven dependencies + * published to a public Maven repository with GAV == (groupId, artifactId, version) == (org, name, rev). Other components + * are ignored. + * + * If the ivy.xml defines a configuration called "default" or "runtime", any dependency required in that configuration + * is assumed to be a runtime dependency, and the rest are assumed to be dev dependencies. Otherwise, all dependencies + * are assumed to be non-dev dependencies. + * + * This task is based on Ivy source code from here: + * https://github.com/apache/ant-ivy/blob/master/src/java/org/apache/ivy/ant/IvyDependencyTree.java + * + * JSON output is constructed directly to avoid adding an extra dependency for a JSON library. + */ +public final class IvyComponentDetectionAntTask extends IvyPostResolveTask { + + private static final String[] RUNTIME_CONF_NAMES = {"default", "runtime"}; + + /* Parameters passed via build.xml */ + private File outFile = null; + + /* Internal state fields */ + private final Map> dependencies = new HashMap<>(); + private boolean detectDevDependencies = false; + + @Override + public void doExecute() throws BuildException { + prepareAndCheck(); + final ResolveReport report = getResolvedReport(); + if (report == null) { + throw new BuildException("No resolution report was available to run the post-resolve task. Make sure resolve was done before this task"); + } + log("Component Detection for " + report.getResolveId()); + final ModuleRevisionId rootMrid = report.getModuleDescriptor().getModuleRevisionId(); + final String rootOrganisation = rootMrid.getOrganisation(); + log("Root organisation is " + rootOrganisation + ". Dependencies with this groupId will be ignored."); + final String[] allConfigurations = report.getConfigurations(); + log("All configurations: " + String.join(", ", report.getConfigurations())); + for (final String runtimeConfigurationName : RUNTIME_CONF_NAMES) { + if (Arrays.stream(allConfigurations).anyMatch(runtimeConfigurationName::equals)) { + this.detectDevDependencies = true; + log("Detected configuration with name '" + runtimeConfigurationName + "' in ivy.xml. Activating dev dependency detection: all dependencies not required for " + String.join(" or ", RUNTIME_CONF_NAMES) + " will be marked as dev dependencies."); + } + } + final IvyNode[] unresolvedDeps = report.getUnresolvedDependencies(); + if (unresolvedDeps != null) for (final IvyNode unresolvedDep : unresolvedDeps) { + if (unresolvedDep.getId().getOrganisation() != rootOrganisation) { + log("Warning: dependency could not be resolved and will not be passed to Component Governance: " + unresolvedDep.getId()); + } + } + if (!this.detectDevDependencies) { + log("Warning: will not discriminate between dev dependencies and runtime dependencies, because ivy.xml defines no configurations called " + String.join(" or ", RUNTIME_CONF_NAMES)); + } + for (final IvyNode dependency : report.getDependencies()) { + if (dependency.getId().getOrganisation() == rootOrganisation) { + log("Warning: direct dependency " + dependency.getId() + " has organisation " + rootOrganisation + " and so is considered to be custom code. If it is really a third-party dependency published to Maven Central, please update its org/name/rev attributes to match its Maven GAV."); + } + if (dependency.isCompletelyEvicted()) { + log("Ignoring evicted dependency " + dependency.getId()); + } + populateDependencyTree(dependency); + } + final List dependencyList = this.dependencies.get(rootMrid); + if (dependencyList != null) { + log("Writing output to " + this.outFile.getAbsolutePath()); + try (final PrintStream outStream = new PrintStream(new FileOutputStream(this.outFile))) { + writeRegisterUsage(outStream, dependencyList, rootMrid); + } catch (final FileNotFoundException e) { + throw new BuildException("Failed to write cgmanifest.json", e); + } + } + } + + /** + * Flatten the list of unique dependencies. + */ + private void flattenDependencyListRecursive(final List listIn, final List listOut, final Set dedup, final String rootOrganisation) { + for (final CGNode node : listIn) { + final ModuleRevisionId mrid = stripExtraAttributes(node.getIvyNode().getId()); + if (mrid.getOrganisation() != rootOrganisation && dedup.add(mrid)) { + listOut.add(node); + final List dependenciesForModule = this.dependencies.get(mrid); + if (dependenciesForModule != null && !dependenciesForModule.isEmpty()) { + flattenDependencyListRecursive(dependenciesForModule, listOut, dedup, rootOrganisation); + } + } + } + } + + /** + * Write the output file. + */ + private void writeRegisterUsage(final PrintStream out, final List baseDependencyList, final ModuleRevisionId rootMrid) { + final List flattenedDependencyList = new ArrayList<>(); + flattenDependencyListRecursive(baseDependencyList, flattenedDependencyList, new HashSet(), rootMrid.getOrganisation()); + out.println("{\"RegisterUsage\": ["); + boolean needComma = false; + for (final CGNode node : flattenedDependencyList) { + final IvyNode dependency = node.getIvyNode(); + final ModuleRevisionId parentMrid = node.getParentMrid(); + final boolean isDevDependency = node.isDevDependency(); + final ModuleRevisionId mrid = stripExtraAttributes(dependency.getId()); + if (needComma) { + out.println(","); + } + out.print("{\"gav\": "); + out.print(jsonGav(mrid)); + out.print(", \"DevelopmentDependency\": "); + out.print(jsonBoolean(isDevDependency)); + out.print(", \"resolved\": "); + out.print(jsonBoolean(!dependency.hasProblem())); + if (parentMrid != null && parentMrid != rootMrid) { + out.print(", \"parent_gav\": "); + out.print(jsonGav(parentMrid)); + } + out.print("}"); + needComma = true; + } + out.println("\n]\n}"); + } + + /** + * Quote and escape a JSON string literal. + */ + private static String jsonStringLiteral(final String s) { + if (s == null) { + return "null"; + } + final StringBuilder ret = new StringBuilder("\""); + final int len = s.length(); + for (int charOffset = 0; charOffset < len; ) { + final int codepoint = s.codePointAt(charOffset); + if (codepoint == '\\') { + ret.append("\\\\"); + } else if (codepoint == '"') { + ret.append("\\\""); + } else if (codepoint == 10) { + ret.append("\\n"); + } else if (codepoint == 13) { + ret.append("\\r"); + } else if (codepoint < 32) { + ret.append(String.format("\\u%04x", codepoint)); + } else { + ret.appendCodePoint(codepoint); + } + charOffset += Character.charCount(codepoint); + } + ret.append("\""); + return ret.toString(); + } + + private static String jsonBoolean(final boolean value) { + return value ? "true" : "false"; + } + + /** + * Serialise JSON representing a GAV {"g": , "a": , "v": } + * @param mrid Ivy dependency ID object. + * @return String representing mrid as JSON. + */ + private static String jsonGav(final ModuleRevisionId mrid) { + final StringBuilder ret = new StringBuilder("{\"g\": "); + ret.append(jsonStringLiteral(mrid.getOrganisation())); + ret.append(", \"a\": "); + ret.append(jsonStringLiteral(mrid.getName())); + ret.append(", \"v\": "); + ret.append(jsonStringLiteral(mrid.getRevision())); + ret.append("}"); + return ret.toString(); + } + + /** + * Extra attributes are in the ModuleRevisionId's returned by ResolveReport.getDependencies() but not those returned by + * dependency.getAllCallers(). So strip them so that the ModuleRevisionId's match up. + */ + private static ModuleRevisionId stripExtraAttributes(final ModuleRevisionId mrid) { + final Map extraAttributes = mrid.getExtraAttributes(); + if (extraAttributes.isEmpty()) { + return mrid; + } else { + return ModuleRevisionId.newInstance(mrid.getOrganisation(), mrid.getName(), mrid.getBranch(), mrid.getRevision()); + } + } + + /** + * Check whether a dependency is a dev dependency. If dev dependency detection is enabled, any dependency not required in + * any of the RUNTIME_CONF_NAMES configurations is marked as a dev dependency. + */ + private boolean checkIsDevDependency(final IvyNode dependency) { + if (this.detectDevDependencies) { + for (final String nondevConfName : RUNTIME_CONF_NAMES) { + final String[] depConfs = dependency.getConfigurations(nondevConfName); + if (depConfs != null && depConfs.length > 0) { + return false; + } + } + log("Marking dependency " + dependency.getId() + " as a dev dependency because it's not required by configurations " + String.join(", ", RUNTIME_CONF_NAMES)); + return true; + } else { + return false; + } + } + + /** + * Build the dependency tree from the given root. + */ + private void populateDependencyTree(final IvyNode dependency) { + registerNodeIfNecessary(stripExtraAttributes(dependency.getId())); + final Set dedup = new HashSet(); + final boolean isDevDependency = checkIsDevDependency(dependency); + for (final Caller caller : dependency.getAllCallers()) { + // stripExtraAttributes in the next line is redundant in Ivy v2.5.0, but included in case future versions of Ivy + // include the extraAttributes in caller.getModuleRevisionId(). + final ModuleRevisionId mrid = stripExtraAttributes(caller.getModuleRevisionId()); + if (dedup.add(dependency.getId())) { + if (dependency.isCompletelyEvicted()) { + log("Ignoring evicted dependency " + dependency.getId() + " (transitive dependency of " + mrid + ")"); + } else { + log("Dependency " + mrid + " has transitive dependency " + dependency.getId()); + addDependency(mrid, dependency, isDevDependency); + } + } + } + } + + /** + * Add a new entry to the this.dependencies if needed. + */ + private void registerNodeIfNecessary(final ModuleRevisionId moduleRevisionId) { + if (!this.dependencies.containsKey(moduleRevisionId)) { + this.dependencies.put(moduleRevisionId, new ArrayList()); + } + } + + /** + * Register a new dependency of a given node. + */ + private void addDependency(final ModuleRevisionId parentMrid, final IvyNode childDependency, final boolean isDevDependency) { + final ModuleRevisionId parentMridStripped = stripExtraAttributes(parentMrid); + registerNodeIfNecessary(parentMridStripped); + final CGNode newNode = new CGNode(childDependency, parentMridStripped, isDevDependency); + this.dependencies.get(parentMridStripped).add(newNode); + } + + /** + * Set task argument. + * @param outFile Desired output file. + */ + public void setOut(final File outFile) { + this.outFile = outFile; + } + + /** + * Class to represent an entry in the dependency tree. It stores the underlying Ivy node and the + * dev/non-dev metadata, and also allows them to be sorted. + */ + private static final class CGNode implements Comparable { + private final IvyNode ivyNode; + private final ModuleRevisionId parentMrid; + private final boolean devDependency; + + public CGNode(final IvyNode ivyNode, final ModuleRevisionId parentMrid, final boolean devDependency) { + this.ivyNode = ivyNode; + this.parentMrid = parentMrid; + this.devDependency = devDependency; + } + + public IvyNode getIvyNode() { + return this.ivyNode; + } + + public ModuleRevisionId getParentMrid() { + return this.parentMrid; + } + + public boolean isDevDependency() { + return this.devDependency; + } + + @Override + public int compareTo(final CGNode other) { + final ModuleRevisionId myMrid = this.ivyNode.getId(); + final ModuleRevisionId theirMrid = other.ivyNode.getId(); + int ret = myMrid.getOrganisation().compareTo(theirMrid.getOrganisation()); + if (ret != 0) { + return ret; + } + ret = myMrid.getName().compareTo(theirMrid.getName()); + if (ret != 0) { + return ret; + } + ret = myMrid.getRevision().compareTo(theirMrid.getRevision()); + if (ret != 0) { + return ret; + } + return myMrid.toString().compareTo(theirMrid.toString()); + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Classification.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Classification.cs new file mode 100644 index 000000000..30d01f0fb --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Classification.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + public class Classification + { + public string Class { get; set; } + + public Dictionary Metadata { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/ConfigurationUnion.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/ConfigurationUnion.cs new file mode 100644 index 000000000..9a6d110e5 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/ConfigurationUnion.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; + +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + public struct ConfigurationUnion + { + public object[] AnythingArray; + public Dictionary AnythingMap; + public bool? Bool; + public double? Double; + public long? Integer; + public string String; + + public static implicit operator ConfigurationUnion(object[] anythingArray) + { + return new ConfigurationUnion { AnythingArray = anythingArray }; + } + + public static implicit operator ConfigurationUnion(Dictionary anythingMap) + { + return new ConfigurationUnion { AnythingMap = anythingMap }; + } + + public static implicit operator ConfigurationUnion(bool @bool) + { + return new ConfigurationUnion { Bool = @bool }; + } + + public static implicit operator ConfigurationUnion(double @double) + { + return new ConfigurationUnion { Double = @double }; + } + + public static implicit operator ConfigurationUnion(long integer) + { + return new ConfigurationUnion { Integer = integer }; + } + + public static implicit operator ConfigurationUnion(string @string) + { + return new ConfigurationUnion { String = @string }; + } + + public bool IsNull => AnythingArray == null && Bool == null && Double == null && Integer == null && AnythingMap == null && + String == null; + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Descriptor.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Descriptor.cs new file mode 100644 index 000000000..4d3162376 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Descriptor.cs @@ -0,0 +1,11 @@ +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + public class Descriptor + { + public ConfigurationUnion? Configuration { get; set; } + + public string Name { get; set; } + + public string Version { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Digest.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Digest.cs new file mode 100644 index 000000000..3657c966f --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Digest.cs @@ -0,0 +1,9 @@ +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + public class Digest + { + public string Algorithm { get; set; } + + public string Value { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/DigestElement.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/DigestElement.cs new file mode 100644 index 000000000..dedd1f933 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/DigestElement.cs @@ -0,0 +1,9 @@ +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + public class DigestElement + { + public string Algorithm { get; set; } + + public string Value { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Distribution.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Distribution.cs new file mode 100644 index 000000000..fd188e682 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Distribution.cs @@ -0,0 +1,11 @@ +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + public class Distribution + { + public string IdLike { get; set; } + + public string Name { get; set; } + + public string Version { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/File.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/File.cs new file mode 100644 index 000000000..19346e68b --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/File.cs @@ -0,0 +1,18 @@ +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + public struct File + { + public FileRecord FileRecord; + public string String; + + public static implicit operator File(FileRecord fileRecord) + { + return new File { FileRecord = fileRecord }; + } + + public static implicit operator File(string @string) + { + return new File { String = @string }; + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/FileClassifications.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/FileClassifications.cs new file mode 100644 index 000000000..4329efa50 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/FileClassifications.cs @@ -0,0 +1,9 @@ +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + public class FileClassifications + { + public Classification Classification { get; set; } + + public Location Location { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/FileContents.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/FileContents.cs new file mode 100644 index 000000000..332db8232 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/FileContents.cs @@ -0,0 +1,9 @@ +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + public class FileContents + { + public string Contents { get; set; } + + public Location Location { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/FileMetadata.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/FileMetadata.cs new file mode 100644 index 000000000..41c3fdab6 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/FileMetadata.cs @@ -0,0 +1,9 @@ +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + public class FileMetadata + { + public Location Location { get; set; } + + public FileMetadataEntry Metadata { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/FileMetadataEntry.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/FileMetadataEntry.cs new file mode 100644 index 000000000..4aded93d9 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/FileMetadataEntry.cs @@ -0,0 +1,17 @@ +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + public class FileMetadataEntry + { + public DigestElement[] Digests { get; set; } + + public long GroupId { get; set; } + + public string LinkDestination { get; set; } + + public long Mode { get; set; } + + public string Type { get; set; } + + public long UserId { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/FileRecord.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/FileRecord.cs new file mode 100644 index 000000000..e2c4eff71 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/FileRecord.cs @@ -0,0 +1,27 @@ +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + public class FileRecord + { + public Digest Digest { get; set; } + + public string OwnerGid { get; set; } + + public string OwnerUid { get; set; } + + public string Path { get; set; } + + public string Permissions { get; set; } + + public bool? IsConfigFile { get; set; } + + public Size? Size { get; set; } + + public string Flags { get; set; } + + public string GroupName { get; set; } + + public long? Mode { get; set; } + + public string UserName { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/ImageScanningResult.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/ImageScanningResult.cs new file mode 100644 index 000000000..b474228c8 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/ImageScanningResult.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.BcdeModels; + +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + internal class ImageScanningResult + { + public ContainerDetails ContainerDetails { get; set; } + + public IEnumerable Components { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/JavaManifest.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/JavaManifest.cs new file mode 100644 index 000000000..52849b52a --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/JavaManifest.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + public class JavaManifest + { + public Dictionary Main { get; set; } + + public Dictionary> NamedSections { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Location.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Location.cs new file mode 100644 index 000000000..871def87b --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Location.cs @@ -0,0 +1,9 @@ +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + public class Location + { + public string LayerId { get; set; } + + public string Path { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Metadata.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Metadata.cs new file mode 100644 index 000000000..b0e6b479f --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Metadata.cs @@ -0,0 +1,73 @@ +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + public class Metadata + { + public string Architecture { get; set; } + + public string Description { get; set; } + + public File[] Files { get; set; } + + public string GitCommitOfApkPort { get; set; } + + public long? InstalledSize { get; set; } + + public string License { get; set; } + + public string Maintainer { get; set; } + + public string OriginPackage { get; set; } + + public string Package { get; set; } + + public string PullChecksum { get; set; } + + public string PullDependencies { get; set; } + + public long? Size { get; set; } + + public string Url { get; set; } + + public string Version { get; set; } + + public string Checksum { get; set; } + + public string[] Dependencies { get; set; } + + public string Name { get; set; } + + public string Source { get; set; } + + public string SourceVersion { get; set; } + + public string[] Authors { get; set; } + + public string Homepage { get; set; } + + public string[] Licenses { get; set; } + + public JavaManifest Manifest { get; set; } + + public PomProperties PomProperties { get; set; } + + public string VirtualPath { get; set; } + + public string Author { get; set; } + + public string AuthorEmail { get; set; } + + public string Platform { get; set; } + + public string SitePackagesRootPath { get; set; } + + public string[] TopLevelPackages { get; set; } + + public long? Epoch { get; set; } + + public string Release { get; set; } + + public string SourceRpm { get; set; } + + public string Vendor { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Package.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Package.cs new file mode 100644 index 000000000..6b487962c --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Package.cs @@ -0,0 +1,29 @@ +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + public class Package + { + public string[] Cpes { get; set; } + + public string FoundBy { get; set; } + + public string Id { get; set; } + + public string Language { get; set; } + + public string[] Licenses { get; set; } + + public Location[] Locations { get; set; } + + public Metadata Metadata { get; set; } + + public string MetadataType { get; set; } + + public string Name { get; set; } + + public string Purl { get; set; } + + public string Type { get; set; } + + public string Version { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/PomProperties.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/PomProperties.cs new file mode 100644 index 000000000..5267e5d18 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/PomProperties.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + public class PomProperties + { + public string ArtifactId { get; set; } + + public Dictionary ExtraFields { get; set; } + + public string GroupId { get; set; } + + public string Name { get; set; } + + public string Path { get; set; } + + public string Version { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Relationship.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Relationship.cs new file mode 100644 index 000000000..8ed17d994 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Relationship.cs @@ -0,0 +1,13 @@ +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + public class Relationship + { + public string Child { get; set; } + + public ConfigurationUnion Metadata { get; set; } + + public string Parent { get; set; } + + public string Type { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Schema.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Schema.cs new file mode 100644 index 000000000..41fb2672b --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Schema.cs @@ -0,0 +1,9 @@ +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + public class Schema + { + public string Url { get; set; } + + public string Version { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/SearchResult.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/SearchResult.cs new file mode 100644 index 000000000..856b27494 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/SearchResult.cs @@ -0,0 +1,17 @@ +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + public class SearchResult + { + public string Classification { get; set; } + + public long Length { get; set; } + + public long LineNumber { get; set; } + + public long LineOffset { get; set; } + + public long SeekPosition { get; set; } + + public string Value { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Secrets.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Secrets.cs new file mode 100644 index 000000000..f6cd9d065 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Secrets.cs @@ -0,0 +1,9 @@ +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + public class Secrets + { + public Location Location { get; set; } + + public SearchResult[] SecretsSecrets { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Size.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Size.cs new file mode 100644 index 000000000..5c92bb1f5 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Size.cs @@ -0,0 +1,18 @@ +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + public struct Size + { + public long? Integer; + public string String; + + public static implicit operator Size(long integer) + { + return new Size { Integer = integer }; + } + + public static implicit operator Size(string @string) + { + return new Size { String = @string }; + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Source.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Source.cs new file mode 100644 index 000000000..fa1b8cad7 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/Source.cs @@ -0,0 +1,9 @@ +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + public class Source + { + public ConfigurationUnion Target { get; set; } + + public string Type { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/SyftOutput.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/SyftOutput.cs new file mode 100644 index 000000000..bd4c1e3d1 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Contracts/SyftOutput.cs @@ -0,0 +1,30 @@ +namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts +{ + /// + /// Take from https://github.com/anchore/syft/tree/main/schema/json. + /// Match version to tag used i.e. https://github.com/anchore/syft/blob/v0.16.1/internal/constants.go#L9 + /// Can convert JSON Schema to C# using quicktype.io. + /// + public class SyftOutput + { + public Relationship[] ArtifactRelationships { get; set; } + + public Package[] Artifacts { get; set; } + + public Descriptor Descriptor { get; set; } + + public Distribution Distro { get; set; } + + public FileClassifications[] FileClassifications { get; set; } + + public FileContents[] FileContents { get; set; } + + public FileMetadata[] FileMetadata { get; set; } + + public Schema Schema { get; set; } + + public Secrets[] Secrets { get; set; } + + public Source Source { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Exceptions/MissingContainerDetailException.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Exceptions/MissingContainerDetailException.cs new file mode 100644 index 000000000..234fb0101 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Exceptions/MissingContainerDetailException.cs @@ -0,0 +1,12 @@ +using System; + +namespace Microsoft.ComponentDetection.Detectors.Linux.Exceptions +{ + public class MissingContainerDetailException : Exception + { + public MissingContainerDetailException(string imageId) + : base($"No container details information could be found for image ${imageId}") + { + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/ILinuxScanner.cs b/src/Microsoft.ComponentDetection.Detectors/linux/ILinuxScanner.cs new file mode 100644 index 000000000..6163b5526 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/ILinuxScanner.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.ComponentDetection.Contracts.BcdeModels; + +namespace Microsoft.ComponentDetection.Detectors.Linux +{ + public interface ILinuxScanner + { + Task> ScanLinuxAsync(string imageHash, IEnumerable dockerLayers, int baseImageLayerCount, CancellationToken cancellationToken = default); + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/LinuxContainerDetector.cs b/src/Microsoft.ComponentDetection.Detectors/linux/LinuxContainerDetector.cs new file mode 100644 index 000000000..7db96dc81 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/LinuxContainerDetector.cs @@ -0,0 +1,249 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Composition; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.ComponentDetection.Common.Exceptions; +using Microsoft.ComponentDetection.Common.Telemetry.Records; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.BcdeModels; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Microsoft.ComponentDetection.Detectors.Linux.Contracts; +using Microsoft.ComponentDetection.Detectors.Linux.Exceptions; + +namespace Microsoft.ComponentDetection.Detectors.Linux +{ + [Export(typeof(IComponentDetector))] + public class LinuxContainerDetector : IComponentDetector + { + [Import] + public ILogger Logger { get; set; } + + [Import] + public ILinuxScanner LinuxScanner { get; set; } + + [Import] + public IDockerService DockerService { get; set; } + + public string Id => "Linux"; + + public IEnumerable Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.Linux) }; + + public IEnumerable SupportedComponentTypes => new[] { ComponentType.Linux }; + + public int Version => 3; + + public bool NeedsAutomaticRootDependencyCalculation => false; + + public async Task ExecuteDetectorAsync(ScanRequest request) + { + var imagesToProcess = request.ImagesToScan?.Where(image => !string.IsNullOrWhiteSpace(image)) + .Select(image => image.ToLowerInvariant()) + .ToList(); + + if (imagesToProcess == null || !imagesToProcess.Any()) + { + Logger.LogInfo("No instructions received to scan docker images."); + return EmptySuccessfulScan(); + } + + var cancellationTokenSource = new CancellationTokenSource(GetTimeout(request.DetectorArgs)); + + if (!await DockerService.CanRunLinuxContainersAsync(cancellationTokenSource.Token)) + { + using var record = new LinuxContainerDetectorUnsupportedOs + { + Os = RuntimeInformation.OSDescription, + }; + Logger.LogInfo("Linux containers are not available on this host."); + return EmptySuccessfulScan(); + } + + var results = Enumerable.Empty(); + try + { + results = await ProcessImagesAsync(imagesToProcess, request.ComponentRecorder, cancellationTokenSource.Token); + } + catch (OperationCanceledException) + { + using var record = new LinuxContainerDetectorTimeout(); + } + + return new IndividualDetectorScanResult + { + ContainerDetails = results.Where(tuple => tuple.ContainerDetails != null).Select(tuple => tuple.ContainerDetails).ToList(), + ResultCode = ProcessingResultCode.Success, + }; + } + + private async Task> ProcessImagesAsync( + IEnumerable imagesToProcess, + IComponentRecorder componentRecorder, CancellationToken cancellationToken = default) + { + var processedImages = new ConcurrentDictionary(); + + var inspectTasks = imagesToProcess.Select( + async image => + { + try + { + // Check image exists locally. Try docker pull if not + if (!(await DockerService.ImageExistsLocallyAsync(image, cancellationToken) || + await DockerService.TryPullImageAsync(image, cancellationToken))) + { + throw new InvalidUserInputException( + $"Docker image {image} could not be found locally and could not be pulled. Verify the image is either available locally or through docker pull.", + null); + } + + var imageDetails = await DockerService.InspectImageAsync(image, cancellationToken); + + // Unable to fetch image details + if (imageDetails == null) + { + throw new MissingContainerDetailException(image); + } + + processedImages.TryAdd(imageDetails.ImageId, imageDetails); + } + catch (Exception e) + { + using var record = new LinuxContainerDetectorImageDetectionFailed + { + ExceptionType = e.GetType().ToString(), + Message = e.Message, + StackTrace = e.StackTrace, + ImageId = image, + }; + } + }); + + await Task.WhenAll(inspectTasks); + + var scanTasks = processedImages.Select(async kvp => + { + try + { + var internalContainerDetails = kvp.Value; + var image = kvp.Key; + int baseImageLayerCount = await GetBaseImageLayerCount(internalContainerDetails, image, cancellationToken); + + //Update the layer information to specify if a layer was fond in the specified baseImage + internalContainerDetails.Layers = internalContainerDetails.Layers.Select(layer => new DockerLayer + { + DiffId = layer.DiffId, + LayerIndex = layer.LayerIndex, + IsBaseImage = layer.LayerIndex < baseImageLayerCount, + }); + + var layers = await LinuxScanner.ScanLinuxAsync(kvp.Value.ImageId, internalContainerDetails.Layers, baseImageLayerCount, cancellationToken); + + var components = layers.SelectMany(layer => layer.LinuxComponents.Select(linuxComponent => new DetectedComponent(linuxComponent, null, internalContainerDetails.Id, layer.DockerLayer.LayerIndex))); + internalContainerDetails.Layers = layers.Select(layer => layer.DockerLayer); + var singleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder(kvp.Value.ImageId); + components.ToList().ForEach(detectedComponent => singleFileComponentRecorder.RegisterUsage(detectedComponent, true)); + return new ImageScanningResult + { + ContainerDetails = kvp.Value, + Components = components, + }; + } + catch (Exception e) + { + using var record = new LinuxContainerDetectorImageDetectionFailed + { + ExceptionType = e.GetType().ToString(), + Message = e.Message, + StackTrace = e.StackTrace, + ImageId = kvp.Value.ImageId, + }; + } + + return EmptyImageScanningResult(); + }); + + return await Task.WhenAll(scanTasks); + } + + /// + /// Extracts and returns the timeout defined by the user, or a default value if one is not provided. + /// + /// The arguments provided by the user. + /// + private static TimeSpan GetTimeout(IDictionary detectorArgs) + { + if (detectorArgs == null || !detectorArgs.TryGetValue("Linux.ScanningTimeoutSec", out var timeout)) + { + return TimeSpan.FromMinutes(10); + } + + return double.TryParse(timeout, out var parsedTimeout) ? TimeSpan.FromSeconds(parsedTimeout) : TimeSpan.FromMinutes(10); + } + + private static IndividualDetectorScanResult EmptySuccessfulScan() + { + return new IndividualDetectorScanResult + { + ResultCode = ProcessingResultCode.Success, + }; + } + + private static ImageScanningResult EmptyImageScanningResult() + { + return new ImageScanningResult + { + ContainerDetails = null, + Components = Enumerable.Empty(), + }; + } + + private async Task GetBaseImageLayerCount(ContainerDetails scannedImageDetails, string image, CancellationToken cancellationToken = default) + { + using var record = new LinuxContainerDetectorLayerAwareness + { + LayerCount = scannedImageDetails.Layers.Count(), + }; + + if (string.IsNullOrEmpty(scannedImageDetails.BaseImageRef)) + { + record.BaseImageLayerMessage = $"Base image annotations not found on image {image}, Results will not be mapped to base image layers"; + Logger.LogInfo(record.BaseImageLayerMessage); + return 0; + } + + var baseImageDigest = scannedImageDetails.BaseImageDigest; + var refWithDigest = scannedImageDetails.BaseImageRef + (baseImageDigest != string.Empty ? $"@{baseImageDigest}" : string.Empty); + record.BaseImageDigest = baseImageDigest; + record.BaseImageRef = scannedImageDetails.BaseImageRef; + + if (!(await DockerService.ImageExistsLocallyAsync(refWithDigest, cancellationToken) || + await DockerService.TryPullImageAsync(refWithDigest, cancellationToken))) + { + record.BaseImageLayerMessage = $"Base image {refWithDigest} could not be found locally and could not be pulled. Results will not be mapped to base image layers"; + Logger.LogInfo(record.BaseImageLayerMessage); + return 0; + } + + var baseImageDetails = await DockerService.InspectImageAsync(refWithDigest, cancellationToken); + if (!ValidateBaseImageLayers(scannedImageDetails, baseImageDetails)) + { + record.BaseImageLayerMessage = $"Docker image {image} was set to have base image {refWithDigest} but is not built off of it. Results will not be mapped to base image layers"; + Logger.LogInfo(record.BaseImageLayerMessage); + return 0; + } + + record.BaseImageLayerCount = baseImageDetails.Layers.Count(); + return baseImageDetails.Layers.Count(); + } + + // Validate that the image actually does start with the layers from the base image specified in the annotations + private bool ValidateBaseImageLayers(ContainerDetails scannedImageDetails, ContainerDetails baseImageDetails) + { + var scannedImageLayers = scannedImageDetails.Layers.ToArray(); + return !(baseImageDetails.Layers.Count() > scannedImageLayers.Count() || baseImageDetails.Layers.Where((layer, index) => scannedImageLayers[index].DiffId != layer.DiffId).Any()); + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/LinuxScanner.cs b/src/Microsoft.ComponentDetection.Detectors/linux/LinuxScanner.cs new file mode 100644 index 000000000..45b3c03ad --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/linux/LinuxScanner.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.ComponentDetection.Common.Telemetry.Records; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.BcdeModels; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Microsoft.ComponentDetection.Detectors.Linux.Contracts; +using Newtonsoft.Json; + +namespace Microsoft.ComponentDetection.Detectors.Linux +{ + [Export(typeof(ILinuxScanner))] + public class LinuxScanner : ILinuxScanner + { + private const string ScannerImage = "governancecontainerregistry.azurecr.io/syft:0.24.1@sha256:3cc99325855732073ae403a3fa010c3d2fadf27bf7b0a58d4eeaf8454a034ce0"; + + private static readonly IList CmdParameters = new List + { + "--quiet", "--scope", "all-layers", "--output", "json", + }; + + private static readonly IEnumerable AllowedArtifactTypes = new[] { "apk", "deb", "rpm" }; + + private static readonly SemaphoreSlim DockerSemaphore = new SemaphoreSlim(2); + + private static readonly int SemaphoreTimeout = Convert.ToInt32(TimeSpan.FromHours(1).TotalMilliseconds); + + [Import] + public ILogger Logger { get; set; } + + [Import] + public IDockerService DockerService { get; set; } + + public async Task> ScanLinuxAsync(string imageHash, IEnumerable dockerLayers, int baseImageLayerCount, CancellationToken cancellationToken = default) + { + using var record = new LinuxScannerTelemetryRecord + { + ImageToScan = imageHash, + ScannerVersion = ScannerImage, + }; + + var acquired = false; + var stdout = string.Empty; + var stderr = string.Empty; + + using var syftTelemetryRecord = new LinuxScannerSyftTelemetryRecord(); + + try + { + acquired = await DockerSemaphore.WaitAsync(SemaphoreTimeout, cancellationToken); + if (acquired) + { + try + { + var command = new List { imageHash }.Concat(CmdParameters).ToList(); + (stdout, stderr) = await DockerService.CreateAndRunContainerAsync(ScannerImage, command, cancellationToken); + } + catch (Exception e) + { + syftTelemetryRecord.Exception = JsonConvert.SerializeObject(e); + Logger.LogException(e, false); + throw; + } + } + else + { + record.SemaphoreFailure = true; + Logger.LogWarning($"Failed to enter the docker semaphore for image {imageHash}"); + } + } + finally + { + if (acquired) + { + DockerSemaphore.Release(); + } + } + + record.ScanStdErr = stderr; + record.ScanStdOut = stdout; + + if (string.IsNullOrWhiteSpace(stdout) || !string.IsNullOrWhiteSpace(stderr)) + { + throw new InvalidOperationException( + $"Scan failed with exit info: {stdout}{Environment.NewLine}{stderr}"); + } + + var layerDictionary = dockerLayers.ToDictionary( + layer => layer.DiffId, + layer => new List()); + + try + { + var syftOutput = JsonConvert.DeserializeObject(stdout); + var linuxComponentsWithLayers = syftOutput.Artifacts + .DistinctBy(artifact => (artifact.Name, artifact.Version)) + .Where(artifact => AllowedArtifactTypes.Contains(artifact.Type)) + .Select(artifact => + (Component: new LinuxComponent(syftOutput.Distro.Name, syftOutput.Distro.Version, artifact.Name, artifact.Version), layerIds: artifact.Locations.Select(location => location.LayerId).Distinct())); + + foreach (var (component, layers) in linuxComponentsWithLayers) + { + layers.ToList().ForEach(layer => layerDictionary[layer].Add(component)); + } + + var layerMappedLinuxComponents = layerDictionary.Select(kvp => + { + (var layerId, var components) = kvp; + return new LayerMappedLinuxComponents + { + LinuxComponents = components, + DockerLayer = dockerLayers.First(layer => layer.DiffId == layerId), + }; + }); + + syftTelemetryRecord.LinuxComponents = JsonConvert.SerializeObject(linuxComponentsWithLayers.Select(linuxComponentWithLayer => + new + { + Name = linuxComponentWithLayer.Component.Name, + Version = linuxComponentWithLayer.Component.Version, + })); + + return layerMappedLinuxComponents; + } + catch (Exception e) + { + record.FailedDeserializingScannerOutput = e.ToString(); + return null; + } + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/maven/GraphNode.cs b/src/Microsoft.ComponentDetection.Detectors/maven/GraphNode.cs new file mode 100644 index 000000000..679ae7e48 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/maven/GraphNode.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace Microsoft.ComponentDetection.Detectors.Maven +{ + /// + /// Internal state holder used by Maven detector. + /// + /// Node type. + public class GraphNode + { + public GraphNode(T value) + { + Value = value; + } + + public T Value { get; set; } + + public List> Children { get; } = new List>(); + + public List> Parents { get; } = new List>(); + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/maven/IMavenCommandService.cs b/src/Microsoft.ComponentDetection.Detectors/maven/IMavenCommandService.cs new file mode 100644 index 000000000..6dc87326b --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/maven/IMavenCommandService.cs @@ -0,0 +1,16 @@ +using Microsoft.ComponentDetection.Contracts.Internal; +using System.Threading.Tasks; + +namespace Microsoft.ComponentDetection.Detectors.Maven +{ + public interface IMavenCommandService + { + string BcdeMvnDependencyFileName { get; } + + Task MavenCLIExists(); + + Task GenerateDependenciesFile(ProcessRequest processRequest); + + void ParseDependenciesFile(ProcessRequest processRequest); + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/maven/IMavenStyleDependencyGraphParserService.cs b/src/Microsoft.ComponentDetection.Detectors/maven/IMavenStyleDependencyGraphParserService.cs new file mode 100644 index 000000000..b6884ce27 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/maven/IMavenStyleDependencyGraphParserService.cs @@ -0,0 +1,9 @@ +using Microsoft.ComponentDetection.Contracts; + +namespace Microsoft.ComponentDetection.Detectors.Maven +{ + public interface IMavenStyleDependencyGraphParserService + { + void Parse(string[] lines, ISingleFileComponentRecorder singleFileComponentRecorder); + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/maven/MavenCommandService.cs b/src/Microsoft.ComponentDetection.Detectors/maven/MavenCommandService.cs new file mode 100644 index 000000000..01ed2656f --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/maven/MavenCommandService.cs @@ -0,0 +1,55 @@ +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.Internal; +using System; +using System.Composition; +using System.IO; +using System.Threading.Tasks; + +namespace Microsoft.ComponentDetection.Detectors.Maven +{ + [Export(typeof(IMavenCommandService))] + public class MavenCommandService : IMavenCommandService + { + [Import] + public ICommandLineInvocationService CommandLineInvocationService { get; set; } + + [Import] + public IMavenStyleDependencyGraphParserService ParserService { get; set; } + + [Import] + public ILogger Logger { get; set; } + + public string BcdeMvnDependencyFileName => "bcde.mvndeps"; + + internal const string PrimaryCommand = "mvn"; + + internal const string MvnVersionArgument = "--version"; + + internal static readonly string[] AdditionalValidCommands = new[] { "mvn.cmd" }; + + public async Task MavenCLIExists() + { + return await CommandLineInvocationService.CanCommandBeLocated(PrimaryCommand, AdditionalValidCommands, MvnVersionArgument); + } + + public async Task GenerateDependenciesFile(ProcessRequest processRequest) + { + var pomFile = processRequest.ComponentStream; + var cliParameters = new[] { "dependency:tree", "-B", $"-DoutputFile={BcdeMvnDependencyFileName}", "-DoutputType=text", $"-f{pomFile.Location}" }; + var result = await CommandLineInvocationService.ExecuteCommand(PrimaryCommand, AdditionalValidCommands, cliParameters); + if (result.ExitCode != 0) + { + Logger.LogVerbose($"Mvn execution failed for pom file: {pomFile.Location}"); + Logger.LogError(string.IsNullOrEmpty(result.StdErr) ? result.StdOut : result.StdErr); + } + } + + public void ParseDependenciesFile(ProcessRequest processRequest) + { + using StreamReader sr = new StreamReader(processRequest.ComponentStream.Stream); + + var lines = sr.ReadToEnd().Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + ParserService.Parse(lines, processRequest.SingleFileComponentRecorder); + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/maven/MavenParsingUtilities.cs b/src/Microsoft.ComponentDetection.Detectors/maven/MavenParsingUtilities.cs new file mode 100644 index 000000000..290e3f303 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/maven/MavenParsingUtilities.cs @@ -0,0 +1,52 @@ +using System; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.TypedComponent; + +namespace Microsoft.ComponentDetection.Detectors.Maven +{ + public static class MavenParsingUtilities + { + public static (DetectedComponent Component, bool? IsDevelopmentDependency) GenerateDetectedComponentFromMavenString(string key) + { + var component = GetMavenComponentFromComponentString(key); + + var detectedComponent = new DetectedComponent(component.component); + + return (detectedComponent, component.isDevDependency); + } + + private static (MavenComponent component, bool? isDevDependency) GetMavenComponentFromComponentString(string componentString) + { + var info = GetMavenComponentStringInfo(componentString); + return (new MavenComponent(info.groupId, info.artifactId, info.version), info.isDevelopmentDependency); + } + + private static (string groupId, string artifactId, string version, bool? isDevelopmentDependency) + GetMavenComponentStringInfo(string mavenComponentString) + { + var results = mavenComponentString.Split(':'); + if (results.Length > 6 || results.Length < 4) + { + throw new InvalidOperationException($"Bad key ('{mavenComponentString}') found in generated dependency graph."); + } + + if (results.Length == 6) + { + // Six part versions have an entry in their 4th index. We remove it to normalize. E.g.: + // var mysteriousSixPartVersionPart = results[3]; + results = new[] { results[0], results[1], results[2], results[4], results[5] }; + } + + var groupId = results[0]; + var artifactId = results[1]; + var version = results[3]; + bool? isDevDependency = null; + if (results.Length == 5) + { + isDevDependency = string.Equals(results[4], "test", StringComparison.OrdinalIgnoreCase); + } + + return (groupId, artifactId, version, isDevDependency); + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/maven/MavenStyleDependencyGraphParser.cs b/src/Microsoft.ComponentDetection.Detectors/maven/MavenStyleDependencyGraphParser.cs new file mode 100644 index 000000000..1297e9159 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/maven/MavenStyleDependencyGraphParser.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.ComponentDetection.Contracts; + +namespace Microsoft.ComponentDetection.Detectors.Maven +{ + public class MavenStyleDependencyGraphParser + { + public GraphNode DependencyCategory { get; private set; } + + private Stack> stack = new Stack>(); + + private Stack<(int ParseLevel, DetectedComponent Component)> tupleStack = new Stack<(int, DetectedComponent)>(); + + private DetectedComponent topLevelComponent = null; + + private static readonly char[] TrimCharacters = new char[] { '|', ' ' }; + + private static readonly string[] ComponentSplitters = new[] { "+-", "\\-" }; + + private void StartDependencyCategory(string categoryName) + { + if (DependencyCategory != null) + { + throw new InvalidOperationException("Current category must be finished before starting new category."); + } + + DependencyCategory = new GraphNode(categoryName); + } + + public GraphNode Parse(string[] lines) + { + foreach (var line in lines) + { + var localLine = line.Trim(TrimCharacters); + if (!string.IsNullOrWhiteSpace(localLine) && DependencyCategory == null) + { + StartDependencyCategory(localLine); + } + else + { + var splitterOrDefault = ComponentSplitters.FirstOrDefault(x => localLine.Contains(x)); + if (splitterOrDefault != null) + { + TrackDependency(line.IndexOf(splitterOrDefault), localLine.Split(new[] { splitterOrDefault }, StringSplitOptions.None)[1].Trim()); + } + } + } + + return DependencyCategory; + } + + public void Parse(string[] lines, ISingleFileComponentRecorder singleFileComponentRecorder) + { + foreach (var line in lines) + { + var localLine = line.Trim(TrimCharacters); + if (!string.IsNullOrWhiteSpace(localLine) && topLevelComponent == null) + { + var topLevelMavenStringTuple = MavenParsingUtilities.GenerateDetectedComponentFromMavenString(localLine); + topLevelComponent = topLevelMavenStringTuple.Component; + singleFileComponentRecorder.RegisterUsage(topLevelMavenStringTuple.Component, isDevelopmentDependency: topLevelMavenStringTuple.IsDevelopmentDependency); + } + else + { + var splitterOrDefault = ComponentSplitters.FirstOrDefault(x => localLine.Contains(x)); + + if (splitterOrDefault != null) + { + RecordDependencies(line.IndexOf(splitterOrDefault), localLine.Split(new[] { splitterOrDefault }, StringSplitOptions.None)[1].Trim(), singleFileComponentRecorder); + } + } + } + } + + private void TrackDependency(int position, string versionedComponent) + { + while (stack.Count > 0 && stack.Peek().ParseLevel >= position) + { + stack.Pop(); + } + + var myNode = new GraphNodeAtLevel(position, versionedComponent); + + if (stack.Count > 0) + { + var parent = stack.Peek(); + parent.Children.Add(myNode); + myNode.Parents.Add(parent); + } + else + { + DependencyCategory.Children.Add(myNode); + } + + stack.Push(myNode); + } + + private void RecordDependencies(int position, string versionedComponent, ISingleFileComponentRecorder componentRecorder) + { + while (tupleStack.Count > 0 && tupleStack.Peek().ParseLevel >= position) + { + tupleStack.Pop(); + } + + var componentAndDevDependencyTuple = MavenParsingUtilities.GenerateDetectedComponentFromMavenString(versionedComponent); + var newTuple = (ParseLevel: position, componentAndDevDependencyTuple.Component); + + if (tupleStack.Count > 0) + { + var parent = tupleStack.Peek().Component; + componentRecorder.RegisterUsage(parent); + componentRecorder.RegisterUsage(newTuple.Component, parentComponentId: parent.Component.Id, isDevelopmentDependency: componentAndDevDependencyTuple.IsDevelopmentDependency); + } + else + { + componentRecorder.RegisterUsage(newTuple.Component, isExplicitReferencedDependency: true, parentComponentId: topLevelComponent.Component.Id, isDevelopmentDependency: componentAndDevDependencyTuple.IsDevelopmentDependency); + } + + tupleStack.Push(newTuple); + } + + private class GraphNodeAtLevel : GraphNode + { + public int ParseLevel { get; } + + public GraphNodeAtLevel(int level, T value) + : base(value) + { + ParseLevel = level; + } + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/maven/MavenStyleDependencyGraphParserService.cs b/src/Microsoft.ComponentDetection.Detectors/maven/MavenStyleDependencyGraphParserService.cs new file mode 100644 index 000000000..aece60737 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/maven/MavenStyleDependencyGraphParserService.cs @@ -0,0 +1,15 @@ +using Microsoft.ComponentDetection.Contracts; +using System.Composition; + +namespace Microsoft.ComponentDetection.Detectors.Maven +{ + [Export(typeof(IMavenStyleDependencyGraphParserService))] + public class MavenStyleDependencyGraphParserService : IMavenStyleDependencyGraphParserService + { + public void Parse(string[] lines, ISingleFileComponentRecorder singleFileComponentRecorder) + { + var parser = new MavenStyleDependencyGraphParser(); + parser.Parse(lines, singleFileComponentRecorder); + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/maven/MvnCliComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/maven/MvnCliComponentDetector.cs new file mode 100644 index 000000000..c7c72de52 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/maven/MvnCliComponentDetector.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Composition; +using System.IO; +using System.Linq; +using System.Reactive.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using Microsoft.ComponentDetection.Common; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.Internal; +using Microsoft.ComponentDetection.Contracts.TypedComponent; + +namespace Microsoft.ComponentDetection.Detectors.Maven +{ + [Export(typeof(IComponentDetector))] + public class MvnCliComponentDetector : FileComponentDetector + { + public override string Id => "MvnCli"; + + public override IList SearchPatterns => new List { "pom.xml" }; + + public override IEnumerable SupportedComponentTypes => new[] { ComponentType.Maven }; + + public override int Version => 2; + + public override IEnumerable Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.Maven) }; + + [Import] + public IMavenCommandService MavenCommandService { get; set; } + + protected override async Task> OnPrepareDetection(IObservable processRequests, IDictionary detectorArgs) + { + if (!await MavenCommandService.MavenCLIExists()) + { + Logger.LogVerbose("Skipping maven detection as maven is not available in the local PATH."); + return Enumerable.Empty().ToObservable(); + } + + var processPomFile = new ActionBlock(MavenCommandService.GenerateDependenciesFile); + + await RemoveNestedPomXmls(processRequests).ForEachAsync(processRequest => + { + processPomFile.Post(processRequest); + }); + + processPomFile.Complete(); + + await processPomFile.Completion; + + return ComponentStreamEnumerableFactory.GetComponentStreams(CurrentScanRequest.SourceDirectory, new[] { MavenCommandService.BcdeMvnDependencyFileName }, CurrentScanRequest.DirectoryExclusionPredicate) + .Select(componentStream => + { + // The file stream is going to be disposed after the iteration is finished + // so is necessary to read the content and keep it in memory, for further processing. + using var reader = new StreamReader(componentStream.Stream); + var content = reader.ReadToEnd(); + return new ProcessRequest + { + ComponentStream = new ComponentStream + { + Stream = new MemoryStream(Encoding.UTF8.GetBytes(content)), + Location = componentStream.Location, + Pattern = componentStream.Pattern, + }, + SingleFileComponentRecorder = ComponentRecorder.CreateSingleFileComponentRecorder( + Path.Combine(Path.GetDirectoryName(componentStream.Location), "pom.xml")), + }; + }) + .ToObservable(); + } + + protected override async Task OnFileFound(ProcessRequest processRequest, IDictionary detectorArgs) + { + MavenCommandService.ParseDependenciesFile(processRequest); + + File.Delete(processRequest.ComponentStream.Location); + + await Task.CompletedTask; + } + + private IObservable RemoveNestedPomXmls(IObservable componentStreams) + { + List directoryItemFacades = new List(); + Dictionary directoryItemFacadesByPath = new Dictionary(); + return Observable.Create(s => + { + return componentStreams.Subscribe( + processRequest => + { + var item = processRequest.ComponentStream; + string currentDir = item.Location; + DirectoryItemFacade last = null; + do + { + currentDir = Path.GetDirectoryName(currentDir); + + // We've reached the top / root + if (currentDir == null) + { + // If our last directory isn't in our list of top level nodes, it should be added. This happens for the first processed item and then subsequent times we have a new root (edge cases with multiple hard drives, for example) + if (!directoryItemFacades.Contains(last)) + { + directoryItemFacades.Add(last); + } + + // If we got to the top without finding a directory that had a pom.xml on the way, we yield. + s.OnNext(processRequest); + break; + } + + var directoryExisted = directoryItemFacadesByPath.TryGetValue(currentDir, out var current); + if (!directoryExisted) + { + directoryItemFacadesByPath[currentDir] = current = new DirectoryItemFacade + { + Name = currentDir, + Files = new List(), + Directories = new List(), + }; + } + + // If we came from a directory, we add it to our graph. + if (last != null) + { + current.Directories.Add(last); + } + + // If we didn't come from a directory, it's because we're just getting started. Our current directory should include the file that led to it showing up in the graph. + else + { + current.Files.Add(item); + } + + if (last != null && current.Files.FirstOrDefault(x => string.Equals(Path.GetFileName(x.Location), "pom.xml", StringComparison.OrdinalIgnoreCase)) != null) + { + Logger.LogVerbose($"Ignoring pom.xml at {item.Location}, as it has a parent pom.xml that will be processed at {current.Name}\\pom.xml ."); + break; + } + + last = current; + } + + // Go all the way up + while (currentDir != null); + }, s.OnCompleted); + }); + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/npm/NpmComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/npm/NpmComponentDetector.cs new file mode 100644 index 000000000..334e7a27b --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/npm/NpmComponentDetector.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Composition; +using System.IO; +using System.Threading.Tasks; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.Internal; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Newtonsoft.Json.Linq; +using NuGet.Versioning; + +namespace Microsoft.ComponentDetection.Detectors.Npm +{ + [Export(typeof(IComponentDetector))] + public class NpmComponentDetector : FileComponentDetector + { + public override string Id { get; } = "Npm"; + + public override IEnumerable Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.Npm) }; + + public override IList SearchPatterns { get; } = new List { "package.json" }; + + public override IEnumerable SupportedComponentTypes { get; } = new[] { ComponentType.Npm }; + + public override int Version { get; } = 2; + + /// Common delegate for Package.json JToken processing. + /// A JToken, usually corresponding to a package.json file. + /// Used in scenarios where one file path creates multiple JTokens, a false value indicates processing additional JTokens should be halted, proceed otherwise. + protected delegate bool JTokenProcessingDelegate(JToken token); + + protected override async Task OnFileFound(ProcessRequest processRequest, IDictionary detectorArgs) + { + var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder; + var file = processRequest.ComponentStream; + + var filePath = file.Location; + + string contents; + using (var reader = new StreamReader(file.Stream)) + { + contents = await reader.ReadToEndAsync(); + } + + await SafeProcessAllPackageJTokens(filePath, contents, (token) => + { + if (token["name"] == null || token["version"] == null) + { + Logger.LogInfo($"{filePath} does not contain a name and/or version. These are required fields for a valid package.json file." + + $"It and its dependencies will not be registered."); + return false; + } + + return ProcessIndividualPackageJTokens(filePath, singleFileComponentRecorder, token); + }); + } + + private async Task SafeProcessAllPackageJTokens(string sourceFilePath, string contents, JTokenProcessingDelegate jtokenProcessor) + { + try + { + await ProcessAllPackageJTokensAsync(contents, jtokenProcessor); + } + catch (Exception e) + { + // If something went wrong, just ignore the component + Logger.LogBuildWarning($"Could not parse Jtokens from file {sourceFilePath}."); + Logger.LogFailedReadingFile(sourceFilePath, e); + return; + } + } + + protected virtual Task ProcessAllPackageJTokensAsync(string contents, JTokenProcessingDelegate jtokenProcessor) + { + var o = JToken.Parse(contents); + jtokenProcessor(o); + return Task.CompletedTask; + } + + protected virtual bool ProcessIndividualPackageJTokens(string filePath, ISingleFileComponentRecorder singleFileComponentRecorder, JToken packageJToken) + { + var name = packageJToken["name"].ToString(); + var version = packageJToken["version"].ToString(); + + if (!SemanticVersion.TryParse(version, out _)) + { + Logger.LogWarning($"Unable to parse version \"{version}\" for package \"{name}\" found at path \"{filePath}\". This may indicate an invalid npm package component and it will not be registered."); + return false; + } + + var detectedComponent = new DetectedComponent(new NpmComponent(name, version)); + singleFileComponentRecorder.RegisterUsage(detectedComponent); + return true; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Detectors/npm/NpmComponentDetectorWithRoots.cs b/src/Microsoft.ComponentDetection.Detectors/npm/NpmComponentDetectorWithRoots.cs new file mode 100644 index 000000000..58a9677fb --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/npm/NpmComponentDetectorWithRoots.cs @@ -0,0 +1,339 @@ +using System; +using System.Collections.Generic; +using System.Composition; +using System.IO; +using System.Linq; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Microsoft.ComponentDetection.Common; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.Internal; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.ComponentDetection.Detectors.Npm +{ + [Export(typeof(IComponentDetector))] + public class NpmComponentDetectorWithRoots : FileComponentDetector + { + /// Gets or sets the logger for writing basic logging message to both console and file. Injected automatically by MEF composition. + [Import] + public IPathUtilityService PathUtilityService { get; set; } + + public override string Id { get; } = "NpmWithRoots"; + + public override IEnumerable Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.Npm) }; + + public override IList SearchPatterns { get; } = new List { "package-lock.json", "npm-shrinkwrap.json", LernaSearchPattern }; + + public override IEnumerable SupportedComponentTypes { get; } = new[] { ComponentType.Npm }; + + public override int Version { get; } = 2; + + private const string NpmRegistryHost = "registry.npmjs.org"; + + private readonly object lernaFilesLock = new object(); + + /// Common delegate for Package.json JToken processing. + /// A JToken, usually corresponding to a package.json file. + /// Used in scenarios where one file path creates multiple JTokens, a false value indicates processing additional JTokens should be halted, proceed otherwise. + protected delegate bool JTokenProcessingDelegate(JToken token); + + public const string LernaSearchPattern = "lerna.json"; + + public List LernaFiles { get; set; } = new List(); + + /// + protected override IList SkippedFolders => new List { "node_modules", "pnpm-store" }; + + protected override Task> OnPrepareDetection(IObservable processRequests, IDictionary detectorArgs) + { + return Task.FromResult(RemoveNodeModuleNestedFiles(processRequests) + .Where(pr => + { + if (pr.ComponentStream.Pattern.Equals(LernaSearchPattern)) + { + // Lock LernaFiles so we don't add while we are enumerating in processFiles + lock (lernaFilesLock) + { + LernaFiles.Add(pr); + return false; + } + } + + return true; + })); + } + + protected override async Task OnFileFound(ProcessRequest processRequest, IDictionary detectorArgs) + { + IEnumerable packageJsonPattern = new List { "package.json" }; + var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder; + var file = processRequest.ComponentStream; + + IEnumerable packageJsonComponentStream = ComponentStreamEnumerableFactory.GetComponentStreams(new FileInfo(file.Location).Directory, packageJsonPattern, (name, directoryName) => false, false); + + bool foundUnderLerna = false; + IList lernaFilesClone; + + // ToList LernaFiles to generate a copy we can act on without holding the lock for a long time + lock (lernaFilesLock) + { + lernaFilesClone = LernaFiles.ToList(); + } + + foreach (var lernaProcessRequest in lernaFilesClone) + { + var lernaFile = lernaProcessRequest.ComponentStream; + + // We have extra validation on lock files not found below a lerna.json + if (PathUtilityService.IsFileBelowAnother(lernaFile.Location, file.Location)) + { + foundUnderLerna = true; + break; + } + } + + await SafeProcessAllPackageJTokens(file, (token) => + { + if (!foundUnderLerna && (token["name"] == null || token["version"] == null || string.IsNullOrWhiteSpace(token["name"].Value()) || string.IsNullOrWhiteSpace(token["version"].Value()))) + { + Logger.LogInfo($"{file.Location} does not contain a valid name and/or version. These are required fields for a valid package-lock.json file." + + $"It and its dependencies will not be registered."); + return false; + } + + ProcessIndividualPackageJTokens(singleFileComponentRecorder, token, packageJsonComponentStream, skipValidation: foundUnderLerna); + return true; + }); + } + + private IObservable RemoveNodeModuleNestedFiles(IObservable componentStreams) + { + List directoryItemFacades = new List(); + Dictionary directoryItemFacadesByPath = new Dictionary(); + + return Observable.Create(s => + { + return componentStreams.Subscribe( + processRequest => + { + var item = processRequest.ComponentStream; + string currentDir = item.Location; + DirectoryItemFacade last = null; + do + { + currentDir = PathUtilityService.GetParentDirectory(currentDir); + + // We've reached the top / root + if (currentDir == null) + { + // If our last directory isn't in our list of top level nodes, it should be added. This happens for the first processed item and then subsequent times we have a new root (edge cases with multiple hard drives, for example) + if (!directoryItemFacades.Contains(last)) + { + directoryItemFacades.Add(last); + } + + string skippedFolder = SkippedFolders.FirstOrDefault(folder => item.Location.Contains(folder)); + + // When node_modules is part of the path down to a given item, we skip the item. Otherwise, we yield the item. + if (string.IsNullOrEmpty(skippedFolder)) + { + s.OnNext(processRequest); + } + else + { + Logger.LogVerbose($"Ignoring package-lock.json at {item.Location}, as it is inside a {skippedFolder} folder."); + } + + break; + } + + var directoryExisted = directoryItemFacadesByPath.TryGetValue(currentDir, out var current); + if (!directoryExisted) + { + directoryItemFacadesByPath[currentDir] = current = new DirectoryItemFacade + { + Name = currentDir, + Files = new List(), + Directories = new List(), + }; + } + + // If we came from a directory, we add it to our graph. + if (last != null) + { + current.Directories.Add(last); + } + + // If we didn't come from a directory, it's because we're just getting started. Our current directory should include the file that led to it showing up in the graph. + else + { + current.Files.Add(item); + } + + last = current; + } + + // Go all the way up + while (currentDir != null); + }, s.OnCompleted); + }); + } + + private async Task SafeProcessAllPackageJTokens(IComponentStream componentStream, JTokenProcessingDelegate jtokenProcessor) + { + try + { + await ProcessAllPackageJTokensAsync(componentStream, jtokenProcessor); + } + catch (Exception e) + { + // If something went wrong, just ignore the component + Logger.LogBuildWarning($"Could not parse Jtokens from {componentStream.Location} file."); + Logger.LogFailedReadingFile(componentStream.Location, e); + return; + } + } + + protected Task ProcessAllPackageJTokensAsync(IComponentStream componentStream, JTokenProcessingDelegate jtokenProcessor) + { + try + { + if (!componentStream.Stream.CanRead) + { + componentStream.Stream.ReadByte(); + } + } + catch (Exception ex) + { + Logger.LogBuildWarning($"Could not read {componentStream.Location} file."); + Logger.LogFailedReadingFile(componentStream.Location, ex); + return Task.CompletedTask; + } + + using StreamReader file = new StreamReader(componentStream.Stream); + using JsonTextReader reader = new JsonTextReader(file); + + var o = JToken.ReadFrom(reader); + jtokenProcessor(o); + return Task.CompletedTask; + } + + protected void ProcessIndividualPackageJTokens(ISingleFileComponentRecorder singleFileComponentRecorder, JToken packageLockJToken, IEnumerable packageJsonComponentStream, bool skipValidation = false) + { + JToken dependencies = packageLockJToken["dependencies"]; + Queue<(JProperty, TypedComponent)> topLevelDependencies = new Queue<(JProperty, TypedComponent)>(); + + var dependencyLookup = dependencies.Children().ToDictionary(dependency => dependency.Name); + + foreach (var stream in packageJsonComponentStream) + { + using StreamReader file = new StreamReader(stream.Stream); + using JsonTextReader reader = new JsonTextReader(file); + + var packageJsonToken = JToken.ReadFrom(reader); + bool enqueued = TryEnqueueFirstLevelDependencies(topLevelDependencies, packageJsonToken["dependencies"], dependencyLookup, skipValidation: skipValidation); + enqueued = enqueued && TryEnqueueFirstLevelDependencies(topLevelDependencies, packageJsonToken["devDependencies"], dependencyLookup, skipValidation: skipValidation); + if (!enqueued) + { + // This represents a mismatch between lock file and package.json, break out and do not register anything for these files + throw new InvalidOperationException(string.Format("InvalidPackageJson -- There was a mismatch between the components in the package.json and the lock file at '{0}'", singleFileComponentRecorder.ManifestFileLocation)); + } + } + + if (!packageJsonComponentStream.Any()) + { + throw new InvalidOperationException(string.Format("InvalidPackageJson -- There must be a package.json file at '{0}' for components to be registered", singleFileComponentRecorder.ManifestFileLocation)); + } + + TraverseRequirementAndDependencyTree(topLevelDependencies, dependencyLookup, singleFileComponentRecorder); + } + + private void TraverseRequirementAndDependencyTree(IEnumerable<(JProperty, TypedComponent)> topLevelDependencies, IDictionary dependencyLookup, ISingleFileComponentRecorder singleFileComponentRecorder) + { + // iterate through everything for a top level dependency with a single explicitReferencedDependency value + foreach (var (currentDependency, _) in topLevelDependencies) + { + var typedComponent = NpmComponentUtilities.GetTypedComponent(currentDependency, NpmRegistryHost, Logger); + if (typedComponent == null) + { + continue; + } + + HashSet previouslyAddedComponents = new HashSet { typedComponent.Id }; + Queue<(JProperty, TypedComponent)> subQueue = new Queue<(JProperty, TypedComponent)>(); + + NpmComponentUtilities.TraverseAndRecordComponents(currentDependency, singleFileComponentRecorder, typedComponent, explicitReferencedDependency: typedComponent); + EnqueueDependencies(subQueue, currentDependency.Value["dependencies"], parentComponent: typedComponent); + TryEnqueueFirstLevelDependencies(subQueue, currentDependency.Value["requires"], dependencyLookup, parentComponent: typedComponent); + + while (subQueue.Count != 0) + { + var (currentSubDependency, parentComponent) = subQueue.Dequeue(); + + var typedSubComponent = NpmComponentUtilities.GetTypedComponent(currentSubDependency, NpmRegistryHost, Logger); + + // only process components that we haven't seen before that have been brought in by the same explicitReferencedDependency, resolves circular npm 'requires' loop + if (typedSubComponent == null || previouslyAddedComponents.Contains(typedSubComponent.Id)) + { + continue; + } + + previouslyAddedComponents.Add(typedSubComponent.Id); + + NpmComponentUtilities.TraverseAndRecordComponents(currentSubDependency, singleFileComponentRecorder, typedSubComponent, explicitReferencedDependency: typedComponent, parentComponent.Id); + + EnqueueDependencies(subQueue, currentSubDependency.Value["dependencies"], parentComponent: typedSubComponent); + TryEnqueueFirstLevelDependencies(subQueue, currentSubDependency.Value["requires"], dependencyLookup, parentComponent: typedSubComponent); + } + } + } + + private void EnqueueDependencies(Queue<(JProperty, TypedComponent)> queue, JToken dependencies, TypedComponent parentComponent) + { + if (dependencies != null) + { + foreach (JProperty dependency in dependencies) + { + if (dependency != null) + { + queue.Enqueue((dependency, parentComponent)); + } + } + } + } + + private bool TryEnqueueFirstLevelDependencies(Queue<(JProperty, TypedComponent)> queue, JToken dependencies, IDictionary dependencyLookup, Queue parentComponentQueue = null, TypedComponent parentComponent = null, bool skipValidation = false) + { + bool isValid = true; + if (dependencies != null) + { + foreach (JProperty dependency in dependencies) + { + if (dependency == null || dependency.Name == null) + { + continue; + } + + bool inLock = dependencyLookup.TryGetValue(dependency.Name, out JProperty dependencyProperty); + if (inLock) + { + queue.Enqueue((dependencyProperty, parentComponent)); + } + else if (skipValidation) + { + continue; + } + else + { + isValid = false; + } + } + } + + return isValid; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Detectors/npm/NpmComponentUtilities.cs b/src/Microsoft.ComponentDetection.Detectors/npm/NpmComponentUtilities.cs new file mode 100644 index 000000000..8e8792614 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/npm/NpmComponentUtilities.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NuGet.Versioning; + +namespace Microsoft.ComponentDetection.Detectors.Npm +{ + public static class NpmComponentUtilities + { + private static readonly Regex UnsafeCharactersRegex = new Regex( + @"[?<>#%{}|`'^\\~\[\]""\s\x7f]|[\x00-\x1f]|[\x80-\xff]", + RegexOptions.Compiled); + + public static void TraverseAndRecordComponents(JProperty currentDependency, ISingleFileComponentRecorder singleFileComponentRecorder, TypedComponent component, TypedComponent explicitReferencedDependency, string parentComponentId = null) + { + JValue devJValue = currentDependency.Value["dev"] as JValue; + bool isDevDependency = devJValue == null ? false : (bool)devJValue; + AddOrUpdateDetectedComponent(singleFileComponentRecorder, component, isDevDependency, parentComponentId, isExplicitReferencedDependency: string.Equals(component.Id, explicitReferencedDependency.Id)); + } + + public static DetectedComponent AddOrUpdateDetectedComponent( + ISingleFileComponentRecorder singleFileComponentRecorder, + TypedComponent component, + bool isDevDependency, + string parentComponentId = null, + bool isExplicitReferencedDependency = false) + { + var newComponent = new DetectedComponent(component); + singleFileComponentRecorder.RegisterUsage(newComponent, isExplicitReferencedDependency, parentComponentId, isDevDependency); + return singleFileComponentRecorder.GetComponent(component.Id); + } + + public static TypedComponent GetTypedComponent(JProperty currentDependency, string npmRegistryHost, ILogger logger) + { + string name = currentDependency.Name; + string version = currentDependency.Value["version"].ToString(); + string hash = currentDependency.Value["integrity"]?.ToString(); // https://docs.npmjs.com/configuring-npm/package-lock-json.html#integrity + + if (!IsPackageNameValid(name)) + { + logger.LogInfo($"The package name {name} is invalid or unsupported and a component will not be recorded."); + return null; + } + + if (!SemanticVersion.TryParse(version, out SemanticVersion result) && !TryParseNpmVersion(npmRegistryHost, name, version, out result)) + { + logger.LogInfo($"Version string {version} for component {name} is invalid or unsupported and a component will not be recorded."); + return null; + } + + version = result.ToString(); + TypedComponent component = new NpmComponent(name, version, hash); + return component; + } + + public static bool TryParseNpmVersion(string npmRegistryHost, string packageName, string versionString, out SemanticVersion version) + { + if (Uri.TryCreate(versionString, UriKind.Absolute, out Uri parsedUri)) + { + if (string.Equals(npmRegistryHost, parsedUri.Host, StringComparison.OrdinalIgnoreCase)) + { + return TryParseNpmRegistryVersion(packageName, parsedUri, out version); + } + } + + version = null; + return false; + } + + public static bool TryParseNpmRegistryVersion(string packageName, Uri versionString, out SemanticVersion version) + { + try + { + string file = Path.GetFileNameWithoutExtension(versionString.LocalPath); + string potentialVersion = file.Replace($"{packageName}-", string.Empty); + + return SemanticVersion.TryParse(potentialVersion, out version); + } + catch (Exception) + { + version = null; + return false; + } + } + + public static IDictionary> TryGetAllPackageJsonDependencies(Stream stream, out IList yarnWorkspaces) + { + yarnWorkspaces = new List(); + + using StreamReader file = new StreamReader(stream); + using JsonTextReader reader = new JsonTextReader(file); + + IDictionary dependencies = new Dictionary(); + IDictionary devDependencies = new Dictionary(); + + var o = JToken.ReadFrom(reader); + + if (o["private"] != null && o["private"].Value() && o["workspaces"] != null) + { + if (o["workspaces"] is JArray) + { + yarnWorkspaces = o["workspaces"].Values().ToList(); + } + else if (o["workspaces"] is JObject && o["workspaces"]["packages"] != null && o["workspaces"]["packages"] is JArray) + { + yarnWorkspaces = o["workspaces"]["packages"].Values().ToList(); + } + } + + dependencies = PullDependenciesFromJToken(o, "dependencies"); + dependencies = dependencies.Concat(PullDependenciesFromJToken(o, "optionalDependencies")).ToDictionary(x => x.Key, x => x.Value); + devDependencies = PullDependenciesFromJToken(o, "devDependencies"); + + var returnedDependencies = AttachDevInformationToDependencies(dependencies, false); + return returnedDependencies.Concat(AttachDevInformationToDependencies(devDependencies, true)).GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.First().Value); + } + + private static IDictionary> AttachDevInformationToDependencies(IDictionary dependencies, bool isDev) + { + IDictionary> returnedDependencies = new Dictionary>(); + + foreach (var item in dependencies) + { + if (!returnedDependencies.ContainsKey(item.Key)) + { + returnedDependencies[item.Key] = new Dictionary(); + } + + if (returnedDependencies[item.Key].TryGetValue(item.Value, out bool wasDev)) + { + returnedDependencies[item.Key][item.Value] = wasDev && isDev; + } + else + { + returnedDependencies[item.Key].Add(item.Value, isDev); + } + } + + return returnedDependencies; + } + + private static IDictionary PullDependenciesFromJToken(JToken jObject, string dependencyType) + { + IDictionary dependencyJObject = new Dictionary(); + if (jObject[dependencyType] != null) + { + dependencyJObject = (JObject)jObject[dependencyType]; + } + + return dependencyJObject.ToDictionary(x => x.Key, x => (string)x.Value); + } + + private static bool IsPackageNameValid(string name) + { + if (Uri.TryCreate(name, UriKind.Absolute, out _)) + { + return false; + } + + return !(name.Length >= 214 + || name.StartsWith('.') + || name.StartsWith('_') + || UnsafeCharactersRegex.IsMatch(name)); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Detectors/nuget/NuGetComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/nuget/NuGetComponentDetector.cs new file mode 100644 index 000000000..89e7af193 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/nuget/NuGetComponentDetector.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections.Generic; +using System.Composition; +using System.IO; +using System.Linq; +using System.Reactive.Linq; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.Internal; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using NuGet.Versioning; + +namespace Microsoft.ComponentDetection.Detectors.NuGet +{ + [Export(typeof(IComponentDetector))] + public class NuGetComponentDetector : FileComponentDetector + { + public override string Id { get; } = "NuGet"; + + public override IEnumerable Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.NuGet) }; + + public override IList SearchPatterns { get; } = new List { "*.nupkg", "*.nuspec", NugetConfigFileName }; + + public override IEnumerable SupportedComponentTypes { get; } = new[] { ComponentType.NuGet }; + + public override int Version { get; } = 2; + + public const string NugetConfigFileName = "nuget.config"; + + private readonly IList repositoryPathKeyNames = new List { "repositorypath", "globalpackagesfolder" }; + + protected override async Task OnFileFound(ProcessRequest processRequest, IDictionary detectorArgs) + { + var stream = processRequest.ComponentStream; + bool ignoreNugetConfig = detectorArgs.TryGetValue("NuGet.IncludeRepositoryPaths", out string includeRepositoryPathsValue) && includeRepositoryPathsValue.Equals(bool.FalseString, StringComparison.OrdinalIgnoreCase); + + if (NugetConfigFileName.Equals(stream.Pattern, StringComparison.OrdinalIgnoreCase)) + { + await ProcessAdditionalDirectory(processRequest, ignoreNugetConfig); + } + else + { + await ProcessFile(processRequest); + } + } + + private async Task ProcessAdditionalDirectory(ProcessRequest processRequest, bool ignoreNugetConfig) + { + var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder; + var stream = processRequest.ComponentStream; + + if (!ignoreNugetConfig) + { + var additionalPaths = GetRepositoryPathsFromNugetConfig(stream); + Uri rootPath = new Uri(CurrentScanRequest.SourceDirectory.FullName + Path.DirectorySeparatorChar); + + foreach (var additionalPath in additionalPaths) + { + // Only paths outside of our sourceDirectory need to be added + if (!rootPath.IsBaseOf(new Uri(additionalPath.FullName + Path.DirectorySeparatorChar))) + { + Logger.LogInfo($"Found path override in nuget configuration file. Adding {additionalPath} to the package search path."); + Logger.LogWarning($"Path {additionalPath} is not rooted in the source tree. More components may be detected than expected if this path is shared across code projects."); + + Scanner.Initialize(additionalPath, (name, directoryName) => false, 1); + + await Scanner.GetFilteredComponentStreamObservable(additionalPath, SearchPatterns.Where(sp => !NugetConfigFileName.Equals(sp)), singleFileComponentRecorder.GetParentComponentRecorder()) + .ForEachAsync(async fi => await ProcessFile(fi)); + } + } + } + } + + private async Task ProcessFile(ProcessRequest processRequest) + { + var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder; + var stream = processRequest.ComponentStream; + + try + { + byte[] nuspecBytes = null; + + if ("*.nupkg".Equals(stream.Pattern, StringComparison.OrdinalIgnoreCase)) + { + nuspecBytes = await NuGetNuspecUtilities.GetNuspecBytesAsync(stream.Stream); + } + else if ("*.nuspec".Equals(stream.Pattern, StringComparison.OrdinalIgnoreCase)) + { + nuspecBytes = await NuGetNuspecUtilities.GetNuspecBytesFromNuspecStream(stream.Stream, stream.Stream.Length); + } + else + { + return; + } + + using MemoryStream nuspecStream = new MemoryStream(nuspecBytes, false); + + XmlDocument doc = new XmlDocument(); + doc.Load(nuspecStream); + + XmlNode packageNode = doc["package"]; + XmlNode metadataNode = packageNode["metadata"]; + + string name = metadataNode["id"].InnerText; + string version = metadataNode["version"].InnerText; + + if (!NuGetVersion.TryParse(version, out NuGetVersion parsedVer)) + { + Logger.LogInfo($"Version '{version}' from {stream.Location} could not be parsed as a NuGet version"); + + return; + } + + TypedComponent component = new NuGetComponent(name, version); + singleFileComponentRecorder.RegisterUsage(new DetectedComponent(component)); + } + catch (Exception e) + { + // If something went wrong, just ignore the component + Logger.LogFailedReadingFile(stream.Location, e); + } + } + + private IList GetRepositoryPathsFromNugetConfig(IComponentStream componentStream) + { + List potentialPaths = new List(); + List paths = new List(); + + try + { + // Can be made async in later versions of .net standard. + XElement root = XElement.Load(componentStream.Stream); + + var config = root.Elements().SingleOrDefault(x => x.Name == "config"); + + if (config == null) + { + return paths; + } + + foreach (var entry in config.Elements()) + { + if (entry.Attributes().Any(x => repositoryPathKeyNames.Contains(x.Value.ToLower()))) + { + string value = entry.Attributes().SingleOrDefault(x => string.Equals(x.Name.LocalName, "value", StringComparison.OrdinalIgnoreCase))?.Value; + + if (!string.IsNullOrEmpty(value)) + { + potentialPaths.Add(value); + } + } + } + } + catch + { + // Eat all exceptions related to permissions or malformed XML + return paths; + } + + foreach (var potentialPath in potentialPaths) + { + DirectoryInfo path; + + if (Path.IsPathRooted(potentialPath)) + { + path = new DirectoryInfo(Path.GetFullPath(potentialPath)); + } + else if (IsValidPath(componentStream.Location)) + { + path = new DirectoryInfo(Path.GetFullPath(Path.Combine(Path.GetDirectoryName(componentStream.Location), potentialPath))); + } + else + { + Logger.LogWarning($"Excluding discovered path {potentialPath} from location {componentStream.Location} as it could not be determined to be valid."); + continue; + } + + if (path.Exists) + { + paths.Add(path); + } + } + + return paths; + } + + /// + /// Checks to make sure a path is valid (does not have to exist). + /// + /// + /// + private bool IsValidPath(string potentialPath) + { + FileInfo fileInfo = null; + + try + { + fileInfo = new FileInfo(potentialPath); + } + catch + { + return false; + } + + if (fileInfo == null) + { + return false; + } + + return fileInfo.Exists || fileInfo.Directory.Exists; + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/nuget/NuGetNuspecUtilities.cs b/src/Microsoft.ComponentDetection.Detectors/nuget/NuGetNuspecUtilities.cs new file mode 100644 index 000000000..2058983e7 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/nuget/NuGetNuspecUtilities.cs @@ -0,0 +1,64 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.ComponentDetection.Detectors.NuGet +{ + public static class NuGetNuspecUtilities + { + // An empty zip archive file is 22 bytes long which is minumum possible for a zip archive file. + // source: https://en.wikipedia.org/wiki/Zip_(file_format)#Limits + public const int MinimumLengthForZipArchive = 22; + + public static async Task GetNuspecBytesAsync(Stream nupkgStream) + { + try + { + if (nupkgStream.Length < MinimumLengthForZipArchive) + { + throw new ArgumentException("nupkg is too small"); + } + + using var archive = new ZipArchive(nupkgStream, ZipArchiveMode.Read, true); + + // get the first entry ending in .nuspec at the root of the package + ZipArchiveEntry nuspecEntry = + archive.Entries.FirstOrDefault(x => + x.Name.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase) + && x.FullName.IndexOf('/') == -1); + + if (nuspecEntry == null) + { + throw new FileNotFoundException("No nuspec file was found"); + } + + using var nuspecStream = nuspecEntry.Open(); + + return await GetNuspecBytesFromNuspecStream(nuspecStream, nuspecEntry.Length); + } + catch (InvalidDataException ex) + { + throw ex; + } + finally + { + // make sure that no matter what we put the stream back to the beginning + nupkgStream.Seek(0, SeekOrigin.Begin); + } + } + + public static async Task GetNuspecBytesFromNuspecStream(Stream nuspecStream, long nuspecLength) + { + byte[] nuspecBytes = new byte[nuspecLength]; + int bytesReadSoFar = 0; + while (bytesReadSoFar < nuspecBytes.Length) + { + bytesReadSoFar += await nuspecStream.ReadAsync(nuspecBytes, bytesReadSoFar, nuspecBytes.Length - bytesReadSoFar); + } + + return nuspecBytes; + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/nuget/NuGetProjectModelProjectCentricComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/nuget/NuGetProjectModelProjectCentricComponentDetector.cs new file mode 100644 index 000000000..cb88bc642 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/nuget/NuGetProjectModelProjectCentricComponentDetector.cs @@ -0,0 +1,448 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Composition; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.Internal; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Newtonsoft.Json; +using NuGet.Packaging.Core; +using NuGet.ProjectModel; +using NuGet.Versioning; + +namespace Microsoft.ComponentDetection.Detectors.NuGet +{ + [Export(typeof(IComponentDetector))] + public class NuGetProjectModelProjectCentricComponentDetector : FileComponentDetector + { + public override string Id { get; } = "NuGetProjectCentric"; + + public override IEnumerable Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.NuGet) }; + + public override IList SearchPatterns { get; } = new List { "project.assets.json" }; + + public override IEnumerable SupportedComponentTypes { get; } = new[] { ComponentType.NuGet }; + + public override int Version { get; } = 1; + + public const string OmittedFrameworkComponentsTelemetryKey = "OmittedFrameworkComponents"; + + public const string ProjectDependencyType = "project"; + + private readonly ConcurrentDictionary frameworkComponentsThatWereOmmittedWithCount = new ConcurrentDictionary(); + + private readonly List netCoreFrameworkNames = new List { "Microsoft.AspNetCore.App", "Microsoft.AspNetCore.Razor.Design", "Microsoft.NETCore.App" }; + + private readonly HashSet alreadyLoggedWarnings = new HashSet(); + + [Import] + public IFileUtilityService FileUtilityService { get; set; } + + protected override Task OnFileFound(ProcessRequest processRequest, IDictionary detectorArgs) + { + try + { + var lockFile = new LockFileFormat().Read(processRequest.ComponentStream.Stream, processRequest.ComponentStream.Location); + + if (lockFile.PackageSpec == null) + { + throw new FormatException("Lockfile did not contain a PackageSpec"); + } + + HashSet frameworkComponents = GetFrameworkComponents(lockFile); + var explicitReferencedDependencies = + GetTopLevelLibraries(lockFile) + .Select(x => GetLibraryComponentWithDependencyLookup(lockFile.Libraries, x.name, x.version, x.versionRange)) + .ToList(); + var explicitlyReferencedComponentIds = + explicitReferencedDependencies + .Select(x => new NuGetComponent(x.Name, x.Version.ToNormalizedString()).Id) + .ToHashSet(); + + // Since we report projects as the location, we ignore the passed in single file recorder. + var singleFileComponentRecorder = ComponentRecorder.CreateSingleFileComponentRecorder(lockFile.PackageSpec.RestoreMetadata.ProjectPath); + foreach (var target in lockFile.Targets) + { + // This call to GetTargetLibrary is not guarded, because if this can't be resolved then something is fundamentally broken (e.g. an explicit dependency reference not being in the list of libraries) + foreach (var library in explicitReferencedDependencies.Select(x => target.GetTargetLibrary(x.Name)).Where(x => x != null)) + { + NavigateAndRegister(target, explicitlyReferencedComponentIds, singleFileComponentRecorder, library, null, frameworkComponents); + } + } + } + catch (Exception e) + { + // If something went wrong, just ignore the package + Logger.LogFailedReadingFile(processRequest.ComponentStream.Location, e); + } + + return Task.CompletedTask; + } + + protected override Task OnDetectionFinished() + { + Telemetry.Add(OmittedFrameworkComponentsTelemetryKey, JsonConvert.SerializeObject(frameworkComponentsThatWereOmmittedWithCount)); + + return Task.CompletedTask; + } + + private void NavigateAndRegister( + LockFileTarget target, + HashSet explicitlyReferencedComponentIds, + ISingleFileComponentRecorder singleFileComponentRecorder, + LockFileTargetLibrary library, + string parentComponentId, + HashSet dotnetRuntimePackageNames, + HashSet visited = null) + { + if (IsAFrameworkComponent(dotnetRuntimePackageNames, library.Name, library.Dependencies) + || library.Type == ProjectDependencyType) + { + return; + } + + if (visited == null) + { + visited = new HashSet(); + } + + var libraryComponent = new DetectedComponent(new NuGetComponent(library.Name, library.Version.ToNormalizedString())); + singleFileComponentRecorder.RegisterUsage(libraryComponent, explicitlyReferencedComponentIds.Contains(libraryComponent.Component.Id), parentComponentId); + + foreach (var dependency in library.Dependencies) + { + if (visited.Contains(dependency.Id)) + { + continue; + } + + var targetLibrary = target.GetTargetLibrary(dependency.Id); + if (targetLibrary == null) + { + // We have to exclude this case -- it looks like a bug in project.assets.json, but there are project.assets.json files that don't have a dependency library in the libraries set. + } + else + { + visited.Add(dependency.Id); + NavigateAndRegister(target, explicitlyReferencedComponentIds, singleFileComponentRecorder, targetLibrary, libraryComponent.Component.Id, dotnetRuntimePackageNames, visited); + } + } + } + + private bool IsAFrameworkComponent(HashSet frameworkComponents, string libraryName, IList dependencies = null) + { + var isAFrameworkComponent = frameworkComponents.Contains(libraryName); + + if (isAFrameworkComponent) + { + frameworkComponentsThatWereOmmittedWithCount.AddOrUpdate(libraryName, 1, (name, existing) => existing + 1); + + if (dependencies != null) + { + // Also track shallow children if this is a top level library so we have a rough count of how many things have been ommitted + root relationships + foreach (var item in dependencies) + { + frameworkComponentsThatWereOmmittedWithCount.AddOrUpdate(item.Id, 1, (name, existing) => existing + 1); + } + } + } + + return isAFrameworkComponent; + } + + private List<(string name, Version version, VersionRange versionRange)> GetTopLevelLibraries(LockFile lockFile) + { + // First, populate target frameworks -- This is the base level authoritative list of nuget packages a project has dependencies on. + var toBeFilled = new List<(string name, Version version, VersionRange versionRange)>(); + + foreach (var framework in lockFile.PackageSpec.TargetFrameworks) + { + foreach (var dependency in framework.Dependencies) + { + toBeFilled.Add((name: dependency.Name, version: null, versionRange: dependency.LibraryRange.VersionRange)); + } + } + + // Next, we need to resolve project references -- This is a little funky, because project references are only stored via path in + // project.assets.json, so we first build a list of all paths and then compare what is top level to them to resolve their + // associated library. + var projectDirectory = Path.GetDirectoryName(lockFile.PackageSpec.RestoreMetadata.ProjectPath); + var librariesWithAbsolutePath = + lockFile.Libraries.Where(x => x.Type == ProjectDependencyType) + .Select(x => (library: x, absoluteProjectPath: Path.GetFullPath(Path.Combine(projectDirectory, x.Path)))) + .ToDictionary(x => x.absoluteProjectPath, x => x.library); + + foreach (var restoreMetadataTargetFramework in lockFile.PackageSpec.RestoreMetadata.TargetFrameworks) + { + foreach (var projectReference in restoreMetadataTargetFramework.ProjectReferences) + { + if (librariesWithAbsolutePath.TryGetValue(Path.GetFullPath(projectReference.ProjectPath), out var library)) + { + toBeFilled.Add((library.Name, library.Version.Version, (VersionRange)null)); + } + } + } + + return toBeFilled; + } + + // Looks up a library in project.assets.json given a version (preferred) or version range (have to in some cases due to how project.assets.json stores things) + private LockFileLibrary GetLibraryComponentWithDependencyLookup(IList libraries, string dependencyId, Version version, VersionRange versionRange) + { + if ((version == null && versionRange == null) || (version != null && versionRange != null)) + { + throw new ArgumentException($"Either {nameof(version)} or {nameof(versionRange)} must be specified, but not both."); + } + + var matchingLibraryNames = libraries.Where(x => string.Equals(x.Name, dependencyId, StringComparison.OrdinalIgnoreCase)).ToList(); + + if (matchingLibraryNames.Count == 0) + { + throw new InvalidOperationException("Project.assets.json is malformed, no library could be found matching: " + dependencyId); + } + + LockFileLibrary matchingLibrary; + if (version != null) + { + // .Version.Version ensures we get to a nuget normalized 4 part version + matchingLibrary = matchingLibraryNames.FirstOrDefault(x => x.Version.Version.Equals(version)); + } + else + { + matchingLibrary = matchingLibraryNames.FirstOrDefault(x => versionRange.Satisfies(x.Version)); + } + + if (matchingLibrary == null) + { + matchingLibrary = matchingLibraryNames.First(); + string logMessage = $"Couldn't satisfy lookup for {(versionRange != null ? versionRange.ToNormalizedString() : version.ToString())}. Falling back to first found component for {matchingLibrary.Name}, resolving to version {matchingLibrary.Version}."; + if (!alreadyLoggedWarnings.Contains(logMessage)) + { + Logger.LogWarning(logMessage); + alreadyLoggedWarnings.Add(logMessage); + } + } + + return matchingLibrary; + } + + private HashSet GetFrameworkComponents(LockFile lockFile) + { + var frameworkDependencies = new HashSet(); + foreach (var projectFileDependencyGroup in lockFile.ProjectFileDependencyGroups) + { + var topLevelLibraries = GetTopLevelLibraries(lockFile); + foreach (var (name, version, versionRange) in topLevelLibraries) + { + if (netCoreFrameworkNames.Contains(name)) + { + frameworkDependencies.Add(name); + + foreach (var target in lockFile.Targets) + { + var matchingLibrary = target.Libraries.FirstOrDefault(x => x.Name == name); + HashSet dependencyComponents = GetDependencyComponentIds(lockFile, target, matchingLibrary.Dependencies); + frameworkDependencies.UnionWith(dependencyComponents); + } + } + } + } + + foreach (var netstandardDep in netStandardDependencies) + { + frameworkDependencies.Add(netstandardDep); + } + + return frameworkDependencies; + } + + private HashSet GetDependencyComponentIds(LockFile lockFile, LockFileTarget target, IList dependencies, HashSet visited = null) + { + visited = visited ?? new HashSet(); + HashSet currentComponents = new HashSet(); + foreach (var dependency in dependencies) + { + if (visited.Contains(dependency.Id)) + { + continue; + } + + currentComponents.Add(dependency.Id); + var libraryToExpand = target.GetTargetLibrary(dependency.Id); + if (libraryToExpand == null) + { + // We have to exclude this case -- it looks like a bug in project.assets.json, but there are project.assets.json files that don't have a dependency library in the libraries set. + } + else + { + visited.Add(dependency.Id); + currentComponents.UnionWith(GetDependencyComponentIds(lockFile, target, libraryToExpand.Dependencies, visited)); + } + } + + return currentComponents; + } + + // This list is meant to encompass all net standard dependencies, but likely contains some net core app 1.x ones, too. + // The specific guidance we got around populating this list is to do so based on creating a dotnet core 1.x app to make sure we had the complete + // set of netstandard.library files that could show up in later sdk versions. + private readonly string[] netStandardDependencies = new[] + { + "Libuv", + "Microsoft.CodeAnalysis.Analyzers", + "Microsoft.CodeAnalysis.Common", + "Microsoft.CodeAnalysis.CSharp", + "Microsoft.CodeAnalysis.VisualBasic", + "Microsoft.CSharp", + "Microsoft.DiaSymReader.Native", + "Microsoft.NETCore.DotNetHost", + "Microsoft.NETCore.DotNetHostPolicy", + "Microsoft.NETCore.DotNetHostResolver", + "Microsoft.NETCore.Jit", + "Microsoft.NETCore.Platforms", + "Microsoft.NETCore.Runtime.CoreCLR", + "Microsoft.NETCore.Targets", + "Microsoft.NETCore.Windows.ApiSets", + "Microsoft.VisualBasic", + "Microsoft.Win32.Primitives", + "Microsoft.Win32.Registry", + "NETStandard.Library", + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl", + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl", + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl", + "runtime.native.System", + "runtime.native.System.IO.Compression", + "runtime.native.System.Net.Http", + "runtime.native.System.Net.Security", + "runtime.native.System.Security.Cryptography.Apple", + "runtime.native.System.Security.Cryptography.OpenSsl", + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl", + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl", + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple", + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl", + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl", + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl", + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl", + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl", + "System.AppContext", + "System.Buffers", + "System.Collections", + "System.Collections.Concurrent", + "System.Collections.Immutable", + "System.Collections.NonGeneric", + "System.Collections.Specialized", + "System.ComponentModel", + "System.ComponentModel.Annotations", + "System.ComponentModel.EventBasedAsync", + "System.ComponentModel.Primitives", + "System.ComponentModel.TypeConverter", + "System.Console", + "System.Data.Common", + "System.Diagnostics.Contracts", + "System.Diagnostics.Debug", + "System.Diagnostics.DiagnosticSource", + "System.Diagnostics.FileVersionInfo", + "System.Diagnostics.Process", + "System.Diagnostics.StackTrace", + "System.Diagnostics.TextWriterTraceListener", + "System.Diagnostics.Tools", + "System.Diagnostics.TraceSource", + "System.Diagnostics.Tracing", + "System.Drawing.Primitives", + "System.Dynamic.Runtime", + "System.Globalization", + "System.Globalization.Calendars", + "System.Globalization.Extensions", + "System.IO", + "System.IO.Compression", + "System.IO.Compression.ZipFile", + "System.IO.FileSystem", + "System.IO.FileSystem.DriveInfo", + "System.IO.FileSystem.Primitives", + "System.IO.FileSystem.Watcher", + "System.IO.IsolatedStorage", + "System.IO.MemoryMappedFiles", + "System.IO.Pipes", + "System.IO.UnmanagedMemoryStream", + "System.Linq", + "System.Linq.Expressions", + "System.Linq.Parallel", + "System.Linq.Queryable", + "System.Net.Http", + "System.Net.HttpListener", + "System.Net.Mail", + "System.Net.NameResolution", + "System.Net.NetworkInformation", + "System.Net.Ping", + "System.Net.Primitives", + "System.Net.Requests", + "System.Net.Security", + "System.Net.ServicePoint", + "System.Net.Sockets", + "System.Net.WebClient", + "System.Net.WebHeaderCollection", + "System.Net.WebProxy", + "System.Net.WebSockets", + "System.Net.WebSockets.Client", + "System.Numerics.Vectors", + "System.ObjectModel", + "System.Reflection", + "System.Reflection.DispatchProxy", + "System.Reflection.Emit", + "System.Reflection.Emit.ILGeneration", + "System.Reflection.Emit.Lightweight", + "System.Reflection.Extensions", + "System.Reflection.Metadata", + "System.Reflection.Primitives", + "System.Reflection.TypeExtensions", + "System.Resources.Reader", + "System.Resources.ResourceManager", + "System.Resources.Writer", + "System.Runtime", + "System.Runtime.CompilerServices.VisualC", + "System.Runtime.Extensions", + "System.Runtime.Handles", + "System.Runtime.InteropServices", + "System.Runtime.InteropServices.RuntimeInformation", + "System.Runtime.Loader", + "System.Runtime.Numerics", + "System.Runtime.Serialization.Formatters", + "System.Runtime.Serialization.Json", + "System.Runtime.Serialization.Primitives", + "System.Runtime.Serialization.Xml", + "System.Security.Claims", + "System.Security.Cryptography.Algorithms", + "System.Security.Cryptography.Cng", + "System.Security.Cryptography.Csp", + "System.Security.Cryptography.Encoding", + "System.Security.Cryptography.OpenSsl", + "System.Security.Cryptography.Primitives", + "System.Security.Cryptography.X509Certificates", + "System.Security.Principal", + "System.Security.Principal.Windows", + "System.Text.Encoding", + "System.Text.Encoding.CodePages", + "System.Text.Encoding.Extensions", + "System.Text.RegularExpressions", + "System.Threading", + "System.Threading.Overlapped", + "System.Threading.Tasks", + "System.Threading.Tasks.Dataflow", + "System.Threading.Tasks.Extensions", + "System.Threading.Tasks.Parallel", + "System.Threading.Thread", + "System.Threading.ThreadPool", + "System.Threading.Timer", + "System.Web.HttpUtility", + "System.Xml.ReaderWriter", + "System.Xml.XDocument", + "System.Xml.XmlDocument", + "System.Xml.XmlSerializer", + "System.Xml.XPath", + "System.Xml.XPath.XDocument", + }; + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/pip/IPyPiClient.cs b/src/Microsoft.ComponentDetection.Detectors/pip/IPyPiClient.cs new file mode 100644 index 000000000..96f8265d4 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/pip/IPyPiClient.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Composition; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.ComponentDetection.Common.Telemetry.Records; +using Microsoft.ComponentDetection.Contracts; +using Newtonsoft.Json; +using Polly; + +[assembly: InternalsVisibleTo("Microsoft.ComponentDetection.Detectors.Tests")] + +namespace Microsoft.ComponentDetection.Detectors.Pip +{ + public interface IPyPiClient + { + Task> FetchPackageDependencies(string name, string version, PythonProjectRelease release); + + Task>> GetReleases(PipDependencySpecification spec); + } + + [Export(typeof(IPyPiClient))] + public class PyPiClient : IPyPiClient + { + [Import] + public ILogger Logger { get; set; } + + private static HttpClientHandler httpClientHandler = new HttpClientHandler() { CheckCertificateRevocationList = true }; + + internal static HttpClient HttpClient = new HttpClient(httpClientHandler); + + // time to wait before retrying a failed call to pypi.org + private static readonly TimeSpan RETRYDELAY = TimeSpan.FromSeconds(1); + + // max number of retries allowed, to cap the total delay period + private const long MAXRETRIES = 15; + + // retries used so far for calls to pypi.org + private long retries = 0; + + /// + /// This cache is used mostly for consistency, to create a unified view of Pypi response. + /// + private readonly ConcurrentDictionary> cachedResponses = new ConcurrentDictionary>(); + + /// + /// Returns a cached response if it exists, otherwise returns the response from Pypi REST call. + /// The response from Pypi is not automatically added to the cache, to allow caller to make that decision. + /// + /// The REST Uri to call. + /// The cached response or a new result from Pypi. + private async Task GetPypiResponse(string uri) + { + if (cachedResponses.TryGetValue(uri, out var value)) + { + return await value; + } + + Logger.LogInfo("Getting Python data from " + uri); + return await HttpClient.GetAsync(uri); + } + + /// + /// Used to update the consistency cache, decision has to be made by the caller to allow for retries!. + /// + /// The REST Uri to call. + /// The proposed response by the caller to store for this Uri. + /// The `first-wins` response accepted into the cache. + /// This might be different from the input if another caller wins the race!. + private async Task CachePypiResponse(string uri, HttpResponseMessage message) + { + if (!cachedResponses.TryAdd(uri, Task.FromResult(message))) + { + return await cachedResponses[uri]; + } + + return message; + } + + public async Task> FetchPackageDependencies(string name, string version, PythonProjectRelease release) + { + var dependencies = new List(); + + var uri = release.Url.ToString(); + var response = await GetPypiResponse(uri); + + response = await CachePypiResponse(uri, response); + + if (!response.IsSuccessStatusCode) + { + Logger.LogWarning($"Http GET at {release.Url} failed with status code {response.StatusCode}"); + return dependencies; + } + + ZipArchive package = new ZipArchive(await response.Content.ReadAsStreamAsync()); + + var entry = package.GetEntry($"{name.Replace('-', '_')}-{version}.dist-info/METADATA"); + + // If there is no metadata file, the package doesn't have any declared dependencies + if (entry == null) + { + return dependencies; + } + + List content = new List(); + using (var stream = entry.Open()) + { + using var streamReader = new StreamReader(stream); + + while (!streamReader.EndOfStream) + { + var line = await streamReader.ReadLineAsync(); + + if (PipDependencySpecification.RequiresDistRegex.IsMatch(line)) + { + content.Add(line); + } + } + } + + // Pull the packages that aren't conditional based on "extras" + // Right now we just want to resolve the graph as most comsumers will + // experience it + foreach (var deps in content.Where(x => !x.Contains("extra =="))) + { + dependencies.Add(new PipDependencySpecification(deps, true)); + } + + return dependencies; + } + + public async Task>> GetReleases(PipDependencySpecification spec) + { + var requestUri = $"https://pypi.org/pypi/{spec.Name}/json"; + + var request = await Policy + .HandleResult(message => + { + // stop retrying if MAXRETRIES was hit! + if (message == null) + { + return false; + } + + var statusCode = (int)message.StatusCode; + + // only retry if server doesn't classify the call as a client error! + var isRetryable = statusCode < 400 || statusCode > 499; + return !message.IsSuccessStatusCode && isRetryable; + }) + .WaitAndRetryAsync(1, i => RETRYDELAY, (result, timeSpan, retryCount, context) => + { + using var r = new PypiRetryTelemetryRecord { Name = spec.Name, DependencySpecifiers = spec.DependencySpecifiers?.ToArray(), StatusCode = result.Result.StatusCode }; + + Logger.LogWarning($"Received {(int)result.Result.StatusCode} {result.Result.ReasonPhrase} from {requestUri}. Waiting {timeSpan} before retry attempt {retryCount}"); + + Interlocked.Increment(ref retries); + }) + .ExecuteAsync(() => + { + if (Interlocked.Read(ref retries) >= MAXRETRIES) + { + return Task.FromResult(null); + } + + return GetPypiResponse(requestUri); + }); + + request = await CachePypiResponse(requestUri, request); + + if (request == null) + { + using var r = new PypiMaxRetriesReachedTelemetryRecord { Name = spec.Name, DependencySpecifiers = spec.DependencySpecifiers?.ToArray() }; + + Logger.LogWarning($"Call to pypi.org failed, but no more retries allowed!"); + + return new SortedDictionary>(); + } + + if (!request.IsSuccessStatusCode) + { + using var r = new PypiFailureTelemetryRecord { Name = spec.Name, DependencySpecifiers = spec.DependencySpecifiers?.ToArray(), StatusCode = request.StatusCode }; + + Logger.LogWarning($"Received {(int)request.StatusCode} {request.ReasonPhrase} from {requestUri}"); + + return new SortedDictionary>(); + } + + var response = await request.Content.ReadAsStringAsync(); + var project = JsonConvert.DeserializeObject(response); + var versions = new SortedDictionary>(new PythonVersionComparer()); + + foreach (var release in project.Releases) + { + try + { + var parsedVersion = new PythonVersion(release.Key); + if (release.Value != null && release.Value.Count > 0 && + parsedVersion.Valid && parsedVersion.IsReleasedPackage && + PythonVersionUtilities.VersionValidForSpec(release.Key, spec.DependencySpecifiers)) + { + versions.Add(release.Key, release.Value); + } + } + catch (ArgumentException ae) + { + Logger.LogError($"Component {release.Key} : {JsonConvert.SerializeObject(release.Value)} could not be added to the sorted list of pip components. Error details follow:"); + Logger.LogException(ae, true); + continue; + } + } + + return versions; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Detectors/pip/IPythonCommandService.cs b/src/Microsoft.ComponentDetection.Detectors/pip/IPythonCommandService.cs new file mode 100644 index 000000000..fa841fde5 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/pip/IPythonCommandService.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.ComponentDetection.Contracts.TypedComponent; + +namespace Microsoft.ComponentDetection.Detectors.Pip +{ + public interface IPythonCommandService + { + Task PythonExists(string pythonPath = null); + + Task> ParseFile(string path, string pythonPath = null); + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Detectors/pip/IPythonResolver.cs b/src/Microsoft.ComponentDetection.Detectors/pip/IPythonResolver.cs new file mode 100644 index 000000000..1a86ce8b0 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/pip/IPythonResolver.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.ComponentDetection.Detectors.Pip +{ + public interface IPythonResolver + { + /// + /// Resolves the root Python packages from the initial list of packages. + /// + /// The initial list of packages. + /// The root packages, with dependencies associated as children. + Task> ResolveRoots(IList initialPackages); + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Detectors/pip/PipComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/pip/PipComponentDetector.cs new file mode 100644 index 000000000..0bf5d371d --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/pip/PipComponentDetector.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Composition; +using System.Linq; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.Internal; +using Microsoft.ComponentDetection.Contracts.TypedComponent; + +namespace Microsoft.ComponentDetection.Detectors.Pip +{ + [Export(typeof(IComponentDetector))] + public class PipComponentDetector : FileComponentDetector + { + public override string Id => "Pip"; + + public override IList SearchPatterns => new List { "setup.py", "requirements.txt" }; + + public override IEnumerable Categories => new List { "Python" }; + + public override IEnumerable SupportedComponentTypes { get; } = new[] { ComponentType.Pip }; + + public override int Version { get; } = 4; + + [Import] + public IPythonCommandService PythonCommandService { get; set; } + + [Import] + public IPythonResolver PythonResolver { get; set; } + + protected override async Task> OnPrepareDetection(IObservable processRequests, IDictionary detectorArgs) + { + CurrentScanRequest.DetectorArgs.TryGetValue("Pip.PythonExePath", out string pythonExePath); + if (!await PythonCommandService.PythonExists(pythonExePath)) + { + Logger.LogInfo($"No python found on system. Python detection will not run."); + + return Enumerable.Empty().ToObservable(); + } + + return processRequests; + } + + protected override async Task OnFileFound(ProcessRequest processRequest, IDictionary detectorArgs) + { + CurrentScanRequest.DetectorArgs.TryGetValue("Pip.PythonExePath", out string pythonExePath); + var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder; + var file = processRequest.ComponentStream; + + try + { + var initialPackages = await PythonCommandService.ParseFile(file.Location, pythonExePath); + var listedPackage = initialPackages.Where(tuple => tuple.Item1 != null) + .Select(tuple => tuple.Item1) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => new PipDependencySpecification(x)) + .Where(x => !x.PackageIsUnsafe()) + .ToList(); + + var roots = await PythonResolver.ResolveRoots(listedPackage); + + RecordComponents( + singleFileComponentRecorder, + roots); + + initialPackages.Where(tuple => tuple.Item2 != null) + .Select(tuple => new DetectedComponent(tuple.Item2)) + .ToList() + .ForEach(gitComponent => singleFileComponentRecorder.RegisterUsage(gitComponent, isExplicitReferencedDependency: true)); +} + catch (Exception e) + { + Logger.LogFailedReadingFile(file.Location, e); + } + } + + private static void RecordComponents( + ISingleFileComponentRecorder recorder, + IList roots) + { + var nonRoots = new Queue<(DetectedComponent, PipGraphNode)>(); + + var explicitRoots = roots.Select(a => a.Value).ToHashSet(); + + foreach (var root in roots) + { + var rootDetectedComponent = new DetectedComponent(root.Value); + + recorder.RegisterUsage( + rootDetectedComponent, + isExplicitReferencedDependency: true); + + foreach (var child in root.Children) + { + nonRoots.Enqueue((rootDetectedComponent, child)); + } + } + + var registeredIds = new HashSet(); + + while (nonRoots.Count > 0) + { + var (parent, item) = nonRoots.Dequeue(); + + var detectedComponent = new DetectedComponent(item.Value); + + recorder.RegisterUsage( + detectedComponent, + parentComponentId: parent.Component.Id); + + if (!registeredIds.Contains(detectedComponent.Component.Id)) + { + foreach (var child in item.Children) + { + nonRoots.Enqueue((detectedComponent, child)); + } + + registeredIds.Add(detectedComponent.Component.Id); + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Detectors/pip/PipDependencySpecification.cs b/src/Microsoft.ComponentDetection.Detectors/pip/PipDependencySpecification.cs new file mode 100644 index 000000000..495b843cf --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/pip/PipDependencySpecification.cs @@ -0,0 +1,122 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Microsoft.ComponentDetection.Detectors.Pip +{ + /// + /// Represents a package and a list of dependency specifications that the package must be. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class PipDependencySpecification + { + private string DebuggerDisplay => $"{Name} ({string.Join(';', DependencySpecifiers)})"; + + /// + /// Gets or sets the package (ex: pyyaml). + /// + public string Name { get; set; } + + /// + /// Gets or sets the set of dependency specifications that constrain the overall dependency request (ex: ==1.0, >=2.0). + /// + public IList DependencySpecifiers { get; set; } = new List(); + + /// + /// These are packages that we don't want to evaluate in our graph as they are generally python builtins. + /// + private static readonly HashSet PackagesToIgnore = new HashSet + { + "-markerlib", + "pip", + "pip-tools", + "pip-review", + "pkg-resources", + "setuptools", + "wheel", + }; + + // Extracts abcd from a string like abcd==1.*,!=1.3 + private static readonly Regex PipNameExtractionRegex = new Regex( + @"^.+?((?=<)|(?=>)|(?=>=)|(?=<=)|(?===)|(?=!=)|(?=~=)|(?====))", + RegexOptions.Compiled); + + // Extracts ==1.*,!=1.3 from a string like abcd==1.*,!=1.3 + private static readonly Regex PipVersionExtractionRegex = new Regex( + @"((?=<)|(?=>)|(?=>=)|(?=<=)|(?===)|(?=!=)|(?=~=)|(?====))(.*)", + RegexOptions.Compiled); + + // Extracts name and version from a Requires-Dist string that is found in a metadata file + public static readonly Regex RequiresDistRegex = new Regex( + @"Requires-Dist:\s*(?:(.*?)\s*\((.*?)\)|([^\s;]*))", + RegexOptions.Compiled); + + /// + /// Whether or not the package is safe to resolve based on the packagesToIgnore. + /// + /// + public bool PackageIsUnsafe() + { + return PackagesToIgnore.Contains(Name); + } + + /// + /// This constructor is used in test code. + /// + public PipDependencySpecification() + { + } + + /// + /// Constructs a dependency specification from a string in one of two formats (Requires-Dist: a (==1.3)) OR a==1.3. + /// + /// The to parse. + /// The package format. + public PipDependencySpecification(string packageString, bool requiresDist = false) + { + if (requiresDist) + { + var distMatch = RequiresDistRegex.Match(packageString); + + for (int i = 1; i < distMatch.Groups.Count; i++) + { + if (string.IsNullOrWhiteSpace(distMatch.Groups[i].Value)) + { + continue; + } + + if (string.IsNullOrWhiteSpace(Name)) + { + Name = distMatch.Groups[i].Value; + } + else + { + DependencySpecifiers = distMatch.Groups[i].Value.Split(','); + } + } + } + else + { + var nameMatches = PipNameExtractionRegex.Match(packageString); + var versionMatches = PipVersionExtractionRegex.Match(packageString); + + if (nameMatches.Captures.Count > 0) + { + Name = nameMatches.Captures[0].Value; + } + else + { + Name = packageString; + } + + if (versionMatches.Captures.Count > 0) + { + DependencySpecifiers = versionMatches.Captures[0].Value.Split(','); + } + } + + DependencySpecifiers = DependencySpecifiers.Where(x => !x.Contains("python_version")).ToList(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Detectors/pip/PipGraphNode.cs b/src/Microsoft.ComponentDetection.Detectors/pip/PipGraphNode.cs new file mode 100644 index 000000000..22606f725 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/pip/PipGraphNode.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using Microsoft.ComponentDetection.Contracts.TypedComponent; + +namespace Microsoft.ComponentDetection.Detectors.Pip +{ + /// + /// Internal state used by PipDetector to hold intermediate structure info until the final + /// combination of dependencies and relationships is determined and can be returned. + /// + public class PipGraphNode + { + public PipGraphNode(PipComponent value) + { + Value = value; + } + + public PipComponent Value { get; set; } + + public List Children { get; } = new List(); + + public List Parents { get; } = new List(); + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Detectors/pip/PythonCommandService.cs b/src/Microsoft.ComponentDetection.Detectors/pip/PythonCommandService.cs new file mode 100644 index 000000000..5ff88f1f2 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/pip/PythonCommandService.cs @@ -0,0 +1,146 @@ +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Microsoft.ComponentDetection.Contracts; +using System; +using System.Collections.Generic; +using System.Composition; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.IO; + +namespace Microsoft.ComponentDetection.Detectors.Pip +{ + [Export(typeof(IPythonCommandService))] + public class PythonCommandService : IPythonCommandService + { + [Import] + public ICommandLineInvocationService CommandLineInvocationService { get; set; } + + public async Task PythonExists(string pythonPath = null) + { + return !string.IsNullOrEmpty(await ResolvePython(pythonPath)); + } + + public async Task> ParseFile(string filePath, string pythonPath = null) + { + if (string.IsNullOrEmpty(filePath)) + { + return new List<(string, GitComponent)>(); + } + + if (filePath.EndsWith(".py")) + { + return (await ParseSetupPyFile(filePath, pythonPath)) + .Select(component => (component, null)) + .ToList(); + } + else if (filePath.EndsWith(".txt")) + { + return ParseRequirementsTextFile(filePath); + } + else + { + return new List<(string, GitComponent)>(); + } + } + + private async Task> ParseSetupPyFile(string filePath, string pythonExePath = null) + { + var pythonExecutable = await ResolvePython(pythonExePath); + + if (string.IsNullOrEmpty(pythonExecutable)) + { + throw new PythonNotFoundException(); + } + + // This calls out to python and prints out an array like: [ packageA, packageB, packageC ] + // We need to have python interpret this file because install_requires can be composed at runtime + var command = await CommandLineInvocationService.ExecuteCommand(pythonExecutable, null, $"-c \"import distutils.core; setup=distutils.core.run_setup('{filePath.Replace('\\', '/')}'); print(setup.install_requires)\""); + + if (command.ExitCode != 0) + { + return new List(); + } + + var result = command.StdOut; + + result = result.Trim('[', ']', '\r', '\n').Trim(); + + return result.Split(new string[] { "'," }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim().Trim('\'').Trim()).ToList(); + } + + private IList<(string, GitComponent)> ParseRequirementsTextFile(string path) + { + var items = new List<(string, GitComponent)>(); + foreach (var line in File.ReadAllLines(path).Select(x => x.Trim().TrimEnd('\\')).Where(x => !x.StartsWith("#") && !x.StartsWith("-") && !string.IsNullOrWhiteSpace(x))) + { + // We technically shouldn't be ignoring information after the ; + // It's used to indicate environment markers like specific python versions + // https://www.python.org/dev/peps/pep-0508/#environment-markers + var toAdd = line.Split(';')[0].Trim(); + var url = toAdd.Split(' ')[0]; + + if (url.StartsWith("git+") && Uri.IsWellFormedUriString(url, UriKind.Absolute)) + { + // A (potentially non exhaustive) list of possible Url formats + // git+git://github.com/path/to/package-two@41b95ec#egg=package-two + // git+git://github.com/path/to/package-two@master#egg=package-two + // git+git://github.com/path/to/package-two@0.1#egg=package-two + // git+git://github.com/path/to/package-two@releases/tag/v3.7.1#egg=package-two + // Source: https://stackoverflow.com/questions/16584552/how-to-state-in-requirements-txt-a-direct-github-source + // Since it's possible not to have a commit id for the git component + // We're just going to skip it instead of doing something weird + var parsedUrl = new Uri(url); + var pathParts = parsedUrl.PathAndQuery.Split("@"); + if (pathParts.Length < 2) + { + // This is no bueno + continue; + } + + var repoProject = pathParts[0]; + + var packageParts = pathParts[1]; + var possibleCommit = packageParts.Split("#")[0]; + + // A best effort attempt to see if we're working with something that _could_ be a commit hash + var shortCommitHash = 7; + var fullCommitHash = 40; + var hexRegex = new Regex("([a-z]|[A-Z]|[0-9])+"); + + if ((possibleCommit.Length == shortCommitHash || possibleCommit.Length == fullCommitHash) + && hexRegex.IsMatch(possibleCommit)) + { + var gitComponent = new GitComponent(new Uri($"https://{parsedUrl.Host}{repoProject}"), possibleCommit); + items.Add((null, gitComponent)); + } + } + else + { + toAdd = toAdd.Split("#")[0]; // Remove comment from the line that contains the component name and version. + toAdd = toAdd.Replace(" ", string.Empty); + items.Add((toAdd, null)); + } + } + + return items; + } + + private async Task ResolvePython(string pythonPath = null) + { + string pythonCommand = string.IsNullOrEmpty(pythonPath) ? "python" : pythonPath; + + if (await CanCommandBeLocated(pythonCommand)) + { + return pythonCommand; + } + + return null; + } + + private async Task CanCommandBeLocated(string pythonPath) + { + return await CommandLineInvocationService.CanCommandBeLocated(pythonPath, new List { "python3", "python2" }, "--version"); + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/pip/PythonNotFoundException.cs b/src/Microsoft.ComponentDetection.Detectors/pip/PythonNotFoundException.cs new file mode 100644 index 000000000..eacc9be3e --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/pip/PythonNotFoundException.cs @@ -0,0 +1,8 @@ +using System; + +namespace Microsoft.ComponentDetection.Detectors.Pip +{ + public class PythonNotFoundException : Exception + { + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Detectors/pip/PythonProject.cs b/src/Microsoft.ComponentDetection.Detectors/pip/PythonProject.cs new file mode 100644 index 000000000..4664e04a2 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/pip/PythonProject.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace Microsoft.ComponentDetection.Detectors.Pip +{ + /// + /// A project on pypi. + /// + public class PythonProject + { + public Dictionary> Releases { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Detectors/pip/PythonProjectRelease.cs b/src/Microsoft.ComponentDetection.Detectors/pip/PythonProjectRelease.cs new file mode 100644 index 000000000..d6a9c624f --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/pip/PythonProjectRelease.cs @@ -0,0 +1,20 @@ +using System; +using Newtonsoft.Json; + +namespace Microsoft.ComponentDetection.Detectors.Pip +{ + /// + /// A specific release of a project on pypy. + /// + public class PythonProjectRelease + { + public string PackageType { get; set; } + + [JsonProperty("python_version")] + public string PythonVersion { get; set; } + + public double Size { get; set; } + + public Uri Url { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Detectors/pip/PythonResolver.cs b/src/Microsoft.ComponentDetection.Detectors/pip/PythonResolver.cs new file mode 100644 index 000000000..2444149f9 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/pip/PythonResolver.cs @@ -0,0 +1,221 @@ +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using System; +using System.Collections.Generic; +using System.Composition; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.ComponentDetection.Detectors.Pip +{ + [Export(typeof(IPythonResolver))] + public class PythonResolver : IPythonResolver + { + [Import] + public IPyPiClient PypiClient { get; set; } + + [Import] + public ILogger Logger { get; set; } + + /// + /// Resolves the root Python packages from the initial list of packages. + /// + /// The initial list of packages. + /// The root packages, with dependencies associated as children. + public async Task> ResolveRoots(IList initialPackages) + { + var state = new PythonResolverState(); + + // Fill the dictionary with valid packages for the roots + foreach (var rootPackage in initialPackages) + { + // If we have it, we probably just want to skip at this phase as this indicates duplicates + if (!state.ValidVersionMap.TryGetValue(rootPackage.Name, out _)) + { + var result = await PypiClient.GetReleases(rootPackage); + + if (result.Keys.Any()) + { + state.ValidVersionMap[rootPackage.Name] = result; + + // Grab the latest version as our candidate version + var candidateVersion = state.ValidVersionMap[rootPackage.Name].Keys.Any() + ? state.ValidVersionMap[rootPackage.Name].Keys.Last() : null; + + var node = new PipGraphNode(new PipComponent(rootPackage.Name, candidateVersion)); + + state.NodeReferences[rootPackage.Name] = node; + + state.Roots.Add(node); + + state.ProcessingQueue.Enqueue((rootPackage.Name, rootPackage)); + } + else + { + Logger.LogWarning($"Root dependency {rootPackage.Name} not found on pypi. Skipping package."); + } + } + } + + // Now queue packages for processing + return await ProcessQueue(state) ?? new List(); + } + + private async Task> ProcessQueue(PythonResolverState state) + { + while (state.ProcessingQueue.Count > 0) + { + var (root, currentNode) = state.ProcessingQueue.Dequeue(); + + // gather all dependencies for the current node + var dependencies = (await FetchPackageDependencies(state, currentNode)).Where(x => !x.PackageIsUnsafe()); + + foreach (var dependencyNode in dependencies) + { + // if we have already seen the dependency and the version we have is valid, just add the dependency to the graph + if (state.NodeReferences.TryGetValue(dependencyNode.Name, out var node) && + PythonVersionUtilities.VersionValidForSpec(((PipComponent)node.Value).Version, dependencyNode.DependencySpecifiers)) + { + state.NodeReferences[currentNode.Name].Children.Add(node); + node.Parents.Add(state.NodeReferences[currentNode.Name]); + } + else if (node != null) + { + Logger.LogWarning($"Candidate version ({node.Value.Id}) for {dependencyNode.Name} already exists in map and the version is NOT valid."); + Logger.LogWarning($"Specifiers: {string.Join(',', dependencyNode.DependencySpecifiers)} for package {currentNode.Name} caused this."); + + // The currently selected version is invalid, try to see if there is another valid version available + if (!await InvalidateAndReprocessAsync(state, node, dependencyNode)) + { + Logger.LogWarning($"Version Resolution for {dependencyNode.Name} failed, assuming last valid version is used."); + + // there is no valid version available for the node, dependencies are incompatible, + } + } + else + { + // We haven't encountered this package before, so let's fetch it and find a candidate + var result = await PypiClient.GetReleases(dependencyNode); + + if (result.Keys.Any()) + { + state.ValidVersionMap[dependencyNode.Name] = result; + var candidateVersion = state.ValidVersionMap[dependencyNode.Name].Keys.Any() + ? state.ValidVersionMap[dependencyNode.Name].Keys.Last() : null; + + AddGraphNode(state, state.NodeReferences[currentNode.Name], dependencyNode.Name, candidateVersion); + + state.ProcessingQueue.Enqueue((root, dependencyNode)); + } + else + { + Logger.LogWarning($"Dependency Package {dependencyNode.Name} not found in Pypi. Skipping package"); + } + } + } + } + + return state.Roots; + } + + private async Task InvalidateAndReprocessAsync( + PythonResolverState state, + PipGraphNode node, + PipDependencySpecification newSpec) + { + var pipComponent = (PipComponent)node.Value; + + var oldVersions = state.ValidVersionMap[pipComponent.Name].Keys.ToList(); + var currentSelectedVersion = ((PipComponent)node.Value).Version; + var currentReleases = state.ValidVersionMap[pipComponent.Name][currentSelectedVersion]; + foreach (var version in oldVersions) + { + if (!PythonVersionUtilities.VersionValidForSpec(version, newSpec.DependencySpecifiers)) + { + state.ValidVersionMap[pipComponent.Name].Remove(version); + } + } + + if (state.ValidVersionMap[pipComponent.Name].Count == 0) + { + state.ValidVersionMap[pipComponent.Name][currentSelectedVersion] = currentReleases; + return false; + } + + var candidateVersion = state.ValidVersionMap[pipComponent.Name].Keys.Any() ? state.ValidVersionMap[pipComponent.Name].Keys.Last() : null; + + node.Value = new PipComponent(pipComponent.Name, candidateVersion); + + var dependencies = (await FetchPackageDependencies(state, newSpec)).ToDictionary(x => x.Name, x => x); + + var toRemove = new List(); + foreach (var child in node.Children) + { + var pipChild = (PipComponent)child.Value; + + if (!dependencies.TryGetValue(pipChild.Name, out var newDependency)) + { + toRemove.Add(child); + } + else if (!PythonVersionUtilities.VersionValidForSpec(pipChild.Version, newDependency.DependencySpecifiers)) + { + if (!await InvalidateAndReprocessAsync(state, child, newDependency)) + { + return false; + } + } + } + + foreach (var remove in toRemove) + { + node.Children.Remove(remove); + } + + return true; + } + + private async Task> FetchPackageDependencies( + PythonResolverState state, + PipDependencySpecification spec) + { + var candidateVersion = ((PipComponent)state.NodeReferences[spec.Name].Value).Version; + + var packageToFetch = state.ValidVersionMap[spec.Name][candidateVersion].FirstOrDefault(x => string.Equals("bdist_wheel", x.PackageType, StringComparison.OrdinalIgnoreCase)) ?? + state.ValidVersionMap[spec.Name][candidateVersion].FirstOrDefault(x => string.Equals("bdist_egg", x.PackageType, StringComparison.OrdinalIgnoreCase)); + if (packageToFetch == null) + { + return new List(); + } + + return await PypiClient.FetchPackageDependencies(spec.Name, candidateVersion, packageToFetch); + } + + private void AddGraphNode(PythonResolverState state, PipGraphNode parent, string name, string version) + { + if (state.NodeReferences.TryGetValue(name, out var value)) + { + parent.Children.Add(value); + value.Parents.Add(parent); + } + else + { + var node = new PipGraphNode(new PipComponent(name, version)); + state.NodeReferences[name] = node; + parent.Children.Add(node); + node.Parents.Add(parent); + } + } + + private class PythonResolverState + { + public readonly IDictionary>> ValidVersionMap + = new Dictionary>>(StringComparer.OrdinalIgnoreCase); + + public readonly Queue<(string, PipDependencySpecification)> ProcessingQueue = new Queue<(string, PipDependencySpecification)>(); + + public IList Roots { get; } = new List(); + + public readonly IDictionary NodeReferences = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/pip/PythonVersion.cs b/src/Microsoft.ComponentDetection.Detectors/pip/PythonVersion.cs new file mode 100644 index 000000000..e829c8830 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/pip/PythonVersion.cs @@ -0,0 +1,322 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Microsoft.ComponentDetection.Detectors.Pip +{ + public class PythonVersion : IComparable + { + // This is a light C# port of the python version capture regex described here: + // https://www.python.org/dev/peps/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions + private static readonly Regex PythonVersionRegex = + new Regex(@"(?:(?[0-9]+)!)?(?[0-9]+(?:\.[0-9]+)*)(?
[-_\.]?(?(a|b|c|rc|alpha|beta|pre|preview))[-_\.]?(?[0-9]+)?)?(?(?:-(?[0-9]+))|(?:[-_\.]?(?post|rev|r)[-_\.]?(?[0-9]+)?))?(?[-_\.]?(?dev)[-_\.]?(?[0-9]+)?)?", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+        private static Dictionary preReleaseMapping = new Dictionary { { "a", 0 }, { "alpha", 0 }, { "b", 1 }, { "beta", 1 }, { "c", 2 }, { "rc", 2 }, { "pre", 2 }, { "preview", 2 } };
+
+        public bool Valid { get; set; }
+
+        public bool IsReleasedPackage => string.IsNullOrEmpty(PreReleaseLabel) && !PreReleaseNumber.HasValue && !DevNumber.HasValue;
+
+        public int Epoch { get; set; }
+
+        public string Release { get; set; }
+
+        public string PreReleaseLabel { get; set; }
+
+        public int? PreReleaseNumber { get; set; }
+
+        public int? PostNumber { get; set; }
+
+        public int? DevNumber { get; set; }
+
+        public bool Floating { get; set; } = false;
+
+        private Match match;
+
+        public PythonVersion(string version)
+        {
+            var toOperate = version;
+            if (version.EndsWith(".*"))
+            {
+                Floating = true;
+                toOperate = toOperate.Replace(".*", string.Empty);
+            }
+
+            match = PythonVersionRegex.Match(version);
+
+            if (!match.Success || !string.Equals(match.Value, toOperate, StringComparison.OrdinalIgnoreCase))
+            {
+                Valid = false;
+                return;
+            }
+
+            var groups = match.Groups;
+
+            // Epoch is optional, implicitly 0 if not present
+            if (groups["epoch"].Success && int.TryParse(groups["epoch"].Value, out int epoch))
+            {
+                Epoch = epoch;
+            }
+            else
+            {
+                Epoch = 0;
+            }
+
+            Release = groups["release"].Success ? groups["release"].Value : string.Empty;
+            PreReleaseLabel = groups["pre_l"].Success ? groups["pre_l"].Value : string.Empty;
+
+            if (groups["pre_n"].Success && int.TryParse(groups["pre_n"].Value, out int preReleaseNumber))
+            {
+                PreReleaseNumber = preReleaseNumber;
+            }
+
+            if (groups["post_n1"].Success && int.TryParse(groups["post_n1"].Value, out int postRelease1))
+            {
+                PostNumber = postRelease1;
+            }
+
+            if (groups["post_n2"].Success && int.TryParse(groups["post_n2"].Value, out int postRelease2))
+            {
+                PostNumber = postRelease2;
+            }
+
+            if (groups["dev_n"].Success && int.TryParse(groups["dev_n"].Value, out int devNumber))
+            {
+                DevNumber = devNumber;
+            }
+
+            Valid = true;
+        }
+
+        public static bool operator >(PythonVersion operand1, PythonVersion operand2)
+        {
+            return operand1.CompareTo(operand2) == 1;
+        }
+
+        public static bool operator <(PythonVersion operand1, PythonVersion operand2)
+        {
+            return operand1.CompareTo(operand2) == -1;
+        }
+
+        public static bool operator >=(PythonVersion operand1, PythonVersion operand2)
+        {
+            return operand1.CompareTo(operand2) >= 0;
+        }
+
+        public static bool operator <=(PythonVersion operand1, PythonVersion operand2)
+        {
+            return operand1.CompareTo(operand2) <= 0;
+        }
+
+        public int CompareTo(PythonVersion other)
+        {
+            if (other == null || !other.Valid)
+            {
+                return 1;
+            }
+
+            if (Epoch > other.Epoch)
+            {
+                return 1;
+            }
+            else if (Epoch < other.Epoch)
+            {
+                return -1;
+            }
+
+            if (!string.Equals(Release, other.Release, StringComparison.OrdinalIgnoreCase))
+            {
+                int result = CompareReleaseVersions(this, other);
+                if (result != 0)
+                {
+                    return result;
+                }
+            }
+
+            int preReleaseComparison = ComparePreRelease(this, other);
+
+            if (preReleaseComparison != 0)
+            {
+                return preReleaseComparison;
+            }
+
+            int postNumberComparison = ComparePostNumbers(this, other);
+
+            if (postNumberComparison != 0)
+            {
+                return postNumberComparison;
+            }
+
+            int devNumberComparison = CompareDevValues(this, other);
+
+            return devNumberComparison;
+        }
+
+        private static int ComparePreRelease(PythonVersion a, PythonVersion b)
+        {
+            if (string.IsNullOrEmpty(a.PreReleaseLabel) && string.IsNullOrEmpty(b.PreReleaseLabel))
+            {
+                return 0;
+            }
+            else if (string.IsNullOrEmpty(a.PreReleaseLabel))
+            {
+                if (a.DevNumber.HasValue)
+                {
+                    return -1;
+                }
+
+                return 1;
+            }
+            else if (string.IsNullOrEmpty(b.PreReleaseLabel))
+            {
+                if (b.DevNumber.HasValue)
+                {
+                    return 1;
+                }
+
+                return -1;
+            }
+
+            var aLabelWeight = preReleaseMapping[a.PreReleaseLabel.ToLowerInvariant()];
+            var bLabelWeight = preReleaseMapping[b.PreReleaseLabel.ToLowerInvariant()];
+
+            if (aLabelWeight > bLabelWeight)
+            {
+                return 1;
+            }
+            else if (bLabelWeight > aLabelWeight)
+            {
+                return -1;
+            }
+
+            var aNum = a.PreReleaseNumber ?? 0;
+            var bNum = b.PreReleaseNumber ?? 0;
+
+            if (aNum > bNum)
+            {
+                return 1;
+            }
+            else if (bNum > aNum)
+            {
+                return -1;
+            }
+
+            // If we get here, we need to compare the post release numbers
+            return ComparePostNumbers(a, b);
+        }
+
+        private static int ComparePostNumbers(PythonVersion a, PythonVersion b)
+        {
+            if (!a.PostNumber.HasValue && !b.PostNumber.HasValue)
+            {
+                return 0;
+            }
+
+            if (a.PostNumber.HasValue && b.PostNumber.HasValue)
+            {
+                if (a.PostNumber.Value > b.PostNumber.Value)
+                {
+                    return 1;
+                }
+                else if (b.PostNumber.Value > a.PostNumber.Value)
+                {
+                    return -1;
+                }
+
+                // We need to compare the dev value
+                return CompareDevValues(a, b);
+            }
+            else if (a.PostNumber.HasValue)
+            {
+                return 1;
+            }
+            else
+            {
+                return -1;
+            }
+        }
+
+        private static int CompareDevValues(PythonVersion a, PythonVersion b)
+        {
+            if (!a.DevNumber.HasValue && !b.DevNumber.HasValue)
+            {
+                return 0;
+            }
+
+            if (a.DevNumber.HasValue && b.DevNumber.HasValue)
+            {
+                if (a.DevNumber.Value > b.DevNumber.Value)
+                {
+                    return 1;
+                }
+                else if (b.DevNumber.Value > a.DevNumber.Value)
+                {
+                    return -1;
+                }
+
+                return 0;
+            }
+            else if (a.DevNumber.HasValue)
+            {
+                return -1;
+            }
+            else
+            {
+                return 1;
+            }
+        }
+
+        private static int CompareReleaseVersions(PythonVersion a, PythonVersion b)
+        {
+            List aSplit = a.Release.Split('.').Select(x => int.Parse(x)).ToList();
+            List bSplit = b.Release.Split('.').Select(x => int.Parse(x)).ToList();
+
+            int longer;
+            int shorter;
+            int lengthCompare;
+            bool shorterFloating;
+
+            if (aSplit.Count > bSplit.Count)
+            {
+                longer = aSplit.Count;
+                shorter = bSplit.Count;
+                shorterFloating = b.Floating;
+                lengthCompare = 1;
+            }
+            else if (bSplit.Count > aSplit.Count)
+            {
+                longer = bSplit.Count;
+                shorter = aSplit.Count;
+                shorterFloating = a.Floating;
+                lengthCompare = -1;
+            }
+            else
+            {
+                longer = bSplit.Count;
+                shorter = aSplit.Count;
+                shorterFloating = false;
+                lengthCompare = 0;
+            }
+
+            for (int i = 0; i < shorter; i++)
+            {
+                if (aSplit[i] > bSplit[i])
+                {
+                    return 1;
+                }
+                else if (bSplit[i] > aSplit[i])
+                {
+                    return -1;
+                }
+            }
+
+            if (longer == (shorter + 1) && shorterFloating)
+            {
+                return 0;
+            }
+
+            return lengthCompare;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Microsoft.ComponentDetection.Detectors/pip/PythonVersionComparer.cs b/src/Microsoft.ComponentDetection.Detectors/pip/PythonVersionComparer.cs
new file mode 100644
index 000000000..58502141b
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/pip/PythonVersionComparer.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+
+namespace Microsoft.ComponentDetection.Detectors.Pip
+{
+    public class PythonVersionComparer : IComparer
+    {
+        public int Compare(string x, string y)
+        {
+            PythonVersion xVer = new PythonVersion(x);
+            PythonVersion yVer = new PythonVersion(y);
+
+            return xVer.CompareTo(yVer);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Microsoft.ComponentDetection.Detectors/pip/PythonVersionUtilities.cs b/src/Microsoft.ComponentDetection.Detectors/pip/PythonVersionUtilities.cs
new file mode 100644
index 000000000..73524658e
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/pip/PythonVersionUtilities.cs
@@ -0,0 +1,140 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.ComponentDetection.Detectors.Pip
+{
+    public static class PythonVersionUtilities
+    {
+        /// 
+        /// Determine if the version is valid for all specs.
+        /// 
+        /// Version.
+        /// Version specifications.
+        /// 
+        /// The version or any of the specs are an invalid python version.
+        public static bool VersionValidForSpec(string version, IList specs)
+        {
+            foreach (var spec in specs)
+            {
+                if (!VersionValidForSpec(version, spec))
+                {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        private static bool VersionValidForSpec(string version, string spec)
+        {
+            char[] opChars = new char[] { '=', '<', '>', '~', '!' };
+            var specArray = spec.ToCharArray();
+
+            int i = 0;
+            while (i < spec.Length && i < 3 && opChars.Contains(specArray[i]))
+            {
+                i++;
+            }
+
+            string op = spec.Substring(0, i);
+
+            var targetVer = new PythonVersion(version);
+            var specVer = new PythonVersion(spec.Substring(i));
+
+            if (!targetVer.Valid)
+            {
+                throw new ArgumentException($"{version} is not a valid python version");
+            }
+
+            if (!specVer.Valid)
+            {
+                throw new ArgumentException($"The version specification {spec.Substring(i)} is not a valid python version");
+            }
+
+            switch (op)
+            {
+                case "==":
+                    return targetVer.CompareTo(specVer) == 0;
+                case "===":
+                    return targetVer.CompareTo(specVer) == 0;
+                case "<":
+                    return specVer > targetVer;
+                case ">":
+                    return targetVer > specVer;
+                case "<=":
+                    return specVer >= targetVer;
+                case ">=":
+                    return targetVer >= specVer;
+                case "!=":
+                    return targetVer.CompareTo(specVer) != 0;
+                case "~=":
+                    return CheckEquality(version, spec.Substring(i), true);
+                default:
+                    return false;
+            }
+        }
+
+        // Todo, remove this code once * parsing is handled in the python version class
+        public static bool CheckEquality(string version, string specVer, bool fuzzy = false)
+        {
+            // This handles locked prerelease versions and non *
+            if (string.Equals(version, specVer, StringComparison.OrdinalIgnoreCase))
+            {
+                return true;
+            }
+
+            int i = 0;
+            var splitVersion = version.Split('.');
+            var splitSpecVer = specVer.Split('.');
+
+            while (true)
+            {
+                if (fuzzy && i == (splitSpecVer.Length - 1))
+                {
+                    // Fuzzy matching excludes everything after first two
+                    if (splitVersion.Length > i && int.TryParse(splitVersion[i], out int lVer) && int.TryParse(splitSpecVer[i], out int rVer) && lVer >= rVer)
+                    {
+                        return true;
+                    }
+                    else
+                    {
+                        return false;
+                    }
+                }
+
+                // If we got here, we have an * terminator to our spec ver, so anything is fair game
+                if (splitSpecVer.Length > i && splitSpecVer[i] == "*")
+                {
+                    return true;
+                }
+
+                if (splitSpecVer.Length > i && splitVersion.Length > i)
+                {
+                    if (string.Equals(splitSpecVer[i], splitVersion[i], StringComparison.OrdinalIgnoreCase)) // Match keep going
+                    {
+                        i++;
+                        continue;
+                    }
+                    else // No match
+                    {
+                        return false;
+                    }
+                }
+                else if (i <= splitSpecVer.Length && i <= splitVersion.Length) // We got to the end, no problems
+                {
+                    return true;
+                }
+                else // either one string terminated early, or something didn't match
+                {
+                    if (fuzzy && splitVersion.Length > i && !int.TryParse(splitVersion[i], out _))
+                    {
+                        return true;
+                    }
+
+                    return false;
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/Package.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/Package.cs
new file mode 100644
index 000000000..26983714c
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/Package.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+
+namespace Microsoft.ComponentDetection.Detectors.Pnpm
+{
+#pragma warning disable SA1300 // Used for deserialization and the process is case sensitive
+    public class Package
+    {
+        public Dictionary dependencies { get; set; }
+
+        public string dev { get; set; }
+
+        public string name { get; set; }
+
+        public Dictionary resolution { get; set; }
+
+        public string version { get; set; }
+
+        public override string ToString()
+        {
+            return name;
+        }
+    }
+#pragma warning restore SA1300
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetector.cs
new file mode 100644
index 000000000..8fb6ffd31
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetector.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Collections.Generic;
+using System.Composition;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.Internal;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+
+namespace Microsoft.ComponentDetection.Detectors.Pnpm
+{
+    [Export(typeof(IComponentDetector))]
+    public class PnpmComponentDetector : FileComponentDetector
+    {
+        public override string Id { get; } = "Pnpm";
+
+        public override IEnumerable Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.Npm) };
+
+        public override IList SearchPatterns { get; } = new List { "shrinkwrap.yaml", "pnpm-lock.yaml" };
+
+        public override IEnumerable SupportedComponentTypes { get; } = new[] { ComponentType.Npm };
+
+        public override int Version { get; } = 4;
+
+        /// 
+        protected override IList SkippedFolders => new List { "node_modules", "pnpm-store" };
+
+        public PnpmComponentDetector()
+        {
+            NeedsAutomaticRootDependencyCalculation = true;
+        }
+
+        protected override async Task OnFileFound(ProcessRequest processRequest, IDictionary detectorArgs)
+        {
+            var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder;
+            var file = processRequest.ComponentStream;
+
+            Logger.LogVerbose("Found yaml file: " + file.Location);
+            string skippedFolder = SkippedFolders.FirstOrDefault(folder => file.Location.Contains(folder));
+            if (!string.IsNullOrEmpty(skippedFolder))
+            {
+                Logger.LogVerbose($"Skipping found file, it was detected as being within a {skippedFolder} folder.");
+            }
+
+            try
+            {
+                var pnpmYaml = await PnpmParsingUtilities.DeserializePnpmYamlFile(file);
+                RecordDependencyGraphFromFile(pnpmYaml, singleFileComponentRecorder);
+            }
+            catch (Exception e)
+            {
+                Logger.LogFailedReadingFile(file.Location, e);
+            }
+        }
+
+        private void RecordDependencyGraphFromFile(PnpmYaml yaml, ISingleFileComponentRecorder singleFileComponentRecorder)
+        {
+            foreach (var packageKeyValue in yaml.packages ?? Enumerable.Empty>())
+            {
+                // Ignore file: as these are local packages.
+                if (packageKeyValue.Key.StartsWith("file:"))
+                {
+                    continue;
+                }
+
+                var parentDetectedComponent = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPath(pnpmPackagePath: packageKeyValue.Key);
+                bool isDevDependency = packageKeyValue.Value != null && PnpmParsingUtilities.IsPnpmPackageDevDependency(packageKeyValue.Value);
+                singleFileComponentRecorder.RegisterUsage(parentDetectedComponent, isDevelopmentDependency: isDevDependency);
+                parentDetectedComponent = singleFileComponentRecorder.GetComponent(parentDetectedComponent.Component.Id);
+
+                if (packageKeyValue.Value.dependencies != null)
+                {
+                    foreach (var dependency in packageKeyValue.Value.dependencies)
+                    {
+                        // Ignore file: as these are local packages.
+                        if (dependency.Key.StartsWith("file:"))
+                        {
+                            continue;
+                        }
+
+                        var childDetectedComponent = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPath(pnpmPackagePath: CreatePnpmPackagePathFromDependency(dependency.Key, dependency.Value));
+
+                        // Older code used the root's dev dependency value. We're leaving this null until we do a second pass to look at each components' top level referrers.
+                        singleFileComponentRecorder.RegisterUsage(childDetectedComponent, parentComponentId: parentDetectedComponent.Component.Id, isDevelopmentDependency: null);
+                    }
+                }
+            }
+
+            // PNPM doesn't know at the time of RegisterUsage being called for a dependency whether something is a dev dependency or not, so after building up the graph we look at top level referrers.
+            foreach (var component in singleFileComponentRecorder.GetDetectedComponents())
+            {
+                var graph = singleFileComponentRecorder.DependencyGraph;
+                var explicitReferences = graph.GetExplicitReferencedDependencyIds(component.Key);
+                foreach (var explicitReference in explicitReferences)
+                {
+                    singleFileComponentRecorder.RegisterUsage(component.Value, isDevelopmentDependency: graph.IsDevelopmentDependency(explicitReference));
+                }
+            }
+        }
+
+        private string CreatePnpmPackagePathFromDependency(string dependencyName, string dependencyVersion)
+        {
+            return dependencyVersion.Contains('/') ? dependencyVersion : $"/{dependencyName}/{dependencyVersion}";
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmParsingUtilities.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmParsingUtilities.cs
new file mode 100644
index 000000000..585941698
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmParsingUtilities.cs
@@ -0,0 +1,81 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using NuGet.Versioning;
+using YamlDotNet.Serialization;
+
+namespace Microsoft.ComponentDetection.Detectors.Pnpm
+{
+    public static class PnpmParsingUtilities
+    {
+        public static async Task DeserializePnpmYamlFile(IComponentStream file)
+        {
+            var text = await new StreamReader(file.Stream).ReadToEndAsync();
+            var deserializer = new DeserializerBuilder()
+                .IgnoreUnmatchedProperties()
+                .Build();
+            return deserializer.Deserialize(new StringReader(text));
+        }
+
+        public static DetectedComponent CreateDetectedComponentFromPnpmPath(string pnpmPackagePath)
+        {
+            var (parentName, parentVersion) = ExtractNameAndVersionFromPnpmPackagePath(pnpmPackagePath);
+            return new DetectedComponent(new NpmComponent(parentName, parentVersion));
+        }
+
+        public static bool IsPnpmPackageDevDependency(Package pnpmPackage)
+        {
+            if (pnpmPackage == null)
+            {
+                throw new ArgumentNullException(nameof(pnpmPackage));
+            }
+
+            return string.Equals(bool.TrueString, pnpmPackage.dev, StringComparison.InvariantCultureIgnoreCase);
+        }
+
+        private static (string Name, string Version) ExtractNameAndVersionFromPnpmPackagePath(string pnpmPackagePath)
+        {
+            var pnpmComponentDefSections = pnpmPackagePath.Trim('/').Split('/');
+            (string packageVersion, int indexVersionIsAt) = GetPackageVersion(pnpmComponentDefSections);
+            if (indexVersionIsAt == -1)
+            {
+                // No version = not expected input
+                return (null, null);
+            }
+
+            var normalizedPackageName = string.Join("/", pnpmComponentDefSections.Take(indexVersionIsAt).ToArray());
+            return (normalizedPackageName, packageVersion);
+        }
+
+        private static (string PackageVersion, int VersionIndex) GetPackageVersion(string[] pnpmComponentDefSections)
+        {
+            var indexVersionIsAt = -1;
+            var packageVersion = string.Empty;
+            var lastIndex = pnpmComponentDefSections.Length - 1;
+
+            // get version from packages with format /mute-stream/0.0.6
+            if (SemanticVersion.TryParse(pnpmComponentDefSections[lastIndex], out var _))
+            {
+                return (pnpmComponentDefSections[lastIndex], lastIndex);
+            }
+
+            // get version from packages with format /@babel/helper-compilation-targets/7.10.4_@babel+core@7.10.5
+            var lastComponentSplit = pnpmComponentDefSections[lastIndex].Split("_");
+            if (SemanticVersion.TryParse(lastComponentSplit[0], out var _))
+            {
+                return (lastComponentSplit[0], lastIndex);
+            }
+
+            // get version from packages with format /sinon-chai/2.8.0/chai@3.5.0+sinon@1.17.7
+            if (SemanticVersion.TryParse(pnpmComponentDefSections[lastIndex - 1], out var _))
+            {
+                return (pnpmComponentDefSections[lastIndex - 1], lastIndex - 1);
+            }
+
+            return (packageVersion, indexVersionIsAt);
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmYaml.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmYaml.cs
new file mode 100644
index 000000000..719c0f924
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmYaml.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+
+namespace Microsoft.ComponentDetection.Detectors.Pnpm
+{
+#pragma warning disable SA1300 // Used for deserialization and the process is case sensitive
+    public class PnpmYaml
+    {
+        public Dictionary dependencies { get; set; }
+
+        public Dictionary packages { get; set; }
+    }
+#pragma warning restore SA1300
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/pnpmVersionSpecification.md b/src/Microsoft.ComponentDetection.Detectors/pnpm/pnpmVersionSpecification.md
new file mode 100644
index 000000000..96a55d864
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/pnpmVersionSpecification.md
@@ -0,0 +1,100 @@
+# Pnpm Version format specification
+
+This is a summary of some conversations with the Pnpm community about how the version is formatted for pnpm packages:
+
+A dependency defined in package.json as:
+```
+jquery: 1.0.0
+```
+is going to be represented in the lock files of pnpm as:
+```
+/jquery/1.0.0
+```
+
+This nomenclature is known for the pnpm community as the package path, and is normally found as the package definition in the section Packages of the lock files (pnpm-lock.yml and shrinkwrap.yml).
+Normally most of the packages has this structure but there others situations like [peer dependencies](https://pnpm.js.org/en/how-peers-are-resolved), where the path format can change to represent the peer dependency combination, for example:
+
+First the regular case, suppose that we have in the package.json a dependency defined as 
+
+```
+{
+    name: foo
+    version: 1.0.0
+    dependencies: {
+        abc: 1.0.0
+    }
+}
+```
+using the command pnpm install this is going to create a folder structure like
+```
+/foo/1.0.0/
+|- foo (hardlink)
+|- abc (symlink)
+```
+and the entry in the lock file is going to looks like 
+```
+/foo/1.0.0
+```
+Now if the package foo define a peer dependency as:
+
+```
+{
+    name: foo
+    version: 1.0.0
+    dependencies: {
+        abc: 1.0.0
+    },
+    peerDependencies: {
+        bar: ^1.0.0
+    }
+}
+```
+And foo has diferent parents that dependents on this peer dependency but using a different version that still satisfy the semantic version defined by foo:
+
+```
+{
+    name: parent1
+    version: 1.0.0
+    dependencies: {
+        foo: 1.0.0
+        bar: 1.0.0
+    }
+}
+
+
+{
+    name: parent2
+    version: 1.0.0
+    dependencies: {
+        foo: 1.0.0
+        bar: 1.0.1
+    }
+}
+```
+
+then Pnpm is going to create a new folder for each combination _foo_  _bar 1.0.0_ and _foo_  _bar 1.0.1_
+
+The folder structure would looks like
+
+```
+/foo/1.0.0
+|-bar@1.0.0
+  |- foo(hardlink)
+  |- bar v1.0.0(symlink)
+  |- abc (symlink)
+|-bar@1.0.1
+  |- foo(hardlink)
+  |- bar v1.0.1(symlink)
+  |- abc(symlink)
+```
+This is going to create 2 different entries in the lock file, these entries can have a different format depending on the version of pnpm, they can look like:
+```
+/foo/1.0.0/bar@1.0.0
+/foo/1.0.0/bar@1.0.1
+```
+or
+```
+/foo/1.0.0_bar@1.0.0
+/foo/1.0.0_bar@1.0.1
+```
+
diff --git a/src/Microsoft.ComponentDetection.Detectors/ruby/RubyComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/ruby/RubyComponentDetector.cs
new file mode 100644
index 000000000..3ca15704f
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/ruby/RubyComponentDetector.cs
@@ -0,0 +1,272 @@
+// Ruby detection highlights and todos:
+// 
+// Dependencies are "fuzzy versions":
+// this in and of itself could be solved by deferring dependency resolution alone until after all components are registered.
+// Different sections of Ruby's lockfile can point into other sections, and the authoritative version is not replicated across
+// sections-- it's only stored in, say, the Gems section.
+// 
+// Git components are even stranger in Ruby land:
+// they have an annotation for a git component that is a "name" that has no relationship to how we normally think of 
+// a GitComponent (remote / version). The mapping from git component name to a GitComponent can't really be handled 
+// in ComponentRecorder today, because "component name" for a Git component is a Ruby specific concept. 
+// This could be pointing to a sideloaded storage in ComponentRecorder (e.g. a  style storage that detectors
+// could use to track related state as their execution goes on).
+// 
+// The basic approach in ruby is to do two passes:
+// first, make sure you have all authoritative components, then, resolve and register all dependency relationships.
+// 
+// If we had sideloaded state for nodes in the graph, I could see us at least being able to remove the "name" mapping from ruby.
+// Deferred dependencies is a lot more complicated, you would basically need a way to set up a pointer to a component based on a mapped value
+// (in this case, just component name sans version) that would be resolved in an arbitrary way after the graph writing was "done".
+// I don't think this is impossible (having a custom delegate for a detector to identify and map nodes to one another seems pretty easy),
+// but seems complicated.
+// 
+// There is a possibility to use manual root detection instead of automatic:
+// Gemfile.lock comes with a section called "Dependencies", in the section are listed the dependencies that the user specified in the Gemfile,
+// is necessary to investigate if this section is a new adition or always has been there.
+
+using System;
+using System.Collections.Generic;
+using System.Composition;
+using System.IO;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.Internal;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+
+namespace Microsoft.ComponentDetection.Detectors.Ruby
+{
+    [Export(typeof(IComponentDetector))]
+    public class RubyComponentDetector : FileComponentDetector
+    {
+        private static readonly Regex HeadingRegex = new Regex("^[A-Z ]+$", RegexOptions.Compiled);
+        private static readonly Regex DependencyDefinitionRegex = new Regex("^ {4}[A-Za-z-]+", RegexOptions.Compiled);
+        private static readonly Regex SubDependencyRegex = new Regex("^ {6}[A-Za-z-]+", RegexOptions.Compiled);
+
+        public override string Id { get; } = "Ruby";
+
+        public override IEnumerable Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.RubyGems) };
+
+        public override IList SearchPatterns { get; } = new List { "Gemfile.lock" };
+
+        public override IEnumerable SupportedComponentTypes { get; } = new[] { ComponentType.RubyGems };
+
+        public override int Version { get; } = 3;
+
+        private enum SectionType
+        {
+            GEM,
+            GIT,
+            PATH,
+        }
+
+        private class Dependency
+        {
+            public string Name { get; }
+
+            public string Location { get; }
+
+            public string Id => $"{Name}:{Location}";
+
+            public Dependency(string name, string location)
+            {
+                Name = name;
+                Location = location;
+            }
+        }
+
+        public RubyComponentDetector()
+        {
+            NeedsAutomaticRootDependencyCalculation = true;
+        }
+
+        protected override Task OnFileFound(ProcessRequest processRequest, IDictionary detectorArgs)
+        {
+            var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder;
+            var file = processRequest.ComponentStream;
+
+            Logger.LogVerbose("Found Gemfile.lock: " + file.Location);
+            ParseGemLockFile(singleFileComponentRecorder, file);
+
+            return Task.CompletedTask;
+        }
+
+        private void ParseGemLockFile(ISingleFileComponentRecorder singleFileComponentRecorder, IComponentStream file)
+        {
+            var components = new Dictionary();
+            var dependencies = new Dictionary>();
+
+            var text = string.Empty;
+            using (var reader = new StreamReader(file.Stream))
+            {
+                text = reader.ReadToEnd();
+            }
+
+            var lines = new List(text.Split("\n"));
+
+            while (lines.Count > 0)
+            {
+                if (HeadingRegex.IsMatch(lines[0].Trim()))
+                {
+                    var heading = lines[0].Trim();
+                    lines.RemoveAt(0);
+
+                    // Get the lines from the section sections end with a blank line
+                    var sublines = new List();
+                    while (lines.Count > 0 && lines[0].Trim().Length > 0)
+                    {
+                        sublines.Add(lines[0]);
+                        lines.RemoveAt(0);
+                    }
+
+                    // lines[0] is now a blank line, so lets remove it
+                    if (lines.Count > 0)
+                    {
+                        lines.RemoveAt(0);
+                    }
+
+                    switch (heading)
+                    {
+                        case "GIT":
+                            ParseSection(SectionType.GIT, sublines, components, dependencies, file);
+                            break;
+                        case "GEM":
+                            ParseSection(SectionType.GEM, sublines, components, dependencies, file);
+                            break;
+                        case "PATH":
+                            ParseSection(SectionType.PATH, sublines, components, dependencies, file);
+                            break;
+                        case "BUNDLED WITH":
+                            var line = sublines[0].Trim();
+                            var name = "bundler";
+
+                            // Nothing in the lockfile tells us where bundler came from
+                            DetectedComponent addComponent = new DetectedComponent(new RubyGemsComponent(name, line, "unknown"));
+                            components.TryAdd(string.Format("{0}:{1}", name, file.Location), addComponent);
+                            dependencies.TryAdd(string.Format("{0}:{1}", name, file.Location), new List());
+                            break;
+                        default:
+                            // We ignore other sections
+                            break;
+                    }
+                }
+                else
+                {
+                    // Throw this line away. Is this malformed? We were expecting a header
+                    Logger.LogVerbose(lines[0]);
+                    Logger.LogVerbose("Appears to be malformed/is not expected here.  Expected heading.");
+                    lines.RemoveAt(0);
+                }
+            }
+
+            foreach (var detectedComponent in components.Values)
+            {
+                singleFileComponentRecorder.RegisterUsage(detectedComponent);
+            }
+
+            foreach (string key in dependencies.Keys)
+            {
+                foreach (Dependency dependency in dependencies[key])
+                {
+                    // there are cases that we ommit the dependency
+                    // because its version is not valid like for example 
+                    // is a relative version instead of an absolute one
+                    // because of that there are children elements 
+                    // that does not contains a entry in the dictionary
+                    // those elements should be removed
+                    if (components.ContainsKey(dependency.Id))
+                    {
+                        singleFileComponentRecorder.RegisterUsage(components[dependency.Id], parentComponentId: components[key].Component.Id);
+                    }
+                }
+            }
+        }
+
+        private void ParseSection(SectionType sectionType, List lines, Dictionary components, Dictionary> dependencies, IComponentStream file)
+        {
+            string name, remote, revision;
+            name = remote = revision = string.Empty;
+
+            bool wasParentDependencyExcluded = false;
+
+            while (lines.Count > 0)
+            {
+                var line = lines[0].Trim();
+                lines.RemoveAt(0);
+                if (line.StartsWith("remote:"))
+                {
+                    remote = line.Substring(8);
+
+                    // revision is only used for Git components.
+                    revision = string.Empty;
+                }
+                else if (line.StartsWith("revision:"))
+                {
+                    revision = line.Substring(10);
+                }
+                else if (line.StartsWith("specs:"))
+                {
+                    while (lines.Count > 0)
+                    {
+                        line = lines[0].TrimEnd();
+                        lines.RemoveAt(0);
+                        if (line.Trim().Equals(string.Empty))
+                        {
+                            break;
+                        }
+
+                        // Sub-dependency, store dependencies data of parents that were not excluded because of relative version
+                        else if (SubDependencyRegex.IsMatch(line) && !wasParentDependencyExcluded)
+                        {
+                            var depName = line.Trim().Split(' ')[0];
+                            dependencies[string.Format("{0}:{1}", name, file.Location)].Add(new Dependency(depName, file.Location));
+                        }
+                        else if (DependencyDefinitionRegex.IsMatch(line))
+                        {
+                            wasParentDependencyExcluded = false;
+                            var splits = line.Trim().Split(" ");
+                            name = splits[0].Trim();
+                            var version = splits[1].Substring(1, splits[1].Length - 2);
+                            TypedComponent newComponent;
+
+                            if (IsVersionRelative(version))
+                            {
+                                Logger.LogWarning($"Found component with invalid version, name = {name} and version = {version}");
+                                wasParentDependencyExcluded = true;
+                                continue;
+                            }
+
+                            if (sectionType == SectionType.GEM || sectionType == SectionType.PATH)
+                            {
+                                newComponent = new RubyGemsComponent(name, version, remote);
+                            }
+                            else
+                            {
+                                newComponent = new GitComponent(new Uri(remote), revision);
+                            }
+
+                            DetectedComponent addComponent = new DetectedComponent(newComponent);
+                            string lookupKey = string.Format("{0}:{1}", name, file.Location);
+
+                            if (components.ContainsKey(lookupKey))
+                            {
+                                components.TryAdd(string.Format("{0}:{1}", lookupKey, version), addComponent);
+                            }
+                            else
+                            {
+                                components.TryAdd(lookupKey, addComponent);
+                                dependencies.Add(lookupKey, new List());
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        private bool IsVersionRelative(string version)
+        {
+            return version.StartsWith("~") || version.StartsWith("=");
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/rust/CargoDependencyData.cs b/src/Microsoft.ComponentDetection.Detectors/rust/CargoDependencyData.cs
new file mode 100644
index 000000000..aaed25503
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/rust/CargoDependencyData.cs
@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+
+namespace Microsoft.ComponentDetection.Detectors.Rust
+{
+    public class CargoDependencyData
+    {
+        public HashSet CargoWorkspaces;
+
+        public HashSet CargoWorkspaceExclusions;
+
+        public IList NonDevDependencies;
+
+        public IList DevDependencies;
+
+        public CargoDependencyData()
+        {
+            CargoWorkspaces = new HashSet();
+            CargoWorkspaceExclusions = new HashSet();
+            NonDevDependencies = new List();
+            DevDependencies = new List();
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/rust/Contracts/CargoLock.cs b/src/Microsoft.ComponentDetection.Detectors/rust/Contracts/CargoLock.cs
new file mode 100644
index 000000000..c0f088943
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/rust/Contracts/CargoLock.cs
@@ -0,0 +1,15 @@
+using System.Diagnostics.CodeAnalysis;
+using Nett;
+
+namespace Microsoft.ComponentDetection.Detectors.Rust.Contracts
+{
+    // Represents Cargo.Lock file structure.
+    public class CargoLock
+    {
+        [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:ElementMustBeginWithUpperCaseLetter", Justification = "Deserialization contract. Casing cannot be overwritten.")]
+        public CargoPackage[] package { get; set; }
+
+        [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:ElementMustBeginWithUpperCaseLetter", Justification = "Deserialization contract. Casing cannot be overwritten.")]
+        public TomlTable metadata { get; set; }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/rust/Contracts/CargoPackage.cs b/src/Microsoft.ComponentDetection.Detectors/rust/Contracts/CargoPackage.cs
new file mode 100644
index 000000000..8b2e711ae
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/rust/Contracts/CargoPackage.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.ComponentDetection.Detectors.Rust.Contracts
+{
+    public class CargoPackage
+    {
+        [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:ElementMustBeginWithUpperCaseLetter", Justification = "Deserialization contract. Casing cannot be overwritten.")]
+        public string name { get; set; }
+
+        [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:ElementMustBeginWithUpperCaseLetter", Justification = "Deserialization contract. Casing cannot be overwritten.")]
+        public string version { get; set; }
+
+        [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:ElementMustBeginWithUpperCaseLetter", Justification = "Deserialization contract. Casing cannot be overwritten.")]
+        public string author { get; set; }
+
+        [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:ElementMustBeginWithUpperCaseLetter", Justification = "Deserialization contract. Casing cannot be overwritten.")]
+        public string source { get; set; }
+
+        [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:ElementMustBeginWithUpperCaseLetter", Justification = "Deserialization contract. Casing cannot be overwritten.")]
+        public string checksum { get; set; }
+
+        [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:ElementMustBeginWithUpperCaseLetter", Justification = "Deserialization contract. Casing cannot be overwritten.")]
+        public string[] dependencies { get; set; }
+
+        // Get hash code and equals are IDE generated
+        // Manually added some casing handling
+        public override bool Equals(object obj)
+        {
+            var package = obj as CargoPackage;
+            return package != null &&
+                   name.Equals(package.name) &&
+                   version.Equals(package.version, StringComparison.OrdinalIgnoreCase);
+        }
+
+        public override int GetHashCode()
+        {
+            var hashCode = -2143789899;
+            hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(name);
+            hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(version.ToLowerInvariant());
+            return hashCode;
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/rust/Contracts/CargoToml.cs b/src/Microsoft.ComponentDetection.Detectors/rust/Contracts/CargoToml.cs
new file mode 100644
index 000000000..a0ab57211
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/rust/Contracts/CargoToml.cs
@@ -0,0 +1,10 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.ComponentDetection.Detectors.Rust.Contracts
+{
+    public class CargoToml
+    {
+        [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:ElementMustBeginWithUpperCaseLetter", Justification = "Deserialization contract. Casing cannot be overwritten.")]
+        public CargoPackage package { get; set; }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/rust/DependencySpecification.cs b/src/Microsoft.ComponentDetection.Detectors/rust/DependencySpecification.cs
new file mode 100644
index 000000000..2115c6e3b
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/rust/DependencySpecification.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.ComponentDetection.Detectors.Rust.Contracts;
+using Semver;
+
+using Range = Microsoft.ComponentDetection.Detectors.Rust.SemVer.Range;
+
+namespace Microsoft.ComponentDetection.Detectors.Rust
+{
+    public class DependencySpecification
+    {
+        private IDictionary>> dependencies;
+
+        public DependencySpecification()
+        {
+            dependencies = new Dictionary>>();
+        }
+
+        public void Add(string name, string cargoVersionSpecifier)
+        {
+            ISet ranges = new HashSet();
+            var specifiers = cargoVersionSpecifier.Split(new char[] { ',' });
+            foreach (var specifier in specifiers)
+            {
+                ranges.Add(new Range(specifier.Trim()));
+            }
+
+            if (!dependencies.ContainsKey(name))
+            {
+                dependencies.Add(name, new HashSet>());
+            }
+
+            dependencies[name].Add(ranges);
+        }
+
+        public bool MatchesPackage(CargoPackage package)
+        {
+            if (!dependencies.ContainsKey(package.name))
+            {
+                return false;
+            }
+
+            foreach (var ranges in dependencies[package.name])
+            {
+                var allSatisfied = true;
+                foreach (var range in ranges)
+                {
+                    if (SemVersion.TryParse(package.version, out SemVersion sv))
+                    {
+                        if (!range.IsSatisfied(sv))
+                        {
+                            allSatisfied = false;
+                        }
+                    }
+                    else
+                    {
+                        throw new FormatException($"Could not parse {package.version} into a valid Semver");
+                    }
+                }
+
+                if (allSatisfied)
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/rust/InvalidRustTomlFileException.cs b/src/Microsoft.ComponentDetection.Detectors/rust/InvalidRustTomlFileException.cs
new file mode 100644
index 000000000..7633655f7
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/rust/InvalidRustTomlFileException.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Microsoft.ComponentDetection.Detectors.Rust
+{
+    public class InvalidRustTomlFileException : Exception
+    {
+        public InvalidRustTomlFileException()
+        {
+        }
+
+        public InvalidRustTomlFileException(string message)
+            : base(message)
+        {
+        }
+
+        public InvalidRustTomlFileException(string message, Exception innerException)
+            : base(message, innerException)
+        {
+        }
+
+        protected InvalidRustTomlFileException(SerializationInfo info, StreamingContext context)
+            : base(info, context)
+        {
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/rust/RustCrateDetector.cs b/src/Microsoft.ComponentDetection.Detectors/rust/RustCrateDetector.cs
new file mode 100644
index 000000000..3eb86cdbe
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/rust/RustCrateDetector.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+using System.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.Internal;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Detectors.Rust.Contracts;
+using Nett;
+
+namespace Microsoft.ComponentDetection.Detectors.Rust
+{
+    [Export(typeof(IComponentDetector))]
+    public class RustCrateDetector : FileComponentDetector
+    {
+        public override string Id => "RustCrateDetector";
+
+        public override IList SearchPatterns => new List { RustCrateUtilities.CargoLockSearchPattern };
+
+        public override IEnumerable SupportedComponentTypes => new[] { ComponentType.Cargo };
+
+        public override int Version { get; } = 4;
+
+        public override IEnumerable Categories => new List { "Rust" };
+
+        protected override Task OnFileFound(ProcessRequest processRequest, IDictionary detectorArgs)
+        {
+            var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder;
+            var cargoLockFile = processRequest.ComponentStream;
+
+            try
+            {
+                var cargoLock = StreamTomlSerializer.Deserialize(cargoLockFile.Stream, TomlSettings.Create()).Get();
+
+                // This makes sure we're only trying to parse Cargo.lock v1 formats
+                if (cargoLock.metadata == null)
+                {
+                    Logger.LogInfo($"Cargo.lock file at {cargoLockFile.Location} contains no metadata section so we're parsing it as the v2 format. The v1 detector will not process it.");
+                    return Task.CompletedTask;
+                }
+
+                FileInfo lockFileInfo = new FileInfo(cargoLockFile.Location);
+                IEnumerable cargoTomlComponentStream = ComponentStreamEnumerableFactory.GetComponentStreams(lockFileInfo.Directory, new List { RustCrateUtilities.CargoTomlSearchPattern }, (name, directoryName) => false, recursivelyScanDirectories: false);
+
+                CargoDependencyData cargoDependencyData = RustCrateUtilities.ExtractRootDependencyAndWorkspaceSpecifications(cargoTomlComponentStream, singleFileComponentRecorder);
+
+                // If workspaces have been defined in the root cargo.toml file, scan for specified cargo.toml manifests
+                int numWorkspaceComponentStreams = 0;
+                int expectedWorkspaceTomlCount = cargoDependencyData.CargoWorkspaces.Count;
+                if (expectedWorkspaceTomlCount > 0)
+                {
+                    string rootCargoTomlLocation = Path.Combine(lockFileInfo.DirectoryName, "Cargo.toml");
+
+                    IEnumerable cargoTomlWorkspaceComponentStreams = ComponentStreamEnumerableFactory.GetComponentStreams(
+                        lockFileInfo.Directory,
+                        new List { RustCrateUtilities.CargoTomlSearchPattern },
+                        RustCrateUtilities.BuildExcludeDirectoryPredicateFromWorkspaces(lockFileInfo, cargoDependencyData.CargoWorkspaces, cargoDependencyData.CargoWorkspaceExclusions),
+                        recursivelyScanDirectories: true)
+                        .Where(x => !x.Location.Equals(rootCargoTomlLocation)); // The root directory needs to be included in directoriesToScan, but should not be reprocessed
+                    numWorkspaceComponentStreams = cargoTomlWorkspaceComponentStreams.Count();
+
+                    // Now that the non-root files have been located, add their dependencies
+                    RustCrateUtilities.ExtractDependencySpecifications(cargoTomlWorkspaceComponentStreams, singleFileComponentRecorder, cargoDependencyData.NonDevDependencies, cargoDependencyData.DevDependencies);
+                }
+
+                // Even though we can't read the file streams, we still have the enumerable!
+                if (!cargoTomlComponentStream.Any() || cargoTomlComponentStream.Count() > 1)
+                {
+                    Logger.LogWarning($"We are expecting exactly 1 accompanying Cargo.toml file next to the cargo.lock file found at {cargoLockFile.Location}");
+                    return Task.CompletedTask;
+                }
+
+                // If there is a mismatch between the number of expected and found workspaces, exit
+                if (expectedWorkspaceTomlCount > numWorkspaceComponentStreams)
+                {
+                    Logger.LogWarning($"We are expecting at least {expectedWorkspaceTomlCount} accompanying Cargo.toml file(s) from workspaces outside of the root directory {lockFileInfo.DirectoryName}, but found {numWorkspaceComponentStreams}");
+                    return Task.CompletedTask;
+                }
+
+                var cargoPackages = cargoLock.package.ToHashSet();
+                RustCrateUtilities.BuildGraph(cargoPackages, cargoDependencyData.NonDevDependencies, cargoDependencyData.DevDependencies, singleFileComponentRecorder);
+            }
+            catch (Exception e)
+            {
+                // If something went wrong, just ignore the file
+                Logger.LogFailedReadingFile(cargoLockFile.Location, e);
+            }
+
+            return Task.CompletedTask;
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/rust/RustCrateUtilities.cs b/src/Microsoft.ComponentDetection.Detectors/rust/RustCrateUtilities.cs
new file mode 100644
index 000000000..68742d7f7
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/rust/RustCrateUtilities.cs
@@ -0,0 +1,440 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using DotNet.Globbing;
+using Microsoft.ComponentDetection.Common.Telemetry.Records;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Detectors.Rust.Contracts;
+using Nett;
+using Semver;
+
+namespace Microsoft.ComponentDetection.Detectors.Rust
+{
+    public class RustCrateUtilities
+    {
+        private static readonly Regex DependencyFormatRegex = new Regex(
+        ////  PkgName Version    Source
+            @"([^ ]+) ([^ ]+) \(([^()]*)\)",
+            RegexOptions.Compiled);
+
+        public const string CargoTomlSearchPattern = "Cargo.toml";
+        public const string CargoLockSearchPattern = "Cargo.lock";
+
+        public static string[] NonDevDependencyKeys => new string[] { "dependencies", "build-dependencies" };
+
+        public static string[] DevDependencyKeys => new string[] { "dev-dependencies" };
+
+        private const string WorkspaceKey = "workspace";
+
+        private const string WorkspaceMemberKey = "members";
+
+        private const string WorkspaceExcludeKey = "exclude";
+
+        /// 
+        /// Given the project root Cargo.toml file, extract any workspaces specified and any root dependencies.
+        /// 
+        /// A stream representing the root cargo.toml file.
+        /// The component recorder which will have workspace toml files added as related.
+        /// 
+        /// A CargoDependencyData containing populated lists of CargoWorkspaces that will be included from search, CargoWorkspaceExclusions that will be excluded from search,
+        /// a list of non-development dependencies, and a list of development dependencies.
+        /// 
+        public static CargoDependencyData ExtractRootDependencyAndWorkspaceSpecifications(IEnumerable cargoTomlComponentStream, ISingleFileComponentRecorder singleFileComponentRecorder)
+        {
+            CargoDependencyData cargoDependencyData = new CargoDependencyData();
+
+            // The file handle is disposed if you call .First() on cargoTomlComponentStream
+            // Since multiple Cargo.toml files for 1 Cargo.lock file obviously doesn't make sense
+            // We break at the end of this loop
+            foreach (var cargoTomlFile in cargoTomlComponentStream)
+            {
+                var cargoToml = StreamTomlSerializer.Deserialize(cargoTomlFile.Stream, TomlSettings.Create());
+
+                singleFileComponentRecorder.AddAdditionalRelatedFile(cargoTomlFile.Location);
+
+                // Extract the workspaces present, if any
+                if (cargoToml.ContainsKey(RustCrateUtilities.WorkspaceKey))
+                {
+                    TomlTable workspaces = cargoToml.Get(RustCrateUtilities.WorkspaceKey);
+
+                    TomlObject workspaceMembers = workspaces.ContainsKey(RustCrateUtilities.WorkspaceMemberKey) ? workspaces[RustCrateUtilities.WorkspaceMemberKey] : null;
+                    TomlObject workspaceExclusions = workspaces.ContainsKey(RustCrateUtilities.WorkspaceExcludeKey) ? workspaces[RustCrateUtilities.WorkspaceExcludeKey] : null;
+
+                    if (workspaceMembers != null)
+                    {
+                        if (workspaceMembers.TomlType != TomlObjectType.Array)
+                        {
+                            throw new InvalidRustTomlFileException($"In accompanying Cargo.toml file expected {RustCrateUtilities.WorkspaceMemberKey} within {RustCrateUtilities.WorkspaceKey} to be of type Array, but found {workspaceMembers.TomlType}");
+                        }
+
+                        // TomlObject arrays do not natively implement a HashSet get, so add from a list
+                        cargoDependencyData.CargoWorkspaces.UnionWith(workspaceMembers.Get>());
+                    }
+
+                    if (workspaceExclusions != null)
+                    {
+                        if (workspaceExclusions.TomlType != TomlObjectType.Array)
+                        {
+                            throw new InvalidRustTomlFileException($"In accompanying Cargo.toml file expected {RustCrateUtilities.WorkspaceExcludeKey} within {RustCrateUtilities.WorkspaceKey} to be of type Array, but found {workspaceExclusions.TomlType}");
+                        }
+
+                        cargoDependencyData.CargoWorkspaceExclusions.UnionWith(workspaceExclusions.Get>());
+                    }
+                }
+
+                RustCrateUtilities.GenerateDependencies(cargoToml, cargoDependencyData.NonDevDependencies, cargoDependencyData.DevDependencies);
+
+                break;
+            }
+
+            return cargoDependencyData;
+        }
+
+        /// 
+        /// Given a set of Cargo.toml files, extract development and non-development dependency lists for each.
+        /// 
+        /// A list of streams representing cargo workspaces.
+        /// The component recorder which will have workspace toml files added as related.
+        /// Current list of non-development dependencies.
+        /// Current list of development dependencies.
+        public static void ExtractDependencySpecifications(IEnumerable cargoTomlComponentStreams, ISingleFileComponentRecorder singleFileComponentRecorder, IList nonDevDependencySpecifications, IList devDependencySpecifications)
+        {
+            // The file handles within cargoTomlComponentStreams will be disposed after enumeration
+            // This method is only used in non root toml extraction, so the whole list should be iterated
+            foreach (var cargoTomlFile in cargoTomlComponentStreams)
+            {
+                var cargoToml = StreamTomlSerializer.Deserialize(cargoTomlFile.Stream, TomlSettings.Create());
+
+                singleFileComponentRecorder.AddAdditionalRelatedFile(cargoTomlFile.Location);
+
+                RustCrateUtilities.GenerateDependencies(cargoToml, nonDevDependencySpecifications, devDependencySpecifications);
+            }
+        }
+
+        /// 
+        /// Extract development and non-development dependency lists from a given TomlTable.
+        /// 
+        /// The TomlTable representing a whole cargo.toml file.
+        /// Current list of non-development dependencies.
+        /// Current list of development dependencies.
+        private static void GenerateDependencies(TomlTable cargoToml, IList nonDevDependencySpecifications, IList devDependencySpecifications)
+        {
+            var dependencySpecification = RustCrateUtilities.GenerateDependencySpecifications(cargoToml, RustCrateUtilities.NonDevDependencyKeys);
+            var devDependencySpecification = RustCrateUtilities.GenerateDependencySpecifications(cargoToml, RustCrateUtilities.DevDependencyKeys);
+
+            // If null, this indicates the toml is an internal file that should not be tracked as a component.
+            if (dependencySpecification != null)
+            {
+                nonDevDependencySpecifications.Add(dependencySpecification);
+            }
+
+            if (devDependencySpecification != null)
+            {
+                devDependencySpecifications.Add(devDependencySpecification);
+            }
+        }
+
+        /// 
+        /// Generate a predicate which will be used to exclude directories which should not contain cargo.toml files.
+        /// 
+        /// The FileInfo for the cargo.lock file found in the root directory.
+        /// A list of relative folder paths to include in search.
+        /// A list of relative folder paths to exclude from search.
+        /// 
+        public static ExcludeDirectoryPredicate BuildExcludeDirectoryPredicateFromWorkspaces(FileInfo rootLockFileInfo, HashSet definedWorkspaces, HashSet definedExclusions)
+        {
+            Dictionary workspaceGlobs = BuildGlobMatchingFromWorkspaces(rootLockFileInfo, definedWorkspaces);
+
+            // Since the paths come in as relative, make them fully qualified
+            HashSet fullyQualifiedExclusions = definedExclusions.Select(x => Path.Combine(rootLockFileInfo.DirectoryName, x)).ToHashSet();
+
+            // The predicate will be evaluated with the current directory name to search and the full path of its parent. Return true when it should be excluded from search.
+            return (ReadOnlySpan nameOfDirectoryToConsider, ReadOnlySpan pathOfParentOfDirectoryToConsider) =>
+            {
+                string currentPath = Path.Combine(pathOfParentOfDirectoryToConsider.ToString(), nameOfDirectoryToConsider.ToString());
+
+                return !workspaceGlobs.Values.Any(x => x.IsMatch(currentPath)) || fullyQualifiedExclusions.Contains(currentPath);
+            };
+        }
+
+        /// 
+        /// Generates a list of Glob compatible Cargo workspace directories which will be searched. See https://docs.rs/glob/0.3.0/glob/struct.Pattern.html for glob patterns.
+        /// 
+        /// The FileInfo for the cargo.lock file found in the root directory.
+        /// A list of relative folder paths to include in search.
+        /// 
+        private static Dictionary BuildGlobMatchingFromWorkspaces(FileInfo rootLockFileInfo, HashSet definedWorkspaces)
+        {
+            Dictionary directoryGlobs = new Dictionary
+            {
+                { rootLockFileInfo.DirectoryName, Glob.Parse(rootLockFileInfo.DirectoryName) },
+            };
+
+            // For the given workspaces, add their paths to search list
+            foreach (string workspace in definedWorkspaces)
+            {
+                string currentPath = rootLockFileInfo.DirectoryName;
+                string[] directoryPathParts = workspace.Split('/');
+
+                // When multiple levels of subdirectory are present, each directory parent must be added or the directory will not be reached
+                // For example, ROOT/test-space/first-test/src/Cargo.toml requires the following directories be matched:
+                // ROOT/test-space, ROOT/test-space/first-test, ROOT/test-space/first-test, ROOT/test-space/first-test/src
+                // Each directory is matched explicitly instead of performing a StartsWith due to the potential of Glob character matching
+                foreach (string pathPart in directoryPathParts)
+                {
+                    currentPath = Path.Combine(currentPath, pathPart);
+                    directoryGlobs[currentPath] = Glob.Parse(currentPath);
+                }
+            }
+
+            return directoryGlobs;
+        }
+
+        public static void BuildGraph(HashSet cargoPackages, IList nonDevDependencies, IList devDependencies, ISingleFileComponentRecorder singleFileComponentRecorder)
+        {
+            // Get all root components that are not dev dependencies
+            // This is a bug:
+            // Say Cargo.toml defined async ^1.0 as a dependency
+            // Say Cargo.lock has async 1.0.0 and async 1.0.2
+            // Both will be marked as root and there's no way to tell which one is "real"
+            IList nonDevRoots = cargoPackages
+                                                .Where(detectedComponent => IsCargoPackageInDependencySpecifications(detectedComponent, nonDevDependencies))
+                                                .ToList();
+
+            // Get all roots that are dev deps
+            IList devRoots = cargoPackages
+                                                .Where(detectedComponent => IsCargoPackageInDependencySpecifications(detectedComponent, devDependencies))
+                                                .ToList();
+
+            var packagesDict = cargoPackages.ToDictionary(cargoPackage => new CargoComponent(cargoPackage.name, cargoPackage.version).Id);
+
+            FollowRoots(packagesDict, devRoots, singleFileComponentRecorder, true);
+            FollowRoots(packagesDict, nonDevRoots, singleFileComponentRecorder, false);
+        }
+
+        private static void FollowRoots(Dictionary packagesDict, IList roots, ISingleFileComponentRecorder singleFileComponentRecorder, bool isDevDependencies)
+        {
+            var componentQueue = new Queue<(string, CargoPackage)>();
+            roots.ToList().ForEach(devRootDetectedComponent => componentQueue.Enqueue((null, devRootDetectedComponent)));
+
+            var visited = new HashSet();
+
+            // All of these components will be dev deps
+            while (componentQueue.Count > 0)
+            {
+                var (parentId, currentPackage) = componentQueue.Dequeue();
+                var currentComponent = CargoPackageToCargoComponent(currentPackage);
+
+                if (visited.Contains(currentComponent.Id))
+                {
+                    continue;
+                }
+
+                if (string.IsNullOrEmpty(parentId)) // This is a root component
+                {
+                    AddOrUpdateDetectedComponent(singleFileComponentRecorder, currentComponent, isDevDependencies, isExplicitReferencedDependency: true);
+                }
+                else
+                {
+                    AddOrUpdateDetectedComponent(singleFileComponentRecorder, currentComponent, isDevDependencies, parentComponentId: parentId);
+                }
+
+                visited.Add(currentComponent.Id);
+
+                if (currentPackage.dependencies != null && currentPackage.dependencies.Any())
+                {
+                    foreach (var dependency in currentPackage.dependencies)
+                    {
+                        var regexMatch = DependencyFormatRegex.Match(dependency);
+                        if (regexMatch.Success)
+                        {
+                            if (SemVersion.TryParse(regexMatch.Groups[2].Value, out SemVersion sv))
+                            {
+                                var name = regexMatch.Groups[1].Value;
+                                var version = sv.ToString();
+                                var source = regexMatch.Groups[3].Value;
+
+                                packagesDict.TryGetValue(new CargoComponent(name, version).Id, out var dependencyPackage);
+
+                                componentQueue.Enqueue((currentComponent.Id, dependencyPackage));
+                            }
+                            else
+                            {
+                                throw new FormatException($"Could not parse {regexMatch.Groups[2].Value} into a valid Semver");
+                            }
+                        }
+                        else
+                        {
+                            throw new FormatException("Could not parse: " + dependency);
+                        }
+                    }
+                }
+            }
+        }
+
+        private static DetectedComponent AddOrUpdateDetectedComponent(
+            ISingleFileComponentRecorder singleFileComponentRecorder,
+            TypedComponent component,
+            bool isDevDependency,
+            string parentComponentId = null,
+            bool isExplicitReferencedDependency = false)
+        {
+            var newComponent = new DetectedComponent(component);
+            singleFileComponentRecorder.RegisterUsage(newComponent, isExplicitReferencedDependency, parentComponentId: parentComponentId, isDevelopmentDependency: isDevDependency);
+            var recordedComponent = singleFileComponentRecorder.GetComponent(newComponent.Component.Id);
+            recordedComponent.DevelopmentDependency &= isDevDependency;
+
+            return recordedComponent;
+        }
+
+        public static DependencySpecification GenerateDependencySpecifications(TomlTable cargoToml, IEnumerable tomlDependencyKeys)
+        {
+            var dependencySpecifications = new DependencySpecification();
+            foreach (var tomlDependencyKey in tomlDependencyKeys)
+            {
+                if (cargoToml.ContainsKey(tomlDependencyKey))
+                {
+                    var dependencies = cargoToml.Get(tomlDependencyKey);
+                    foreach (var dependency in dependencies.Keys)
+                    {
+                        string versionSpecifier;
+                        if (dependencies[dependency].TomlType == TomlObjectType.String)
+                        {
+                            versionSpecifier = dependencies.Get(dependency);
+                        }
+                        else if (dependencies.Get(dependency).ContainsKey("version") && dependencies.Get(dependency).Get("version") != "0.0.0")
+                        {
+                            // We have a valid version that doesn't indicate 'internal' like 0.0.0 does.
+                            versionSpecifier = dependencies.Get(dependency).Get("version");
+                        }
+                        else if (dependencies.Get(dependency).ContainsKey("path"))
+                        {
+                            // If this is a workspace dependency specification that specifies a component by path reference, skip adding it directly here.
+                            // Example: kubos-app = { path = "../../apis/app-api/rust" }
+                            continue;
+                        }
+                        else
+                        {
+                            return null;
+                        }
+
+                        dependencySpecifications.Add(dependency, versionSpecifier);
+                    }
+                }
+            }
+
+            return dependencySpecifications;
+        }
+
+        public static IEnumerable ConvertCargoLockV2PackagesToV1(CargoLock cargoLock)
+        {
+            var packageMap = new Dictionary>();
+            cargoLock.package.ToList().ForEach(package =>
+            {
+                if (!packageMap.TryGetValue(package.name, out var packageList))
+                {
+                    packageMap[package.name] = new List() { package };
+                }
+                else
+                {
+                    packageList.Add(package);
+                }
+            });
+
+            return cargoLock.package.Select(package =>
+            {
+                if (package.dependencies == null)
+                {
+                    return package;
+                }
+
+                try
+                {
+                    // We're just formatting the v2 dependencies in the v1 way
+                    package.dependencies = package.dependencies
+                    .Select(dep =>
+                    {
+                        // parts[0] => name
+                        // parts[1] => version
+                        var parts = dep.Split(' ');
+
+                        // Using v1 format, nothing to change
+                        if (string.IsNullOrEmpty(dep) || parts.Length == 3)
+                        {
+                            return dep;
+                        }
+
+                        // Below 2 cases use v2 format
+                        else if (parts.Length == 1)
+                        {
+                            // There should only be 1 package in packageMap with this name since we don't specify a version
+                            // We want this to throw if we find more than 1 package because it means there is ambiguity about which package is being used
+                            var mappedPackage = packageMap[parts[0]].Single();
+
+                            return MakeDependencyStringFromPackage(mappedPackage);
+                        }
+                        else if (parts.Length == 2)
+                        {
+                            // Search for the package name + version
+                            // Throws if more than 1 for same reason as above
+                            var mappedPackage = packageMap[parts[0]].Where(subPkg => subPkg.version == parts[1]).Single();
+
+                            return MakeDependencyStringFromPackage(mappedPackage);
+                        }
+
+                        throw new FormatException($"Did not expect the dependency string {dep} to have more than 3 parts");
+                    }).ToArray();
+                }
+                catch
+                {
+                    using var record = new RustCrateV2DetectorTelemetryRecord();
+
+                    record.PackageInfo = $"{package.name}, {package.version}, {package.source}";
+                    record.Dependencies = string.Join(',', package.dependencies);
+                }
+
+                return package;
+            });
+        }
+
+        public static string MakeDependencyStringFromPackage(CargoPackage package)
+        {
+            return $"{package.name} {package.version} ({package.source})";
+        }
+
+        private static CargoPackage DependencyStringToCargoPackage(string depString)
+        {
+            var regexMatch = DependencyFormatRegex.Match(depString);
+            if (regexMatch.Success)
+            {
+                if (SemVersion.TryParse(regexMatch.Groups[2].Value, out SemVersion sv))
+                {
+                    CargoPackage dependencyPackage = new CargoPackage();
+                    dependencyPackage.name = regexMatch.Groups[1].Value;
+                    dependencyPackage.version = sv.ToString();
+                    dependencyPackage.source = regexMatch.Groups[3].Value;
+                    return dependencyPackage;
+                }
+
+                throw new FormatException($"Could not parse {regexMatch.Groups[2].Value} into a valid Semver");
+            }
+
+            throw new FormatException("Could not parse: " + depString);
+        }
+
+        private static bool IsCargoPackageInDependencySpecifications(CargoPackage cargoPackage, IList dependencySpecifications)
+        {
+            return dependencySpecifications
+                        .Where(dependencySpecification => dependencySpecification.MatchesPackage(cargoPackage))
+                        .Any();
+        }
+
+        private static TypedComponent CargoPackageToCargoComponent(CargoPackage cargoPackage)
+        {
+            return new CargoComponent(cargoPackage.name, cargoPackage.version);
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/rust/RustCrateV2Detector.cs b/src/Microsoft.ComponentDetection.Detectors/rust/RustCrateV2Detector.cs
new file mode 100644
index 000000000..577191fbb
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/rust/RustCrateV2Detector.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+using System.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.Internal;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Detectors.Rust.Contracts;
+using Nett;
+
+namespace Microsoft.ComponentDetection.Detectors.Rust
+{
+    [Export(typeof(IComponentDetector))]
+    public class RustCrateV2Detector : FileComponentDetector
+    {
+        public override string Id => "RustCrateV2Detector";
+
+        public override IList SearchPatterns => new List { RustCrateUtilities.CargoLockSearchPattern };
+
+        public override IEnumerable SupportedComponentTypes => new[] { ComponentType.Cargo };
+
+        public override int Version { get; } = 3;
+
+        public override IEnumerable Categories => new List { "Rust" };
+
+        protected override Task OnFileFound(ProcessRequest processRequest, IDictionary detectorArgs)
+        {
+            var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder;
+            var cargoLockFile = processRequest.ComponentStream;
+
+            try
+            {
+                var cargoLock = StreamTomlSerializer.Deserialize(cargoLockFile.Stream, TomlSettings.Create()).Get();
+
+                // This makes sure we're only trying to parse Cargo.lock v2 formats
+                if (cargoLock.metadata != null)
+                {
+                    Logger.LogInfo($"Cargo.lock file at {cargoLockFile.Location} contains a metadata section so we're parsing it as the v1 format. The v2 detector will no process it.");
+                    return Task.CompletedTask;
+                }
+
+                FileInfo lockFileInfo = new FileInfo(cargoLockFile.Location);
+                IEnumerable cargoTomlComponentStream = ComponentStreamEnumerableFactory.GetComponentStreams(lockFileInfo.Directory, new List { RustCrateUtilities.CargoTomlSearchPattern }, (name, directoryName) => false, recursivelyScanDirectories: false);
+
+                CargoDependencyData cargoDependencyData = RustCrateUtilities.ExtractRootDependencyAndWorkspaceSpecifications(cargoTomlComponentStream, singleFileComponentRecorder);
+
+                // If workspaces have been defined in the root cargo.toml file, scan for specified cargo.toml manifests
+                int numWorkspaceComponentStreams = 0;
+                int expectedWorkspaceTomlCount = cargoDependencyData.CargoWorkspaces.Count;
+                if (expectedWorkspaceTomlCount > 0)
+                {
+                    string rootCargoTomlLocation = Path.Combine(lockFileInfo.DirectoryName, "Cargo.toml");
+
+                    IEnumerable cargoTomlWorkspaceComponentStreams = ComponentStreamEnumerableFactory.GetComponentStreams(
+                        lockFileInfo.Directory,
+                        new List { RustCrateUtilities.CargoTomlSearchPattern },
+                        RustCrateUtilities.BuildExcludeDirectoryPredicateFromWorkspaces(lockFileInfo, cargoDependencyData.CargoWorkspaces, cargoDependencyData.CargoWorkspaceExclusions),
+                        recursivelyScanDirectories: true)
+                        .Where(x => !x.Location.Equals(rootCargoTomlLocation)); // The root directory needs to be included in directoriesToScan, but should not be reprocessed
+                    numWorkspaceComponentStreams = cargoTomlWorkspaceComponentStreams.Count();
+
+                    // Now that the non-root files have been located, add their dependencies
+                    RustCrateUtilities.ExtractDependencySpecifications(cargoTomlWorkspaceComponentStreams, singleFileComponentRecorder, cargoDependencyData.NonDevDependencies, cargoDependencyData.DevDependencies);
+                }
+
+                // Even though we can't read the file streams, we still have the enumerable!
+                if (!cargoTomlComponentStream.Any() || cargoTomlComponentStream.Count() > 1)
+                {
+                    Logger.LogWarning($"We are expecting exactly 1 accompanying Cargo.toml file next to the cargo.lock file found at {cargoLockFile.Location}");
+                    return Task.CompletedTask;
+                }
+
+                // If there is a mismatch between the number of expected and found workspaces, exit
+                if (expectedWorkspaceTomlCount > numWorkspaceComponentStreams)
+                {
+                    Logger.LogWarning($"We are expecting at least {expectedWorkspaceTomlCount} accompanying Cargo.toml file(s) from workspaces outside of the root directory {lockFileInfo.DirectoryName}, but found {numWorkspaceComponentStreams}");
+                    return Task.CompletedTask;
+                }
+
+                var cargoPackages = RustCrateUtilities.ConvertCargoLockV2PackagesToV1(cargoLock).ToHashSet();
+                RustCrateUtilities.BuildGraph(cargoPackages, cargoDependencyData.NonDevDependencies, cargoDependencyData.DevDependencies, singleFileComponentRecorder);
+            }
+            catch (Exception e)
+            {
+                // If something went wrong, just ignore the file
+                Logger.LogFailedReadingFile(cargoLockFile.Location, e);
+            }
+
+            return Task.CompletedTask;
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/rust/SemVer/Comparator.cs b/src/Microsoft.ComponentDetection.Detectors/rust/SemVer/Comparator.cs
new file mode 100644
index 000000000..7d07cdec4
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/rust/SemVer/Comparator.cs
@@ -0,0 +1,255 @@
+// This file was copied from the SemanticVersioning package found at https://github.com/adamreeve/semver.net.
+// The range logic from SemanticVersioning is needed in the Rust detector to supplement the Semver versioning package
+// that is used elsewhere in this project.
+// 
+// This is a temporary solution, so avoid using this functionality outside of the Rust detector. The following
+// issues describe the problems with the SemanticVersioning package that make it problematic to use for versioning. 
+// https://github.com/adamreeve/semver.net/issues/46
+// https://github.com/adamreeve/semver.net/issues/47
+
+using System;
+using System.Text.RegularExpressions;
+using Semver;
+
+namespace Microsoft.ComponentDetection.Detectors.Rust.SemVer
+{
+    internal class Comparator : IEquatable
+    {
+        public readonly Operator ComparatorType;
+
+        public readonly SemVersion Version;
+
+        private const string RangePattern = @"
+            \s*
+            ([=<>]*)                # Comparator type (can be empty)
+            \s*
+            ([0-9a-zA-Z\-\+\.\*]+)  # Version (potentially partial version)
+            \s*
+            ";
+
+        private static readonly Regex RangePatternRegex = new Regex(
+            RangePattern,
+            RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
+
+        public Comparator(string input)
+        {
+            var match = RangePatternRegex.Match(input);
+            if (!match.Success)
+            {
+                throw new ArgumentException(string.Format("Invalid comparator string: {0}", input));
+            }
+
+            ComparatorType = ParseComparatorType(match.Groups[1].Value);
+            var partialVersion = new PartialVersion(match.Groups[2].Value);
+
+            if (!partialVersion.IsFull())
+            {
+                // For Operator.Equal, partial versions are handled by the StarRange
+                // desugarer, and desugar to multiple comparators.
+                switch (ComparatorType)
+                {
+                    // For <= with a partial version, eg. <=1.2.x, this
+                    // means the same as < 1.3.0, and <=1.x means <2.0
+                    case Operator.LessThanOrEqual:
+                        ComparatorType = Operator.LessThan;
+                        if (!partialVersion.Major.HasValue)
+                        {
+                            // <=* means >=0.0.0
+                            ComparatorType = Operator.GreaterThanOrEqual;
+                            Version = new SemVersion(0, 0, 0);
+                        }
+                        else if (!partialVersion.Minor.HasValue)
+                        {
+                            Version = new SemVersion(partialVersion.Major.Value + 1, 0, 0);
+                        }
+                        else
+                        {
+                            Version = new SemVersion(partialVersion.Major.Value, partialVersion.Minor.Value + 1, 0);
+                        }
+
+                        break;
+                    case Operator.GreaterThan:
+                        ComparatorType = Operator.GreaterThanOrEqual;
+                        if (!partialVersion.Major.HasValue)
+                        {
+                            // >* is unsatisfiable, so use <0.0.0
+                            ComparatorType = Operator.LessThan;
+                            Version = new SemVersion(0, 0, 0);
+                        }
+                        else if (!partialVersion.Minor.HasValue)
+                        {
+                            // eg. >1.x -> >=2.0
+                            Version = new SemVersion(partialVersion.Major.Value + 1, 0, 0);
+                        }
+                        else
+                        {
+                            // eg. >1.2.x -> >=1.3
+                            Version = new SemVersion(partialVersion.Major.Value, partialVersion.Minor.Value + 1, 0);
+                        }
+
+                        break;
+                    default:
+                        // <1.2.x means <1.2.0
+                        // >=1.2.x means >=1.2.0
+                        Version = partialVersion.ToZeroVersion();
+                        break;
+                }
+            }
+            else
+            {
+                Version = partialVersion.ToZeroVersion();
+            }
+        }
+
+        public Comparator(Operator comparatorType, SemVersion comparatorVersion)
+        {
+            if (comparatorVersion == null)
+            {
+                throw new NullReferenceException("Null comparator version");
+            }
+
+            ComparatorType = comparatorType;
+            Version = comparatorVersion;
+        }
+
+        public static Tuple TryParse(string input)
+        {
+            var match = RangePatternRegex.Match(input);
+
+            return match.Success ?
+                Tuple.Create(
+                    match.Length,
+                    new Comparator(match.Value))
+                : null;
+        }
+
+        private static Operator ParseComparatorType(string input)
+        {
+            switch (input)
+            {
+                case "":
+                case "=":
+                    return Operator.Equal;
+                case "<":
+                    return Operator.LessThan;
+                case "<=":
+                    return Operator.LessThanOrEqual;
+                case ">":
+                    return Operator.GreaterThan;
+                case ">=":
+                    return Operator.GreaterThanOrEqual;
+                default:
+                    throw new ArgumentException(string.Format("Invalid comparator type: {0}", input));
+            }
+        }
+
+        public bool IsSatisfied(SemVersion version)
+        {
+            switch (ComparatorType)
+            {
+                case Operator.Equal:
+                    return version == Version;
+                case Operator.LessThan:
+                    return version < Version;
+                case Operator.LessThanOrEqual:
+                    return version <= Version;
+                case Operator.GreaterThan:
+                    return version > Version;
+                case Operator.GreaterThanOrEqual:
+                    return version >= Version;
+                default:
+                    throw new InvalidOperationException("Comparator type not recognised.");
+            }
+        }
+
+        public bool Intersects(Comparator other)
+        {
+            Func operatorIsGreaterThan = c =>
+                c.ComparatorType == Operator.GreaterThan ||
+                c.ComparatorType == Operator.GreaterThanOrEqual;
+            Func operatorIsLessThan = c =>
+                c.ComparatorType == Operator.LessThan ||
+                c.ComparatorType == Operator.LessThanOrEqual;
+            Func operatorIncludesEqual = c =>
+                c.ComparatorType == Operator.GreaterThanOrEqual ||
+                c.ComparatorType == Operator.Equal ||
+                c.ComparatorType == Operator.LessThanOrEqual;
+
+            if (Version > other.Version && (operatorIsLessThan(this) || operatorIsGreaterThan(other)))
+            {
+                return true;
+            }
+
+            if (Version < other.Version && (operatorIsGreaterThan(this) || operatorIsLessThan(other)))
+            {
+                return true;
+            }
+
+            if (Version == other.Version && (
+                (operatorIncludesEqual(this) && operatorIncludesEqual(other)) ||
+                (operatorIsLessThan(this) && operatorIsLessThan(other)) ||
+                (operatorIsGreaterThan(this) && operatorIsGreaterThan(other))))
+            {
+                return true;
+            }
+
+            return false;
+        }
+
+        public enum Operator
+        {
+            Equal = 0,
+            LessThan,
+            LessThanOrEqual,
+            GreaterThan,
+            GreaterThanOrEqual,
+        }
+
+        public override string ToString()
+        {
+            string operatorString = null;
+            switch (ComparatorType)
+            {
+                case Operator.Equal:
+                    operatorString = "=";
+                    break;
+                case Operator.LessThan:
+                    operatorString = "<";
+                    break;
+                case Operator.LessThanOrEqual:
+                    operatorString = "<=";
+                    break;
+                case Operator.GreaterThan:
+                    operatorString = ">";
+                    break;
+                case Operator.GreaterThanOrEqual:
+                    operatorString = ">=";
+                    break;
+                default:
+                    throw new InvalidOperationException("Comparator type not recognised.");
+            }
+
+            return string.Format("{0}{1}", operatorString, Version);
+        }
+
+        public bool Equals(Comparator other)
+        {
+            if (ReferenceEquals(other, null))
+            {
+                return false;
+            }
+
+            return ComparatorType == other.ComparatorType && Version == other.Version;
+        }
+
+        public override bool Equals(object other)
+        {
+            return Equals(other as Comparator);
+        }
+
+        public override int GetHashCode()
+        {
+            return ToString().GetHashCode();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Microsoft.ComponentDetection.Detectors/rust/SemVer/ComparatorSet.cs b/src/Microsoft.ComponentDetection.Detectors/rust/SemVer/ComparatorSet.cs
new file mode 100644
index 000000000..604e10da9
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/rust/SemVer/ComparatorSet.cs
@@ -0,0 +1,194 @@
+// This file was copied from the SemanticVersioning package found at https://github.com/adamreeve/semver.net.
+// The range logic from SemanticVersioning is needed in the Rust detector to supplement the Semver versioning package
+// that is used elsewhere in this project.
+// 
+// This is a temporary solution, so avoid using this functionality outside of the Rust detector. The following
+// issues describe the problems with the SemanticVersioning package that make it problematic to use for versioning. 
+// https://github.com/adamreeve/semver.net/issues/46
+// https://github.com/adamreeve/semver.net/issues/47
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Semver;
+
+namespace Microsoft.ComponentDetection.Detectors.Rust.SemVer
+{
+    internal class ComparatorSet : IEquatable
+    {
+        private readonly List comparators;
+
+        public ComparatorSet(string spec)
+        {
+            comparators = new List { };
+
+            spec = spec.Trim();
+            if (spec == string.Empty)
+            {
+                spec = "*";
+            }
+
+            int position = 0;
+            int end = spec.Length;
+
+            while (position < end)
+            {
+                int iterStartPosition = position;
+
+                // A comparator set might be an advanced range specifier
+                // like ~1.2.3, ^1.2, or 1.*.
+                // Check for these first before standard comparators:
+                foreach (var desugarer in new Func>[]
+                {
+                    Desugarer.HyphenRange,
+                    Desugarer.TildeRange,
+                    Desugarer.CaretRange,
+                    Desugarer.StarRange,
+                })
+                {
+                    var result = desugarer(spec.Substring(position));
+                    if (result != null)
+                    {
+                        position += result.Item1;
+                        comparators.AddRange(result.Item2);
+                    }
+                }
+
+                // Check for standard comparator with operator and version:
+                var comparatorResult = Comparator.TryParse(spec.Substring(position));
+                if (comparatorResult != null)
+                {
+                    position += comparatorResult.Item1;
+                    comparators.Add(comparatorResult.Item2);
+                }
+
+                if (position == iterStartPosition)
+                {
+                    // Didn't manage to read any valid comparators
+                    throw new ArgumentException(string.Format("Invalid range specification: \"{0}\"", spec));
+                }
+            }
+        }
+
+        private ComparatorSet(IEnumerable comparators)
+        {
+            this.comparators = comparators.ToList();
+        }
+
+        public bool IsSatisfied(SemVersion version)
+        {
+            bool satisfied = comparators.All(c => c.IsSatisfied(version));
+            if (version.Prerelease != string.Empty)
+            {
+                // If the version is a pre-release, then one of the
+                // comparators must have the same version and also include
+                // a pre-release tag.
+                return satisfied && comparators.Any(c =>
+                        c.Version.Prerelease != string.Empty &&
+                        c.Version.Major == version.Major &&
+                        c.Version.Minor == version.Minor &&
+                        c.Version.Patch == version.Patch);
+            }
+            else
+            {
+                return satisfied;
+            }
+        }
+
+        public ComparatorSet Intersect(ComparatorSet other)
+        {
+            Func operatorIsGreaterThan = c =>
+                c.ComparatorType == Comparator.Operator.GreaterThan ||
+                c.ComparatorType == Comparator.Operator.GreaterThanOrEqual;
+            Func operatorIsLessThan = c =>
+                c.ComparatorType == Comparator.Operator.LessThan ||
+                c.ComparatorType == Comparator.Operator.LessThanOrEqual;
+            var maxOfMins =
+                this.comparators.Concat(other.comparators)
+                .Where(operatorIsGreaterThan)
+                .OrderByDescending(c => c.Version).FirstOrDefault();
+            var minOfMaxs =
+                this.comparators.Concat(other.comparators)
+                .Where(operatorIsLessThan)
+                .OrderBy(c => c.Version).FirstOrDefault();
+            if (maxOfMins != null && minOfMaxs != null && !maxOfMins.Intersects(minOfMaxs))
+            {
+                return null;
+            }
+
+            // If there is an equality operator, check that it satisfies other operators
+            var equalityVersions =
+                this.comparators.Concat(other.comparators)
+                .Where(c => c.ComparatorType == Comparator.Operator.Equal)
+                .Select(c => c.Version)
+                .ToList();
+            if (equalityVersions.Count > 1)
+            {
+                if (equalityVersions.Any(v => v != equalityVersions[0]))
+                {
+                    return null;
+                }
+            }
+
+            if (equalityVersions.Count > 0)
+            {
+                if (maxOfMins != null && !maxOfMins.IsSatisfied(equalityVersions[0]))
+                {
+                    return null;
+                }
+
+                if (minOfMaxs != null && !minOfMaxs.IsSatisfied(equalityVersions[0]))
+                {
+                    return null;
+                }
+
+                return new ComparatorSet(
+                    new List
+                    {
+                        new Comparator(Comparator.Operator.Equal, equalityVersions[0]),
+                    });
+            }
+
+            var comparators = new List();
+            if (maxOfMins != null)
+            {
+                comparators.Add(maxOfMins);
+            }
+
+            if (minOfMaxs != null)
+            {
+                comparators.Add(minOfMaxs);
+            }
+
+            return comparators.Count > 0 ? new ComparatorSet(comparators) : null;
+        }
+
+        public bool Equals(ComparatorSet other)
+        {
+            if (ReferenceEquals(other, null))
+            {
+                return false;
+            }
+
+            var thisSet = new HashSet(comparators);
+            return thisSet.SetEquals(other.comparators);
+        }
+
+        public override bool Equals(object other)
+        {
+            return Equals(other as ComparatorSet);
+        }
+
+        public override string ToString()
+        {
+            return string.Join(" ", comparators.Select(c => c.ToString()).ToArray());
+        }
+
+        public override int GetHashCode()
+        {
+            // XOR is commutative, so this hash code is independent
+            // of the order of comparators.
+            return comparators.Aggregate(0, (accum, next) => accum ^ next.GetHashCode());
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Microsoft.ComponentDetection.Detectors/rust/SemVer/Desugarer.cs b/src/Microsoft.ComponentDetection.Detectors/rust/SemVer/Desugarer.cs
new file mode 100644
index 000000000..b7f0bcc90
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/rust/SemVer/Desugarer.cs
@@ -0,0 +1,244 @@
+// This file was copied from the SemanticVersioning package found at https://github.com/adamreeve/semver.net.
+// The range logic from SemanticVersioning is needed in the Rust detector to supplement the Semver versioning package
+// that is used elsewhere in this project.
+// 
+// This is a temporary solution, so avoid using this functionality outside of the Rust detector. The following
+// issues describe the problems with the SemanticVersioning package that make it problematic to use for versioning. 
+// https://github.com/adamreeve/semver.net/issues/46
+// https://github.com/adamreeve/semver.net/issues/47
+
+using System;
+using System.Text.RegularExpressions;
+using Semver;
+
+namespace Microsoft.ComponentDetection.Detectors.Rust.SemVer
+{
+    internal static class Desugarer
+    {
+        private const string VersionChars = @"[0-9a-zA-Z\-\+\.\*]";
+
+        private static readonly Regex TildePatternRegex = new Regex(
+            $@"^\s*~\s*({VersionChars}+)\s*",
+            RegexOptions.Compiled);
+
+        private static readonly Regex CaretPatternRegex = new Regex(
+           $@"^\s*\^\s*({VersionChars}+)\s*",
+           RegexOptions.Compiled);
+
+        private static readonly Regex HyphenPatternRegex = new Regex(
+            $@"^\s*({VersionChars}+)\s+\-\s+({VersionChars}+)\s*",
+            RegexOptions.Compiled);
+
+        private static readonly Regex StarPatternRegex = new Regex(
+            $@"^\s*=?\s*({VersionChars}+)\s*",
+            RegexOptions.Compiled);
+
+        // Allows patch-level changes if a minor version is specified
+        // on the comparator. Allows minor-level changes if not.
+        public static Tuple TildeRange(string spec)
+        {
+            var match = TildePatternRegex.Match(spec);
+            if (!match.Success)
+            {
+                return null;
+            }
+
+            SemVersion minVersion = null;
+            SemVersion maxVersion = null;
+
+            var version = new PartialVersion(match.Groups[1].Value);
+            if (version.Minor.HasValue)
+            {
+                // Doesn't matter whether patch version is null or not,
+                // the logic is the same, min patch version will be zero if null.
+                minVersion = version.ToZeroVersion();
+                maxVersion = new SemVersion(version.Major.Value, version.Minor.Value + 1, 0);
+            }
+            else
+            {
+                minVersion = version.ToZeroVersion();
+                maxVersion = new SemVersion(version.Major.Value + 1, 0, 0);
+            }
+
+            return Tuple.Create(
+                    match.Length,
+                    MinMaxComparators(minVersion, maxVersion));
+        }
+
+        // Allows changes that do not modify the left-most non-zero digit
+        // in the [major, minor, patch] tuple.
+        public static Tuple CaretRange(string spec)
+        {
+            var match = CaretPatternRegex.Match(spec);
+            if (!match.Success)
+            {
+                return null;
+            }
+
+            SemVersion minVersion = null;
+            SemVersion maxVersion = null;
+
+            var version = new PartialVersion(match.Groups[1].Value);
+
+            if (version.Major.Value > 0)
+            {
+                // Don't allow major version change
+                minVersion = version.ToZeroVersion();
+                maxVersion = new SemVersion(version.Major.Value + 1, 0, 0);
+            }
+            else if (!version.Minor.HasValue)
+            {
+                // Don't allow major version change, even if it's zero
+                minVersion = version.ToZeroVersion();
+                maxVersion = new SemVersion(version.Major.Value + 1, 0, 0);
+            }
+            else if (!version.Patch.HasValue)
+            {
+                // Don't allow minor version change, even if it's zero
+                minVersion = version.ToZeroVersion();
+                maxVersion = new SemVersion(0, version.Minor.Value + 1, 0);
+            }
+            else if (version.Minor > 0)
+            {
+                // Don't allow minor version change
+                minVersion = version.ToZeroVersion();
+                maxVersion = new SemVersion(0, version.Minor.Value + 1, 0);
+            }
+            else
+            {
+                // Only patch non-zero, don't allow patch change
+                minVersion = version.ToZeroVersion();
+                maxVersion = new SemVersion(0, 0, version.Patch.Value + 1);
+            }
+
+            return Tuple.Create(
+                    match.Length,
+                    MinMaxComparators(minVersion, maxVersion));
+        }
+
+        public static Tuple HyphenRange(string spec)
+        {
+            var match = HyphenPatternRegex.Match(spec);
+            if (!match.Success)
+            {
+                return null;
+            }
+
+            PartialVersion minPartialVersion = null;
+            PartialVersion maxPartialVersion = null;
+
+            // Parse versions from lower and upper ranges, which might
+            // be partial versions.
+            try
+            {
+                minPartialVersion = new PartialVersion(match.Groups[1].Value);
+                maxPartialVersion = new PartialVersion(match.Groups[2].Value);
+            }
+            catch (ArgumentException)
+            {
+                return null;
+            }
+
+            // Lower range has any non-supplied values replaced with zero
+            var minVersion = minPartialVersion.ToZeroVersion();
+
+            Comparator.Operator maxOperator = maxPartialVersion.IsFull()
+                ? Comparator.Operator.LessThanOrEqual : Comparator.Operator.LessThan;
+
+            SemVersion maxVersion = null;
+
+            // Partial upper range means supplied version values can't change
+            if (!maxPartialVersion.Major.HasValue)
+            {
+                // eg. upper range = "*", then maxVersion remains null
+                // and there's only a minimum
+            }
+            else if (!maxPartialVersion.Minor.HasValue)
+            {
+                maxVersion = new SemVersion(maxPartialVersion.Major.Value + 1, 0, 0);
+            }
+            else if (!maxPartialVersion.Patch.HasValue)
+            {
+                maxVersion = new SemVersion(maxPartialVersion.Major.Value, maxPartialVersion.Minor.Value + 1, 0);
+            }
+            else
+            {
+                // Fully specified max version
+                maxVersion = maxPartialVersion.ToZeroVersion();
+            }
+
+            return Tuple.Create(
+                    match.Length,
+                    MinMaxComparators(minVersion, maxVersion, maxOperator));
+        }
+
+        public static Tuple StarRange(string spec)
+        {
+            var match = StarPatternRegex.Match(spec);
+
+            if (!match.Success)
+            {
+                return null;
+            }
+
+            PartialVersion version = null;
+            try
+            {
+                version = new PartialVersion(match.Groups[1].Value);
+            }
+            catch (ArgumentException)
+            {
+                return null;
+            }
+
+            // If partial version match is actually a full version,
+            // then this isn't a star range, so return null.
+            if (version.IsFull())
+            {
+                return null;
+            }
+
+            SemVersion minVersion = null;
+            SemVersion maxVersion = null;
+
+            if (!version.Major.HasValue)
+            {
+                minVersion = version.ToZeroVersion();
+
+                // no max version
+            }
+            else if (!version.Minor.HasValue)
+            {
+                minVersion = version.ToZeroVersion();
+                maxVersion = new SemVersion(version.Major.Value + 1, 0, 0);
+            }
+            else
+            {
+                minVersion = version.ToZeroVersion();
+                maxVersion = new SemVersion(version.Major.Value, version.Minor.Value + 1, 0);
+            }
+
+            return Tuple.Create(
+                    match.Length,
+                    MinMaxComparators(minVersion, maxVersion));
+        }
+
+        private static Comparator[] MinMaxComparators(SemVersion minVersion, SemVersion maxVersion,
+                Comparator.Operator maxOperator = Comparator.Operator.LessThan)
+        {
+            var minComparator = new Comparator(
+                    Comparator.Operator.GreaterThanOrEqual,
+                    minVersion);
+            if (maxVersion == null)
+            {
+                return new[] { minComparator };
+            }
+            else
+            {
+                var maxComparator = new Comparator(
+                        maxOperator, maxVersion);
+                return new[] { minComparator, maxComparator };
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Microsoft.ComponentDetection.Detectors/rust/SemVer/PartialVersion.cs b/src/Microsoft.ComponentDetection.Detectors/rust/SemVer/PartialVersion.cs
new file mode 100644
index 000000000..cd7c9c604
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/rust/SemVer/PartialVersion.cs
@@ -0,0 +1,115 @@
+// This file was copied from the SemanticVersioning package found at https://github.com/adamreeve/semver.net.
+// The range logic from SemanticVersioning is needed in the Rust detector to supplement the Semver versioning package
+// that is used elsewhere in this project.
+// 
+// This is a temporary solution, so avoid using this functionality outside of the Rust detector. The following
+// issues describe the problems with the SemanticVersioning package that make it problematic to use for versioning. 
+// https://github.com/adamreeve/semver.net/issues/46
+// https://github.com/adamreeve/semver.net/issues/47
+
+using System;
+using System.Linq;
+using System.Text.RegularExpressions;
+using Semver;
+
+namespace Microsoft.ComponentDetection.Detectors.Rust.SemVer
+{
+    // A version that might not have a minor or patch
+    // number, for use in ranges like "^1.2" or "2.x"
+    internal class PartialVersion
+    {
+        public int? Major { get; set; }
+
+        public int? Minor { get; set; }
+
+        public int? Patch { get; set; }
+
+        public string PreRelease { get; set; }
+
+        private static readonly Regex VersionRegex = new Regex(
+            @"^
+                [v=\s]*
+                (\d+|[Xx\*])                      # major version
+                (
+                    \.
+                    (\d+|[Xx\*])                  # minor version
+                    (
+                        \.
+                        (\d+|[Xx\*])              # patch version
+                        (\-?([0-9A-Za-z\-\.]+))?  # pre-release version
+                        (\+([0-9A-Za-z\-\.]+))?   # build version (ignored)
+                    )?
+                )?
+                $",
+            RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);
+
+        public PartialVersion(string input)
+        {
+            string[] xValues = { "X", "x", "*" };
+
+            if (input.Trim() == string.Empty)
+            {
+                // Empty input means any version
+                return;
+            }
+
+            var match = VersionRegex.Match(input);
+            if (!match.Success)
+            {
+                throw new ArgumentException(string.Format("Invalid version string: \"{0}\"", input));
+            }
+
+            if (xValues.Contains(match.Groups[1].Value))
+            {
+                Major = null;
+            }
+            else
+            {
+                Major = int.Parse(match.Groups[1].Value);
+            }
+
+            if (match.Groups[2].Success)
+            {
+                if (xValues.Contains(match.Groups[3].Value))
+                {
+                    Minor = null;
+                }
+                else
+                {
+                    Minor = int.Parse(match.Groups[3].Value);
+                }
+            }
+
+            if (match.Groups[4].Success)
+            {
+                if (xValues.Contains(match.Groups[5].Value))
+                {
+                    Patch = null;
+                }
+                else
+                {
+                    Patch = int.Parse(match.Groups[5].Value);
+                }
+            }
+
+            if (match.Groups[6].Success)
+            {
+                PreRelease = match.Groups[7].Value;
+            }
+        }
+
+        public SemVersion ToZeroVersion()
+        {
+            return new SemVersion(
+                    Major ?? 0,
+                    Minor ?? 0,
+                    Patch ?? 0,
+                    PreRelease);
+        }
+
+        public bool IsFull()
+        {
+            return Major.HasValue && Minor.HasValue && Patch.HasValue;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Microsoft.ComponentDetection.Detectors/rust/SemVer/Range.cs b/src/Microsoft.ComponentDetection.Detectors/rust/SemVer/Range.cs
new file mode 100644
index 000000000..751997846
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/rust/SemVer/Range.cs
@@ -0,0 +1,250 @@
+// This file was copied from the SemanticVersioning package found at https://github.com/adamreeve/semver.net.
+// The range logic from SemanticVersioning is needed in the Rust detector to supplement the Semver versioning package
+// that is used elsewhere in this project.
+// 
+// This is a temporary solution, so avoid using this functionality outside of the Rust detector. The following
+// issues describe the problems with the SemanticVersioning package that make it problematic to use for versioning. 
+// https://github.com/adamreeve/semver.net/issues/46
+// https://github.com/adamreeve/semver.net/issues/47
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Semver;
+
+namespace Microsoft.ComponentDetection.Detectors.Rust.SemVer
+{
+    /// 
+    /// Specifies valid versions.
+    /// 
+    public class Range : IEquatable
+    {
+        private readonly ComparatorSet[] comparatorSets;
+
+        private readonly string rangeSpec;
+
+        /// 
+        /// Construct a new range from a range specification.
+        /// 
+        /// The range specification string.
+        /// When true, be more forgiving of some invalid version specifications.
+        /// Thrown when the range specification is invalid.
+        public Range(string rangeSpec, bool loose = false)
+        {
+            this.rangeSpec = rangeSpec;
+            var comparatorSetSpecs = rangeSpec.Split(new[] { "||" }, StringSplitOptions.None);
+            comparatorSets = comparatorSetSpecs.Select(s => new ComparatorSet(s)).ToArray();
+        }
+
+        private Range(IEnumerable comparatorSets)
+        {
+            this.comparatorSets = comparatorSets.ToArray();
+            rangeSpec = string.Join(" || ", comparatorSets.Select(cs => cs.ToString()).ToArray());
+        }
+
+        /// 
+        /// Determine whether the given version satisfies this range.
+        /// 
+        /// The version to check.
+        /// true if the range is satisfied by the version.
+        public bool IsSatisfied(SemVersion version)
+        {
+            return comparatorSets.Any(s => s.IsSatisfied(version));
+        }
+
+        /// 
+        /// Determine whether the given version satisfies this range.
+        /// With an invalid version this method returns false.
+        /// 
+        /// The version to check.
+        /// When true, be more forgiving of some invalid version specifications.
+        /// true if the range is satisfied by the version.
+        public bool IsSatisfied(string versionString, bool loose = false)
+        {
+            try
+            {
+                SemVersion.TryParse(versionString, out SemVersion version, loose);
+                return IsSatisfied(version);
+            }
+            catch (ArgumentException)
+            {
+                return false;
+            }
+        }
+
+        /// 
+        /// Return the set of versions that satisfy this range.
+        /// 
+        /// The versions to check.
+        /// An IEnumerable of satisfying versions.
+        public IEnumerable Satisfying(IEnumerable versions)
+        {
+            return versions.Where(IsSatisfied);
+        }
+
+        /// 
+        /// Return the set of version strings that satisfy this range.
+        /// Invalid version specifications are skipped.
+        /// 
+        /// The version strings to check.
+        /// When true, be more forgiving of some invalid version specifications.
+        /// An IEnumerable of satisfying version strings.
+        public IEnumerable Satisfying(IEnumerable versions, bool loose = false)
+        {
+            return versions.Where(v => IsSatisfied(v, loose));
+        }
+
+        /// 
+        /// Return the maximum version that satisfies this range.
+        /// 
+        /// The versions to select from.
+        /// The maximum satisfying version, or null if no versions satisfied this range.
+        public SemVersion MaxSatisfying(IEnumerable versions)
+        {
+            return Satisfying(versions).Max();
+        }
+
+        /// 
+        /// Return the maximum version that satisfies this range.
+        /// 
+        /// The version strings to select from.
+        /// When true, be more forgiving of some invalid version specifications.
+        /// The maximum satisfying version string, or null if no versions satisfied this range.
+        public string MaxSatisfying(IEnumerable versionStrings, bool loose = false)
+        {
+            var versions = ValidVersions(versionStrings, loose);
+            var maxVersion = MaxSatisfying(versions);
+            return maxVersion == null ? null : maxVersion.ToString();
+        }
+
+        /// 
+        /// Calculate the intersection between two ranges.
+        /// 
+        /// The Range to intersect this Range with.
+        /// The Range intersection.
+        public Range Intersect(Range other)
+        {
+            var allIntersections = comparatorSets.SelectMany(
+                thisCs => other.comparatorSets.Select(thisCs.Intersect))
+                .Where(cs => cs != null).ToList();
+
+            if (allIntersections.Count == 0)
+            {
+                return new Range("<0.0.0");
+            }
+
+            return new Range(allIntersections);
+        }
+
+        /// 
+        /// Returns the range specification string used when constructing this range.
+        /// 
+        /// The range string.
+        public override string ToString()
+        {
+            return rangeSpec;
+        }
+
+        public bool Equals(Range other)
+        {
+            if (ReferenceEquals(other, null))
+            {
+                return false;
+            }
+
+            var thisSet = new HashSet(comparatorSets);
+            return thisSet.SetEquals(other.comparatorSets);
+        }
+
+        public override bool Equals(object other)
+        {
+            return Equals(other as Range);
+        }
+
+        public static bool operator ==(Range a, Range b)
+        {
+            if (ReferenceEquals(a, null))
+            {
+                return ReferenceEquals(b, null);
+            }
+
+            return a.Equals(b);
+        }
+
+        public static bool operator !=(Range a, Range b)
+        {
+            return !(a == b);
+        }
+
+        public override int GetHashCode()
+        {
+            // XOR is commutative, so this hash code is independent
+            // of the order of comparators.
+            return comparatorSets.Aggregate(0, (accum, next) => accum ^ next.GetHashCode());
+        }
+
+        // Static convenience methods
+
+        /// 
+        /// Determine whether the given version satisfies a given range.
+        /// With an invalid version this method returns false.
+        /// 
+        /// The range specification.
+        /// The version to check.
+        /// When true, be more forgiving of some invalid version specifications.
+        /// true if the range is satisfied by the version.
+        public static bool IsSatisfied(string rangeSpec, string versionString, bool loose = false)
+        {
+            var range = new Range(rangeSpec);
+            return range.IsSatisfied(versionString);
+        }
+
+        /// 
+        /// Return the set of version strings that satisfy a given range.
+        /// Invalid version specifications are skipped.
+        /// 
+        /// The range specification.
+        /// The version strings to check.
+        /// When true, be more forgiving of some invalid version specifications.
+        /// An IEnumerable of satisfying version strings.
+        public static IEnumerable Satisfying(string rangeSpec, IEnumerable versions, bool loose = false)
+        {
+            var range = new Range(rangeSpec);
+            return range.Satisfying(versions);
+        }
+
+        /// 
+        /// Return the maximum version that satisfies a given range.
+        /// 
+        /// The range specification.
+        /// The version strings to select from.
+        /// When true, be more forgiving of some invalid version specifications.
+        /// The maximum satisfying version string, or null if no versions satisfied this range.
+        public static string MaxSatisfying(string rangeSpec, IEnumerable versionStrings, bool loose = false)
+        {
+            var range = new Range(rangeSpec);
+            return range.MaxSatisfying(versionStrings);
+        }
+
+        private IEnumerable ValidVersions(IEnumerable versionStrings, bool loose)
+        {
+            foreach (var v in versionStrings)
+            {
+                SemVersion version = null;
+                try
+                {
+                    SemVersion.TryParse(v, out version, loose);
+                }
+                catch (ArgumentException)
+                {
+                    // Skip
+                }
+
+                if (version != null)
+                {
+                    yield return version;
+                }
+            }
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/rust/SemVer/cgmanifest.json b/src/Microsoft.ComponentDetection.Detectors/rust/SemVer/cgmanifest.json
new file mode 100644
index 000000000..076add2c0
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/rust/SemVer/cgmanifest.json
@@ -0,0 +1,14 @@
+{
+  "Registrations": [
+    {
+      "Component": {
+        "Type": "git",
+        "git": {
+          "RepositoryUrl": "https://github.com/adamreeve/semver.net/",
+          "CommitHash": "efe3e2e87276ba6b231fc73898ed1651e584f354"
+        }
+      },
+      "DevelopmentDependency": false
+    }
+  ]
+}
\ No newline at end of file
diff --git a/src/Microsoft.ComponentDetection.Detectors/yarn/IYarnLockParser.cs b/src/Microsoft.ComponentDetection.Detectors/yarn/IYarnLockParser.cs
new file mode 100644
index 000000000..f9e8673e9
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/yarn/IYarnLockParser.cs
@@ -0,0 +1,12 @@
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Detectors.Yarn.Parsers;
+
+namespace Microsoft.ComponentDetection.Detectors.Yarn
+{
+    public interface IYarnLockParser
+    {
+        bool CanParse(YarnLockVersion yarnLockVersion);
+
+        YarnLockFile Parse(IYarnBlockFile fileLines, ILogger logger);
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/yarn/InvalidYarnLockFileException.cs b/src/Microsoft.ComponentDetection.Detectors/yarn/InvalidYarnLockFileException.cs
new file mode 100644
index 000000000..6ff293bdb
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/yarn/InvalidYarnLockFileException.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Microsoft.ComponentDetection.Detectors.Yarn
+{
+    public class InvalidYarnLockFileException : Exception
+    {
+        public InvalidYarnLockFileException()
+        {
+        }
+
+        public InvalidYarnLockFileException(string message)
+            : base(message)
+        {
+        }
+
+        public InvalidYarnLockFileException(string message, Exception innerException)
+            : base(message, innerException)
+        {
+        }
+
+        protected InvalidYarnLockFileException(SerializationInfo info, StreamingContext context)
+            : base(info, context)
+        {
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/yarn/Parsers/IYarnBlockFile.cs b/src/Microsoft.ComponentDetection.Detectors/yarn/Parsers/IYarnBlockFile.cs
new file mode 100644
index 000000000..f1df8b5fa
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/yarn/Parsers/IYarnBlockFile.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace Microsoft.ComponentDetection.Detectors.Yarn.Parsers
+{
+    public interface IYarnBlockFile : IEnumerable
+    {
+        YarnLockVersion YarnLockVersion { get; set; }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/yarn/Parsers/YarnBlock.cs b/src/Microsoft.ComponentDetection.Detectors/yarn/Parsers/YarnBlock.cs
new file mode 100644
index 000000000..393f7514d
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/yarn/Parsers/YarnBlock.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+
+namespace Microsoft.ComponentDetection.Detectors.Yarn.Parsers
+{
+    public class YarnBlock
+    {
+        /// 
+        /// Gets or sets the first line of the block, without the semicolon.
+        /// 
+        public string Title { get; set; }
+
+        /// 
+        /// Gets the key/value pairs that the block contains.
+        /// 
+        public IDictionary Values { get; } = new Dictionary();
+
+        /// 
+        /// Gets child blocks, as dentoed by "{child}:".
+        /// 
+        public IList Children { get; } = new List();
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/yarn/Parsers/YarnBlockFile.cs b/src/Microsoft.ComponentDetection.Detectors/yarn/Parsers/YarnBlockFile.cs
new file mode 100644
index 000000000..84ecc45fc
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/yarn/Parsers/YarnBlockFile.cs
@@ -0,0 +1,247 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+
+namespace Microsoft.ComponentDetection.Detectors.Yarn.Parsers
+{
+    /// 
+    /// https://github.com/yarnpkg/yarn/issues/5629
+    /// 
+    /// Yarn uses something that is "Almost-YAML", and we haven't found a YAML parser 
+    ///  that understands Almost-YAML yet, so here's ours...
+    ///
+    /// In V1, this represents a file of newline delimited blocks in the form:
+    ///
+    /// {blockName}:
+    ///   {key} "{value}"
+    ///   {foo} "{bar}"
+    ///   {subBlock}:
+    ///     {key} "{value}"
+    ///   {baz} "{bar}"
+    ///
+    /// {otherBlockName}:
+    ///   {key} "{value}"
+    ///   ...
+    ///
+    /// In V2, pure YAML is the new standard. This represents a file of newline delimited blocks
+    /// in the form where values can optionally be wrapped in quotes:
+    /// {blockName}:
+    ///   {key}: value
+    ///   {foo}: "bar"
+    ///   {subBlock}:
+    ///     {key}: {value}
+    ///   {baz}: {bar}
+    ///
+    /// {otherBlockName}:
+    ///   {key}: "{value}"
+    ///   ...
+    /// 
+    public class YarnBlockFile : IYarnBlockFile
+    {
+        private static readonly Regex YarnV1Regex = new Regex("(.*)\\s\"(.*)\"", RegexOptions.Compiled);
+
+        private static readonly Regex YarnV2Regex = new Regex("(.*):\\s\"?(.*)", RegexOptions.Compiled);
+
+        private int fileLineIndex = 0;
+
+        private readonly IList fileLines = new List();
+
+        public string VersionHeader { get; set; }
+
+        public YarnLockVersion YarnLockVersion { get; set; }
+
+        private YarnBlockFile(IList parsedFileLines)
+        {
+            fileLines = parsedFileLines;
+
+            if (fileLines.Count > 0)
+            {
+                ReadVersionHeader();
+            }
+            else
+            {
+                VersionHeader = string.Empty;
+                YarnLockVersion = YarnLockVersion.Invalid;
+            }
+        }
+
+        public static async Task CreateBlockFileAsync(Stream stream)
+        {
+            if (stream == null)
+            {
+                throw new ArgumentNullException(nameof(stream));
+            }
+
+            List fileLines = new List();
+            using (StreamReader reader = new StreamReader(stream))
+            {
+                while (!reader.EndOfStream)
+                {
+                    fileLines.Add(await reader.ReadLineAsync());
+                }
+            }
+
+            return new YarnBlockFile(fileLines);
+        }
+
+        public IEnumerator GetEnumerator()
+        {
+            while (ReadToNextMajorBlock())
+            {
+                yield return ParseBlock();
+            }
+
+            yield break;
+        }
+
+        private void ReadVersionHeader()
+        {
+            YarnLockVersion = YarnLockVersion.Invalid;
+
+            do
+            {
+                if (fileLines[fileLineIndex].StartsWith("#"))
+                {
+                    if (fileLines[fileLineIndex].Contains("yarn lockfile"))
+                    {
+                        YarnLockVersion = YarnLockVersion.V1;
+                        VersionHeader = fileLines[fileLineIndex];
+                        break;
+                    }
+                }
+                else if (string.IsNullOrEmpty(fileLines[fileLineIndex]))
+                {
+                    // If the comment header does not specify V1, a V2 metadata block will follow a line break
+                    if (IncrementIndex())
+                    {
+                        if (fileLines[fileLineIndex].StartsWith("__metadata:"))
+                        {
+                            VersionHeader = fileLines[fileLineIndex];
+                            YarnLockVersion = YarnLockVersion.V2;
+
+                            YarnBlock metadataBlock = ParseBlock();
+
+                            if (metadataBlock.Values.ContainsKey("version") && metadataBlock.Values.ContainsKey("cacheKey"))
+                            {
+                                break;
+                            }
+
+                            VersionHeader = null;
+                            YarnLockVersion = YarnLockVersion.Invalid;
+                        }
+                    }
+                }
+                else
+                {
+                    break;
+                }
+            }
+            while (IncrementIndex());
+        }
+
+        /// 
+        /// Parses a block and its sub-blocks into .
+        /// 
+        /// 
+        /// 
+        private YarnBlock ParseBlock(int level = 0)
+        {
+            string currentLevelDelimiter = "  ";
+            for (int i = 0; i < level; i++)
+            {
+                currentLevelDelimiter = currentLevelDelimiter + "  ";
+            }
+
+            // Assuming the pointer has been set up to a block
+            YarnBlock block = new YarnBlock { Title = fileLines[fileLineIndex].TrimEnd(':').Trim('\"').Trim() };
+
+            while (IncrementIndex())
+            {
+                if (!fileLines[fileLineIndex].StartsWith(currentLevelDelimiter) || string.IsNullOrWhiteSpace(fileLines[fileLineIndex]))
+                {
+                    break;
+                }
+
+                if (fileLines[fileLineIndex].EndsWith(":"))
+                {
+                    block.Children.Add(ParseBlock(level + 1));
+                    fileLineIndex--;
+                }
+                else
+                {
+                    string toParse = fileLines[fileLineIndex].Trim();
+
+                    // Yarn V1 and V2 have slightly different formats, where V2 adds a : between property name and value
+                    // Match on the specified version
+                    var matches = YarnLockVersion == YarnLockVersion.V1 ? YarnV1Regex.Match(toParse) : YarnV2Regex.Match(toParse);
+
+                    if (matches.Groups.Count != 3) // Whole group + two captures
+                    {
+                        continue;
+                    }
+
+                    block.Values.Add(matches.Groups[1].Value.Trim('\"'), matches.Groups[2].Value.Trim('\"'));
+                }
+
+                if (!Peek() || !fileLines[fileLineIndex].StartsWith(currentLevelDelimiter) || string.IsNullOrWhiteSpace(fileLines[fileLineIndex]))
+                {
+                    break;
+                }
+            }
+
+            return block;
+        }
+
+        IEnumerator IEnumerable.GetEnumerator()
+        {
+            return GetEnumerator();
+        }
+
+        /// 
+        /// Increments the internal pointer so that it is at the next block.
+        /// 
+        /// 
+        private bool ReadToNextMajorBlock()
+        {
+            string line;
+            do
+            {
+                if (!IncrementIndex())
+                {
+                    return false;
+                }
+                else
+                {
+                    line = fileLines[fileLineIndex];
+                }
+            }
+            while (string.IsNullOrWhiteSpace(line) || line.StartsWith(" ") || line.StartsWith("\t") || line.StartsWith("#"));
+
+            return true;
+        }
+
+        private bool IncrementIndex()
+        {
+            fileLineIndex++;
+
+            return Peek();
+        }
+
+        /// 
+        /// Checks to see if any lines are left in the file contents.
+        /// 
+        /// 
+        private bool Peek()
+        {
+            if (fileLineIndex >= fileLines.Count)
+            {
+                return false;
+            }
+
+            return true;
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/yarn/Parsers/YarnLockParser.cs b/src/Microsoft.ComponentDetection.Detectors/yarn/Parsers/YarnLockParser.cs
new file mode 100644
index 000000000..71e30558b
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/yarn/Parsers/YarnLockParser.cs
@@ -0,0 +1,163 @@
+using System;
+using System.Collections.Generic;
+using System.Composition;
+using System.Linq;
+using Microsoft.ComponentDetection.Contracts;
+
+namespace Microsoft.ComponentDetection.Detectors.Yarn.Parsers
+{
+    public class YarnLockParser : IYarnLockParser
+    {
+        private static readonly List SupportedVersions = new List { YarnLockVersion.V1, YarnLockVersion.V2 };
+
+        private const string VersionString = "version";
+
+        private const string Resolved = "resolved";
+
+        private const string Dependencies = "dependencies";
+
+        private const string OptionalDependencies = "optionalDependencies";
+
+        [Import]
+        public ILogger Logger { get; set; }
+
+        public bool CanParse(YarnLockVersion yarnLockVersion)
+        {
+            return SupportedVersions.Contains(yarnLockVersion);
+        }
+
+        public YarnLockFile Parse(IYarnBlockFile blockFile, ILogger logger)
+        {
+            if (blockFile == null)
+            {
+                throw new ArgumentNullException(nameof(blockFile));
+            }
+
+            YarnLockFile file = new YarnLockFile { LockVersion = blockFile.YarnLockVersion };
+            IList entries = new List();
+
+            foreach (var block in blockFile)
+            {
+                YarnEntry yarnEntry = new YarnEntry();
+                var satisfiedPackages = block.Title.Split(',').Select(x => x.Trim())
+                    .Select(GenerateBlockTitleNormalizer(block));
+
+                foreach (var package in satisfiedPackages)
+                {
+                    if (!TryReadNameAndSatisfiedVersion(package, out Tuple parsed))
+                    {
+                        continue;
+                    }
+
+                    if (string.IsNullOrEmpty(yarnEntry.Name))
+                    {
+                        yarnEntry.Name = parsed.Item1;
+                    }
+
+                    yarnEntry.Satisfied.Add(NormalizeVersion(parsed.Item2));
+                }
+
+                if (string.IsNullOrWhiteSpace(yarnEntry.Name))
+                {
+                    logger.LogWarning($"Failed to read a name for block {block.Title}. The entry will be skipped.");
+                    continue;
+                }
+
+                if (!block.Values.TryGetValue(VersionString, out string version))
+                {
+                    logger.LogWarning($"Failed to read a version for {yarnEntry.Name}. The entry will be skipped.");
+                    continue;
+                }
+
+                yarnEntry.Version = version;
+
+                if (block.Values.TryGetValue(Resolved, out string resolved))
+                {
+                    yarnEntry.Resolved = resolved;
+                }
+
+                var dependencyBlock = block.Children.SingleOrDefault(x => string.Equals(x.Title, Dependencies, StringComparison.OrdinalIgnoreCase));
+
+                if (dependencyBlock != null)
+                {
+                    foreach (var item in dependencyBlock.Values)
+                    {
+                        yarnEntry.Dependencies.Add(new YarnDependency { Name = item.Key, Version = NormalizeVersion(item.Value) });
+                    }
+                }
+
+                var optionalDependencyBlock = block.Children.SingleOrDefault(x => string.Equals(x.Title, OptionalDependencies, StringComparison.OrdinalIgnoreCase));
+
+                if (optionalDependencyBlock != null)
+                {
+                    foreach (var item in optionalDependencyBlock.Values)
+                    {
+                        yarnEntry.OptionalDependencies.Add(new YarnDependency { Name = item.Key, Version = NormalizeVersion(item.Value) });
+                    }
+                }
+
+                entries.Add(yarnEntry);
+            }
+
+            file.Entries = entries;
+
+            return file;
+        }
+
+        private Func GenerateBlockTitleNormalizer(YarnBlock block)
+        {
+            // For cases where we have no version in the title, ex:
+            //   nyc:
+            //    version "10.0.0"
+            //    resolved "https://registry.Yarnpkg.com/nyc/-/nyc-10.0.0.tgz#95bd4a2c3487f33e1e78f213c6d5a53d88074ce6"
+            return blockTitleMember =>
+            {
+                if (blockTitleMember.Contains("@"))
+                {
+                    return blockTitleMember;
+                }
+
+                var versionValue = block.Values.FirstOrDefault(x => string.Equals(x.Key, YarnLockParser.VersionString, StringComparison.OrdinalIgnoreCase));
+                if (default(KeyValuePair).Equals(versionValue))
+                {
+                    Logger.LogWarning("Block without version detected");
+                    return blockTitleMember;
+                }
+
+                return blockTitleMember + $"@{versionValue.Value}";
+            };
+        }
+
+        private bool TryReadNameAndSatisfiedVersion(string nameVersionPairing, out Tuple output)
+        {
+            output = null;
+            string workingString = nameVersionPairing;
+            workingString = workingString.TrimEnd(':');
+            workingString = workingString.Trim('\"');
+            bool startsWithAtSign = false;
+            if (workingString.StartsWith("@"))
+            {
+                startsWithAtSign = true;
+                workingString = workingString.TrimStart('@');
+            }
+
+            string[] parts = workingString.Split('@');
+
+            if (parts.Length != 2)
+            {
+                return false;
+            }
+
+            string at = startsWithAtSign ? "@" : string.Empty;
+            string name = $"{at}{parts[0]}";
+
+            output = new Tuple(name, parts[1]);
+            return true;
+        }
+
+        public static string NormalizeVersion(string version)
+        {
+            return version.StartsWith("npm:") ? version : $"npm:{version}";
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/yarn/YarnDependency.cs b/src/Microsoft.ComponentDetection.Detectors/yarn/YarnDependency.cs
new file mode 100644
index 000000000..80831f96d
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/yarn/YarnDependency.cs
@@ -0,0 +1,11 @@
+namespace Microsoft.ComponentDetection.Detectors.Yarn
+{
+    public class YarnDependency
+    {
+        public string LookupKey => $"{Name}@{Version}";
+
+        public string Name { get; set; }
+
+        public string Version { get; set; }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/yarn/YarnEntry.cs b/src/Microsoft.ComponentDetection.Detectors/yarn/YarnEntry.cs
new file mode 100644
index 000000000..8984525ea
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/yarn/YarnEntry.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+
+namespace Microsoft.ComponentDetection.Detectors.Yarn
+{
+    public class YarnEntry
+    {
+        public string LookupKey => $"{Name}@{Version}";
+
+        /// 
+        /// Gets or sets the non-version qualified name of the entry.
+        /// 
+        public string Name { get; set; }
+
+        /// 
+        /// Gets or sets the version string of the entry.
+        /// 
+        public string Version { get; set; }
+
+        /// 
+        /// Gets or sets the resolution string of the entry.
+        /// 
+        public string Resolved { get; set; }
+
+        /// 
+        /// Gets the satisfied version strings of this entry.
+        /// 
+        public IList Satisfied { get; } = new List();
+
+        /// 
+        /// Gets the name@version dependencies that this package requires.
+        /// 
+        public IList Dependencies { get; } = new List();
+
+        /// 
+        /// Gets the name@version dependencies that this package requires.
+        /// 
+        public IList OptionalDependencies { get; } = new List();
+
+        /// 
+        /// Gets or sets a value indicating whether or not the component is a dev dependency.
+        /// 
+        public bool DevDependency { get; set; }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/yarn/YarnLockComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/yarn/YarnLockComponentDetector.cs
new file mode 100644
index 000000000..1525b10d8
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/yarn/YarnLockComponentDetector.cs
@@ -0,0 +1,291 @@
+using System;
+using System.Collections.Generic;
+using System.Composition;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using DotNet.Globbing;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.Internal;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Detectors.Npm;
+
+namespace Microsoft.ComponentDetection.Detectors.Yarn
+{
+    [Export(typeof(IComponentDetector))]
+    public class YarnLockComponentDetector : FileComponentDetector
+    {
+        public override string Id { get; } = "Yarn";
+
+        public override IList SearchPatterns { get; } = new List { "yarn.lock" };
+
+        public override IEnumerable SupportedComponentTypes { get; } = new[] { ComponentType.Npm };
+
+        public override int Version { get; } = 5;
+
+        public override IEnumerable Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.Npm) };
+
+        /// 
+        /// "Package" is a more common substring, enclose it with \ to verify it is a folder.
+        protected override IList SkippedFolders => new List { "node_modules", "pnpm-store", "\\package\\" };
+
+        protected override async Task OnFileFound(ProcessRequest processRequest, IDictionary detectorArgs)
+        {
+            var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder;
+            var file = processRequest.ComponentStream;
+
+            string skippedFolder = SkippedFolders.FirstOrDefault(folder => file.Location.Contains(folder));
+            if (!string.IsNullOrEmpty(skippedFolder))
+            {
+                Logger.LogInfo($"Yarn.Lock file {file.Location} was found in a {skippedFolder} folder and will be skipped.");
+                return;
+            }
+
+            Logger.LogInfo($"Processing file {file.Location}");
+
+            try
+            {
+                var parsed = await YarnLockFileFactory.ParseYarnLockFileAsync(file.Stream, Logger);
+                DetectComponents(parsed, file.Location, singleFileComponentRecorder);
+            }
+            catch (Exception ex)
+            {
+                Logger.LogBuildWarning($"Could not read components from file {file.Location}.");
+                Logger.LogFailedReadingFile(file.Location, ex);
+            }
+        }
+
+        private void DetectComponents(YarnLockFile file, string location, ISingleFileComponentRecorder singleFileComponentRecorder)
+        {
+            Dictionary yarnPackages = new Dictionary();
+
+            // Iterate once and build our provider lookup for all possible yarn packages requests
+            // Each entry can satisfy more than one request in a Yarn.Lock file
+            // Example: npm@2.3.4 can satisfy the requests for npm@2, npm@2.3.4 and npm@^2.3.4, and each of these cases are
+            // explicitly listed in the lockfile. So we have a dictionary entry for each one of those "npm@{version}" strings
+            // to resolve back to the package in question
+            foreach (var entry in file.Entries)
+            {
+                foreach (var satisfiedVersion in entry.Satisfied)
+                {
+                    yarnPackages.Add($"{entry.Name}@{satisfiedVersion}", entry);
+                }
+            }
+
+            if (yarnPackages.Count == 0 || !TryReadPeerPackageJsonRequestsAsYarnEntries(location, yarnPackages, out List yarnRoots))
+            {
+                return;
+            }
+
+            foreach (var dependency in yarnRoots)
+            {
+                var root = new DetectedComponent(new NpmComponent(dependency.Name, dependency.Version));
+                AddDetectedComponentToGraph(root, null, singleFileComponentRecorder, isRootComponent: true);
+            }
+
+            // It's important that all of the root dependencies get registered *before* we start processing any non-root
+            // dependencies; otherwise, we would miss root dependency links for root dependencies that are also indirect
+            // transitive dependencies. 
+            foreach (var dependency in yarnRoots)
+            {
+                ParseTreeWithAssignedRoot(dependency, yarnPackages, singleFileComponentRecorder);
+            }
+
+            // Catch straggler top level packages in the yarn.lock file that aren't in the package.lock file for whatever reason
+            foreach (var entry in file.Entries)
+            {
+                var component = new DetectedComponent(new NpmComponent(entry.Name, entry.Version));
+                if (singleFileComponentRecorder.GetComponent(component.Component.Id) == null)
+                {
+                    AddDetectedComponentToGraph(component, parentComponent: null, singleFileComponentRecorder);
+                }
+            }
+        }
+
+        /// 
+        /// Takes a tree of components from a package.json root and adds/modifies detected components appropriately.
+        /// 
+        /// The root of the section of the graph that we are parsing.
+        /// A list of all possible yarn components to do lookups against.
+        /// The component recorder for file that is been processed.
+        private void ParseTreeWithAssignedRoot(YarnEntry root, Dictionary providerTable, ISingleFileComponentRecorder singleFileComponentRecorder)
+        {
+            Queue<(YarnEntry, YarnEntry)> queue = new Queue<(YarnEntry, YarnEntry)>();
+
+            queue.Enqueue((root, null));
+            HashSet processed = new HashSet();
+
+            while (queue.Count > 0)
+            {
+                var (currentEntry, parentEntry) = queue.Dequeue();
+                DetectedComponent currentComponent = singleFileComponentRecorder.GetComponent(YarnEntryToComponentId(currentEntry));
+                DetectedComponent parentComponent = parentEntry != null ? singleFileComponentRecorder.GetComponent(YarnEntryToComponentId(parentEntry)) : null;
+
+                if (currentComponent != null)
+                {
+                    AddDetectedComponentToGraph(currentComponent, parentComponent, singleFileComponentRecorder, isDevDependency: root.DevDependency);
+                }
+                else
+                {
+                    // If this is the first time we've seen a component...
+                    var detectedComponent = new DetectedComponent(new NpmComponent(currentEntry.Name, currentEntry.Version));
+                    AddDetectedComponentToGraph(detectedComponent, parentComponent, singleFileComponentRecorder, isDevDependency: root.DevDependency);
+                }
+
+                // Ensure that we continue to parse the tree for dependencies
+                // also maintain a list of components we've seen in this set of the graph
+                // so that we can short-circuit circular dependencies if we hit them
+                var newDependencies = currentEntry.Dependencies.Concat(currentEntry.OptionalDependencies);
+                foreach (var newDependency in newDependencies)
+                {
+                    if (providerTable.ContainsKey(newDependency.LookupKey))
+                    {
+                        var subDependency = providerTable[newDependency.LookupKey];
+
+                        if (!processed.Contains(subDependency.LookupKey))
+                        {
+                            processed.Add(subDependency.LookupKey);
+                            queue.Enqueue((subDependency, currentEntry));
+                        }
+                    }
+                    else
+                    {
+                        Logger.LogInfo($"Failed to find resolved dependency for {newDependency.LookupKey}");
+                    }
+                }
+            }
+        }
+
+        /// 
+        /// We use the yarn.lock's peer package.json file to determine what constitutes a "top-level" package
+        /// This function reads those from the package.json so that they can later be used as the starting points
+        /// in traversing the dependency graph.
+        /// 
+        /// The file location of the yarn.lock file.
+        /// All the yarn entries that we know about.
+        /// The output yarnRoots that we care about using as starting points.
+        /// 
+        private bool TryReadPeerPackageJsonRequestsAsYarnEntries(string location, Dictionary yarnEntries, out List yarnRoots)
+        {
+            yarnRoots = new List();
+
+            var pkgJsons = ComponentStreamEnumerableFactory.GetComponentStreams(new FileInfo(location).Directory, new List { "package.json" }, (name, directoryName) => false, recursivelyScanDirectories: false);
+
+            IDictionary> combinedDependencies = new Dictionary>();
+
+            int pkgJsonCount = 0;
+
+            IList yarnWorkspaces = new List();
+            foreach (var pkgJson in pkgJsons)
+            {
+                combinedDependencies = NpmComponentUtilities.TryGetAllPackageJsonDependencies(pkgJson.Stream, out yarnWorkspaces);
+                pkgJsonCount++;
+            }
+
+            if (pkgJsonCount != 1)
+            {
+                Logger.LogWarning($"No package.json was found for file at {location}. It will not be registered.");
+                return false;
+            }
+
+            if (yarnWorkspaces.Count > 0)
+            {
+                GetWorkspaceDependencies(yarnWorkspaces, new FileInfo(location).Directory, combinedDependencies);
+            }
+
+            // Convert all of the dependencies we retrieved from package.json
+            // into the appropriate yarn package
+            foreach (var dependency in combinedDependencies)
+            {
+                string name = dependency.Key;
+                foreach (var version in dependency.Value)
+                {
+                    var entryKey = $"{name}@npm:{version.Key}";
+                    if (!yarnEntries.ContainsKey(entryKey))
+                    {
+                        Logger.LogWarning($"A package was requested in the package.json file that was a peer of {location} but was not contained in the lockfile. {name} - {version.Key}");
+                        continue;
+                    }
+
+                    var entry = yarnEntries[entryKey];
+
+                    entry.DevDependency = version.Value;
+
+                    yarnRoots.Add(entry);
+                }
+            }
+
+            return true;
+        }
+
+        private void GetWorkspaceDependencies(IList yarnWorkspaces, DirectoryInfo root, IDictionary> dependencies)
+        {
+            var ignoreCase = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+
+            var globOptions = new GlobOptions()
+            {
+                Evaluation = new EvaluationOptions()
+                {
+                    CaseInsensitive = ignoreCase,
+                },
+            };
+
+            foreach (var workspacePattern in yarnWorkspaces)
+            {
+                var glob = Glob.Parse($"{root.FullName.Replace('\\', '/')}/{workspacePattern}/package.json", globOptions);
+
+                var componentStreams = ComponentStreamEnumerableFactory.GetComponentStreams(root, (file) => glob.IsMatch(file.FullName.Replace('\\', '/')), null, true);
+
+                foreach (var stream in componentStreams)
+                {
+                    Logger.LogInfo($"{stream.Location} found for workspace {workspacePattern}");
+                    var combinedDependencies = NpmComponentUtilities.TryGetAllPackageJsonDependencies(stream.Stream, out _);
+
+                    foreach (var dependency in combinedDependencies)
+                    {
+                        ProcessWorkspaceDependency(dependencies, dependency);
+                    }
+                }
+            }
+        }
+
+        private void ProcessWorkspaceDependency(IDictionary> dependencies, KeyValuePair> newDependency)
+        {
+            if (!dependencies.TryGetValue(newDependency.Key, out var existingDependency))
+            {
+                dependencies.Add(newDependency.Key, newDependency.Value);
+                return;
+            }
+
+            foreach (var item in newDependency.Value)
+            {
+                if (existingDependency.TryGetValue(item.Key, out bool wasDev))
+                {
+                    existingDependency[item.Key] = wasDev && item.Value;
+                }
+                else
+                {
+                    existingDependency[item.Key] = item.Value;
+                }
+            }
+        }
+
+        private void AddDetectedComponentToGraph(DetectedComponent componentToAdd, DetectedComponent parentComponent, ISingleFileComponentRecorder singleFileComponentRecorder, bool isRootComponent = false, bool? isDevDependency = null)
+        {
+            if (parentComponent == null)
+            {
+                singleFileComponentRecorder.RegisterUsage(componentToAdd, isRootComponent, isDevelopmentDependency: isDevDependency);
+            }
+            else
+            {
+                singleFileComponentRecorder.RegisterUsage(componentToAdd, isRootComponent, parentComponent.Component.Id, isDevelopmentDependency: isDevDependency);
+            }
+        }
+
+        private string YarnEntryToComponentId(YarnEntry entry)
+        {
+            return new DetectedComponent(new NpmComponent(entry.Name, entry.Version)).Component.Id;
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/yarn/YarnLockFile.cs b/src/Microsoft.ComponentDetection.Detectors/yarn/YarnLockFile.cs
new file mode 100644
index 000000000..346a803c0
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/yarn/YarnLockFile.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+
+namespace Microsoft.ComponentDetection.Detectors.Yarn
+{
+    public class YarnLockFile
+    {
+        /// 
+        /// Gets or sets the declared Yarn Lock Version.
+        /// 
+        public YarnLockVersion LockVersion { get; set; }
+
+        /// 
+        /// Gets or sets the component entries.
+        /// 
+        public IEnumerable Entries { get; set; }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/yarn/YarnLockFileFactory.cs b/src/Microsoft.ComponentDetection.Detectors/yarn/YarnLockFileFactory.cs
new file mode 100644
index 000000000..5714e8643
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/yarn/YarnLockFileFactory.cs
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Detectors.Yarn.Parsers;
+
+namespace Microsoft.ComponentDetection.Detectors.Yarn
+{
+    public static class YarnLockFileFactory
+    {
+        public static IList Parsers { get; }
+
+        static YarnLockFileFactory()
+        {
+            Parsers = new List { new YarnLockParser() };
+        }
+
+        public static async Task ParseYarnLockFileAsync(Stream file, ILogger logger)
+        {
+            YarnBlockFile blockFile = await YarnBlockFile.CreateBlockFileAsync(file);
+
+            foreach (var parser in Parsers)
+            {
+                if (parser.CanParse(blockFile.YarnLockVersion))
+                {
+                    return parser.Parse(blockFile, logger);
+                }
+            }
+
+            throw new InvalidYarnLockFileException();
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/yarn/YarnLockVersion.cs b/src/Microsoft.ComponentDetection.Detectors/yarn/YarnLockVersion.cs
new file mode 100644
index 000000000..d6c21a087
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/yarn/YarnLockVersion.cs
@@ -0,0 +1,9 @@
+namespace Microsoft.ComponentDetection.Detectors.Yarn
+{
+    public enum YarnLockVersion
+    {
+        Invalid = 0,
+        V1 = 1,
+        V2 = 2,
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/ArgumentHelper.cs b/src/Microsoft.ComponentDetection.Orchestrator/ArgumentHelper.cs
new file mode 100644
index 000000000..47bc86fa9
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/ArgumentHelper.cs
@@ -0,0 +1,57 @@
+using System.Collections.Generic;
+using System.Composition;
+using System.Linq;
+using CommandLine;
+using Microsoft.ComponentDetection.Orchestrator.ArgumentSets;
+
+namespace Microsoft.ComponentDetection.Orchestrator
+{
+    [Export(typeof(IArgumentHelper))]
+    public class ArgumentHelper : IArgumentHelper
+    {
+        [ImportMany]
+        public IEnumerable ArgumentSets { get; set; }
+
+        public ArgumentHelper()
+        {
+            ArgumentSets = Enumerable.Empty();
+        }
+
+        public ParserResult ParseArguments(string[] args)
+        {
+            return Parser.Default.ParseArguments(args, ArgumentSets.Select(x => x.GetType()).ToArray());
+        }
+
+        public ParserResult ParseArguments(string[] args, bool ignoreInvalidArgs = false)
+        {
+            Parser p = new Parser(x =>
+            {
+                x.IgnoreUnknownArguments = ignoreInvalidArgs;
+
+                // This is not the main argument dispatch, so we don't want console output.
+                x.HelpWriter = null;
+            });
+
+            return p.ParseArguments(args);
+        }
+
+        public static IDictionary GetDetectorArgs(IEnumerable detectorArgsList)
+        {
+            var detectorArgs = new Dictionary();
+
+            foreach (var arg in detectorArgsList)
+            {
+                var keyValue = arg.Split('=');
+
+                if (keyValue.Count() != 2)
+                {
+                    continue;
+                }
+
+                detectorArgs.Add(keyValue[0], keyValue[1]);
+            }
+
+            return detectorArgs;
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/BaseArguments.cs b/src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/BaseArguments.cs
new file mode 100644
index 000000000..9633eed60
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/BaseArguments.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using CommandLine;
+using Microsoft.ComponentDetection.Common;
+using Newtonsoft.Json;
+
+namespace Microsoft.ComponentDetection.Orchestrator.ArgumentSets
+{
+    public class BaseArguments : IScanArguments
+    {
+        [Option("Debug", Required = false, HelpText = "Wait for debugger on start")]
+        public bool Debug { get; set; }
+
+        [Option("DebugTelemetry", Required = false, HelpText = "Used to output all telemetry events to the console.")]
+        public bool DebugTelemetry { get; set; }
+
+        [JsonIgnore]
+        [Option("AdditionalPluginDirectories", Separator = ';', Required = false, Hidden = true, HelpText = "Semi-colon delimited list of directories to search for plugins")]
+        public IEnumerable AdditionalPluginDirectories { get; set; }
+
+        public IEnumerable AdditionalPluginDirectoriesSerialized => AdditionalPluginDirectories?.Select(x => x.ToString()) ?? new List();
+
+        [Option("CorrelationId", Required = false, HelpText = "Identifier used to correlate all telemetry for a given execution. If not provided, a new GUID will be generated.")]
+        public Guid CorrelationId { get; set; }
+
+        [Option("Verbosity", HelpText = "Flag indicating what level of logging to output to console during execution. Options are: Verbose, Normal, or Quiet.", Default = VerbosityMode.Normal)]
+        public VerbosityMode Verbosity { get; set; }
+
+        [Option("Timeout", Required = false, HelpText = "An integer representing the time limit (in seconds) before detection is cancelled")]
+        public int Timeout { get; set; }
+
+        [Option("Output", Required = false, HelpText = "Output path for log files. Defaults to %TMP%")]
+        public string Output { get; set; }
+
+        [Option("AdditionalDITargets", Required = false, Separator = ',', HelpText = "Comma separated list of paths to additional dependency injection targets.")]
+        public IEnumerable AdditionalDITargets { get; set; }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/BcdeArguments.cs b/src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/BcdeArguments.cs
new file mode 100644
index 000000000..0e985c6a8
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/BcdeArguments.cs
@@ -0,0 +1,51 @@
+using System.Collections.Generic;
+using System.Composition;
+using System.IO;
+using CommandLine;
+using Newtonsoft.Json;
+
+namespace Microsoft.ComponentDetection.Orchestrator.ArgumentSets
+{
+    [Verb("scan", HelpText = "Scans components")]
+    [Export(typeof(IScanArguments))]
+    public class BcdeArguments : BaseArguments, IDetectionArguments
+    {
+        [Option("DirectoryExclusionList", Required = false, Separator = ';', HelpText = "Filters out specific directories following a minimatch pattern.")]
+        public IEnumerable DirectoryExclusionList { get; set; }
+
+        [Option("IgnoreDirectories", Required = false, Separator = ',', HelpText = "Filters out specific directories, providing individual directory paths separated by semicolon. Obsolete in favor of DirectoryExclusionList's glob syntax.")]
+        public IEnumerable DirectoryExclusionListObsolete { get; set; }
+
+        [JsonIgnore]
+        [Option("SourceDirectory", Required = true, HelpText = "Directory to operate on.")]
+        public DirectoryInfo SourceDirectory { get; set; }
+
+        public string SourceDirectorySerialized => SourceDirectory?.ToString();
+
+        [JsonIgnore]
+        [Option("SourceFileRoot", Required = false, HelpText = "Directory where source files can be found.")]
+        public DirectoryInfo SourceFileRoot { get; set; }
+
+        public string SourceFileRootSerialized => SourceFileRoot?.ToString();
+
+        [Option("DetectorArgs", Separator = ',', Required = false, HelpText = "Comma separated list of properties that can affect the detectors execution, like EnableIfDefaultOff that allows a specific detector that is in beta to run, the format for this property is " +
+            "DetectorId=EnableIfDefaultOff, for example Pip=EnableIfDefaultOff.")]
+        public IEnumerable DetectorArgs { get; set; }
+
+        [Option("DetectorCategories", Separator = ',', Required = false, HelpText = "A comma separated list with the categories of components that are going to be scanned. The detectors that are going to run are the ones that belongs to the categories." +
+            "The possible values are: Npm, NuGet, Maven, RubyGems, Cargo, Pip, GoMod, CocoaPods, Linux.")]
+        public IEnumerable DetectorCategories { get; set; }
+
+        [Option("DetectorsFilter", Separator = ',', Required = false, HelpText = "A comma separated list with the identifiers of the specific detectors to be used. This is meant to be used for testing purposes only.")]
+        public IEnumerable DetectorsFilter { get; set; }
+
+        [JsonIgnore]
+        [Option("ManifestFile", Required = false, HelpText = "The file to write scan results to.")]
+        public FileInfo ManifestFile { get; set; }
+
+        public string ManifestFileSerialized => ManifestFile?.ToString();
+
+        [Option("DockerImagesToScan", Required = false, Separator = ',', HelpText = "Comma separated list of docker image names or hashes to execute container scanning on, ex: ubuntu:16.04, 56bab49eef2ef07505f6a1b0d5bd3a601dfc3c76ad4460f24c91d6fa298369ab")]
+        public IEnumerable DockerImagesToScan { get; set; }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/BcdeDevArguments.cs b/src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/BcdeDevArguments.cs
new file mode 100644
index 000000000..1aae69032
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/BcdeDevArguments.cs
@@ -0,0 +1,12 @@
+using System.Composition;
+using CommandLine;
+
+namespace Microsoft.ComponentDetection.Orchestrator.ArgumentSets
+{
+    [Verb("dev", HelpText = "Dev command", Hidden = true)]
+    [Export(typeof(IScanArguments))]
+    public class BcdeDevArguments : BcdeArguments, IDetectionArguments
+    {
+        // TODO: Add option to specify download directory for GH database
+    }
+}
\ No newline at end of file
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/IDetectionArguments.cs b/src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/IDetectionArguments.cs
new file mode 100644
index 000000000..d4674c5cd
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/IDetectionArguments.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using System.IO;
+
+namespace Microsoft.ComponentDetection.Orchestrator.ArgumentSets
+{
+    public interface IDetectionArguments : IScanArguments
+    {
+        IEnumerable DirectoryExclusionList { get; set; }
+
+        IEnumerable DirectoryExclusionListObsolete { get; set; }
+
+        DirectoryInfo SourceDirectory { get; set; }
+
+        DirectoryInfo SourceFileRoot { get; set; }
+
+        IEnumerable DetectorArgs { get; set; }
+
+        IEnumerable DetectorCategories { get; set; }
+
+        IEnumerable DetectorsFilter { get; set; }
+
+        FileInfo ManifestFile { get; set; }
+
+        IEnumerable DockerImagesToScan { get; set; }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/IListDetectionArgs.cs b/src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/IListDetectionArgs.cs
new file mode 100644
index 000000000..7a721e7df
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/IListDetectionArgs.cs
@@ -0,0 +1,6 @@
+namespace Microsoft.ComponentDetection.Orchestrator.ArgumentSets
+{
+    public interface IListDetectionArgs : IScanArguments
+    {
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/IScanArguments.cs b/src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/IScanArguments.cs
new file mode 100644
index 000000000..8c04ac2e3
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/IScanArguments.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Microsoft.ComponentDetection.Common;
+
+namespace Microsoft.ComponentDetection.Orchestrator.ArgumentSets
+{
+    public interface IScanArguments
+    {
+        IEnumerable AdditionalPluginDirectories { get; set; }
+
+        IEnumerable AdditionalDITargets { get; set; }
+
+        Guid CorrelationId { get; set; }
+
+        VerbosityMode Verbosity { get; set; }
+
+        int Timeout { get; set; }
+
+        string Output { get; set; }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/ListDetectionArgs.cs b/src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/ListDetectionArgs.cs
new file mode 100644
index 000000000..eaf6a028c
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/ListDetectionArgs.cs
@@ -0,0 +1,11 @@
+using System.Composition;
+using CommandLine;
+
+namespace Microsoft.ComponentDetection.Orchestrator.ArgumentSets
+{
+    [Verb("list-detectors", HelpText = "Lists available detectors")]
+    [Export(typeof(IScanArguments))]
+    public class ListDetectionArgs : BaseArguments, IListDetectionArgs
+    {
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/CommandLineArgumentsExporter.cs b/src/Microsoft.ComponentDetection.Orchestrator/CommandLineArgumentsExporter.cs
new file mode 100644
index 000000000..795bf9def
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/CommandLineArgumentsExporter.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Composition;
+using Microsoft.ComponentDetection.Orchestrator.ArgumentSets;
+
+namespace Microsoft.ComponentDetection.Orchestrator
+{
+    [Export]
+    public class CommandLineArgumentsExporter
+    {
+        public CommandLineArgumentsExporter()
+        {
+            DelayedInjectionLazy = new Lazy(() => ArgumentsForDelayedInjection);
+        }
+
+        [Export("InjectableDetectionArguments")]
+        public Lazy DelayedInjectionLazy { get; set; }
+
+        public static IScanArguments ArgumentsForDelayedInjection { get; set; }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/DetectorRestrictions.cs b/src/Microsoft.ComponentDetection.Orchestrator/DetectorRestrictions.cs
new file mode 100644
index 000000000..f448a2e9b
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/DetectorRestrictions.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+
+namespace Microsoft.ComponentDetection.Orchestrator
+{
+    public class DetectorRestrictions
+    {
+        public IEnumerable AllowedDetectorIds { get; set; }
+
+        public IEnumerable ExplicitlyEnabledDetectorIds { get; set; }
+
+        public IEnumerable AllowedDetectorCategories { get; set; }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/DetectorRunResult.cs b/src/Microsoft.ComponentDetection.Orchestrator/DetectorRunResult.cs
new file mode 100644
index 000000000..e062616c5
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/DetectorRunResult.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace Microsoft.ComponentDetection.Orchestrator
+{
+    public class DetectorRunResult
+    {
+        public TimeSpan ExecutionTime { get; set; }
+
+        public int ComponentsFoundCount { get; set; }
+
+        public int ExplicitlyReferencedComponentCount { get; set; }
+
+        public bool IsExperimental { get; set; }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Exceptions/InvalidDetectorCategoriesException.cs b/src/Microsoft.ComponentDetection.Orchestrator/Exceptions/InvalidDetectorCategoriesException.cs
new file mode 100644
index 000000000..4f545e365
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Exceptions/InvalidDetectorCategoriesException.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Exceptions
+{
+    [Serializable]
+    public class InvalidDetectorCategoriesException : Exception
+    {
+        public InvalidDetectorCategoriesException()
+        {
+        }
+
+        public InvalidDetectorCategoriesException(string message)
+            : base(message)
+        {
+        }
+
+        public InvalidDetectorCategoriesException(string message, Exception innerException)
+            : base(message, innerException)
+        {
+        }
+
+        protected InvalidDetectorCategoriesException(SerializationInfo info, StreamingContext context)
+            : base(info, context)
+        {
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Exceptions/InvalidDetectorFilterException.cs b/src/Microsoft.ComponentDetection.Orchestrator/Exceptions/InvalidDetectorFilterException.cs
new file mode 100644
index 000000000..f44b4e9f9
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Exceptions/InvalidDetectorFilterException.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Exceptions
+{
+    [Serializable]
+    public class InvalidDetectorFilterException : Exception
+    {
+        public InvalidDetectorFilterException()
+        {
+        }
+
+        public InvalidDetectorFilterException(string message)
+            : base(message)
+        {
+        }
+
+        public InvalidDetectorFilterException(string message, Exception innerException)
+            : base(message, innerException)
+        {
+        }
+
+        protected InvalidDetectorFilterException(SerializationInfo info, StreamingContext context)
+            : base(info, context)
+        {
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Exceptions/NoDetectorsFoundException.cs b/src/Microsoft.ComponentDetection.Orchestrator/Exceptions/NoDetectorsFoundException.cs
new file mode 100644
index 000000000..3f558fd5c
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Exceptions/NoDetectorsFoundException.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Exceptions
+{
+    [Serializable]
+    internal class NoDetectorsFoundException : Exception
+    {
+        public NoDetectorsFoundException()
+            : base("Unable to load any detector plugins.")
+        {
+        }
+
+        public NoDetectorsFoundException(string message)
+            : base(message)
+        {
+        }
+
+        public NoDetectorsFoundException(string message, Exception innerException)
+            : base(message, innerException)
+        {
+        }
+
+        protected NoDetectorsFoundException(SerializationInfo info, StreamingContext context)
+            : base(info, context)
+        {
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/IArgumentHelper.cs b/src/Microsoft.ComponentDetection.Orchestrator/IArgumentHelper.cs
new file mode 100644
index 000000000..6f876e606
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/IArgumentHelper.cs
@@ -0,0 +1,9 @@
+using CommandLine;
+
+namespace Microsoft.ComponentDetection.Orchestrator
+{
+    public interface IArgumentHelper
+    {
+        ParserResult ParseArguments(string[] args);
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Microsoft.ComponentDetection.Orchestrator.csproj b/src/Microsoft.ComponentDetection.Orchestrator/Microsoft.ComponentDetection.Orchestrator.csproj
new file mode 100644
index 000000000..b5d41f853
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Microsoft.ComponentDetection.Orchestrator.csproj
@@ -0,0 +1,22 @@
+
+
+    
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+    
+
+    
+        
+    
+
+
\ No newline at end of file
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Orchestrator.cs b/src/Microsoft.ComponentDetection.Orchestrator/Orchestrator.cs
new file mode 100644
index 000000000..54d0976c3
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Orchestrator.cs
@@ -0,0 +1,309 @@
+using System;
+using System.Collections.Generic;
+using System.Composition;
+using System.Composition.Hosting;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using CommandLine;
+using Microsoft.ComponentDetection.Common;
+using Microsoft.ComponentDetection.Common.Exceptions;
+using Microsoft.ComponentDetection.Common.Telemetry;
+using Microsoft.ComponentDetection.Common.Telemetry.Records;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.BcdeModels;
+using Microsoft.ComponentDetection.Orchestrator.ArgumentSets;
+using Microsoft.ComponentDetection.Orchestrator.Services;
+using Microsoft.ComponentDetection.Orchestrator.Services.GraphTranslation;
+using Newtonsoft.Json;
+
+namespace Microsoft.ComponentDetection.Orchestrator
+{
+    public class Orchestrator
+    {
+        private static readonly bool IsLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
+
+        public ScanResult Load(string[] args)
+        {
+            ArgumentHelper argumentHelper = new ArgumentHelper { ArgumentSets = new[] { new BaseArguments() } };
+            BaseArguments baseArguments = null;
+            var parserResult = argumentHelper.ParseArguments(args, true);
+            parserResult.WithParsed(x => baseArguments = x);
+            if (parserResult.Tag == ParserResultType.NotParsed)
+            {
+                // Blank args for this part of the loader, all things are optional and default to false / empty / null
+                baseArguments = new BaseArguments();
+            }
+
+            IEnumerable additionalDITargets = baseArguments.AdditionalDITargets ?? Enumerable.Empty();
+
+            // Load all types from Common (where Logger lives) and our executing assembly.
+            var configuration = new ContainerConfiguration()
+                .WithAssembly(typeof(Logger).Assembly)
+                .WithAssembly(Assembly.GetExecutingAssembly());
+
+            foreach (var assemblyPath in additionalDITargets)
+            {
+                var assemblies = Assembly.LoadFrom(assemblyPath);
+
+                AddAssembliesWithType(assemblies, configuration);
+                AddAssembliesWithType(assemblies, configuration);
+            }
+
+            using (var container = configuration.CreateContainer())
+            {
+                container.SatisfyImports(this);
+                container.SatisfyImports(TelemetryRelay.Instance);
+            }
+
+            TelemetryRelay.Instance.SetTelemetryMode(baseArguments.DebugTelemetry ? TelemetryMode.Debug : TelemetryMode.Production);
+
+            bool shouldFailureBeSuppressed = false;
+
+            // Don't use the using pattern here so we can take care not to clobber the stack
+            var returnResult = BcdeExecutionTelemetryRecord.Track(
+                (record) =>
+            {
+                var executionResult = HandleCommand(args, record);
+                if (executionResult.ResultCode == ProcessingResultCode.PartialSuccess)    
+                {
+                    shouldFailureBeSuppressed = true;
+                    record.HiddenExitCode = (int)executionResult.ResultCode;
+                }
+
+                return executionResult;
+            }, true);
+
+            // The order of these things is a little weird, but done this way mostly to prevent any of the logic inside if blocks from being duplicated
+            if (shouldFailureBeSuppressed)
+            {
+                Logger.LogInfo("The scan had some detections complete while others encountered errors. The log file should indicate any issues that happened during the scan.");
+            }
+
+            if (returnResult.ResultCode == ProcessingResultCode.TimeoutError)
+            {
+                // If we have a timeout we need to tear the run down as a CYA -- this expected to fix the problem of not responding detection runs (e.g. really long runs that don't terminate when the timeout is reached).
+                // Current suspicion is that we're able to get to this point in the code without child processes cleanly cleaned up.
+                // Observation also shows that doing this is terminating the process significantly more quickly in local executions.
+                Environment.Exit(shouldFailureBeSuppressed ? 0 : (int)returnResult.ResultCode);
+            }
+
+            if (shouldFailureBeSuppressed)
+            {
+                returnResult.ResultCode = ProcessingResultCode.Success;
+            }
+
+            // We should not have input errors at this point, return it as an Error
+            if (returnResult.ResultCode == ProcessingResultCode.InputError)
+            {
+                returnResult.ResultCode = ProcessingResultCode.Error;
+            }
+
+            return returnResult;
+        }
+
+        private static void AddAssembliesWithType(Assembly assembly, ContainerConfiguration containerConfiguration)
+        {
+            assembly.GetTypes()
+                .Where(x => typeof(T).IsAssignableFrom(x)).ToList()
+                .ForEach(service => containerConfiguration = containerConfiguration.WithPart(service));
+        }
+
+        [ImportMany]
+        private static IEnumerable ArgumentHandlers { get; set; }
+
+        [Import]
+        private static Logger Logger { get; set; }
+
+        [Import]
+        private static FileWritingService FileWritingService { get; set; }
+
+        [Import]
+        private static IArgumentHelper ArgumentHelper { get; set; }
+
+        public ScanResult HandleCommand(string[] args, BcdeExecutionTelemetryRecord telemetryRecord)
+        {
+            var scanResult = new ScanResult() 
+            {
+                ResultCode = ProcessingResultCode.Error,
+            };
+
+            var parsedArguments = ArgumentHelper.ParseArguments(args)
+                .WithParsed(argumentSet =>
+                {
+                    CommandLineArgumentsExporter.ArgumentsForDelayedInjection = argumentSet;
+
+                    // Don't set production telemetry if we are running the build task in DevFabric. 0.36.0 is set in the task.json for the build task in development, but is calculated during deployment for production.
+                    TelemetryConstants.CorrelationId = argumentSet.CorrelationId;
+                    telemetryRecord.Command = GetVerb(argumentSet);
+
+                    scanResult = SafelyExecute(telemetryRecord, () =>
+                    {
+                        GenerateEnvironmentSpecificTelemetry(telemetryRecord);
+
+                        telemetryRecord.Arguments = JsonConvert.SerializeObject(argumentSet);
+                        FileWritingService.Init(argumentSet.Output);
+                        Logger.Init(argumentSet.Verbosity);
+                        Logger.LogInfo($"Run correlation id: {TelemetryConstants.CorrelationId.ToString()}");
+
+                        return Dispatch(argumentSet, CancellationToken.None).GetAwaiter().GetResult();
+                    });
+                })
+                .WithNotParsed(errors => 
+                { 
+                    if (errors.Any(e => e is HelpVerbRequestedError))
+                    {
+                        telemetryRecord.Command = "help";
+                        scanResult.ResultCode = ProcessingResultCode.Success;
+                    }
+                });
+
+            if (parsedArguments.Tag == ParserResultType.NotParsed)
+            {
+                // If the parsing failed, we already outputted an error.
+                // so just quit.
+                return scanResult;
+            }
+
+            telemetryRecord.ExitCode = (int)scanResult.ResultCode;
+            return scanResult;
+        }
+
+        private void GenerateEnvironmentSpecificTelemetry(BcdeExecutionTelemetryRecord telemetryRecord)
+        {
+            telemetryRecord.AgentOSDescription = RuntimeInformation.OSDescription;
+
+            if (IsLinux && RuntimeInformation.OSDescription.Contains("Ubuntu", StringComparison.InvariantCultureIgnoreCase))
+            {
+                const string LibSslDetailsKey = "LibSslDetailsKey";
+                var agentOSMeaningfulDetails = new Dictionary { { LibSslDetailsKey, "FailedToFetch" } };
+                var taskTimeout = TimeSpan.FromSeconds(20);
+
+                try
+                {
+                    var getLibSslPackages = Task.Run(() =>
+                    {
+                        var startInfo = new ProcessStartInfo("apt", "list --installed") { RedirectStandardOutput = true };
+                        Process process = new Process { StartInfo = startInfo };
+                        process.Start();
+                        string aptListResult = null;
+                        var task = Task.Run(() => aptListResult = process.StandardOutput.ReadToEnd());
+                        task.Wait();
+                        process.WaitForExit();
+
+                        return string.Join(Environment.NewLine, aptListResult.Split(Environment.NewLine).Where(x => x.Contains("libssl")));
+                    });
+
+                    if (!getLibSslPackages.Wait(taskTimeout))
+                    {
+                        throw new TimeoutException($"The execution did not complete in the alotted time ({taskTimeout} seconds) and has been terminated prior to completion");
+                    }
+
+                    agentOSMeaningfulDetails[LibSslDetailsKey] = getLibSslPackages.GetAwaiter().GetResult();
+                }
+                catch (Exception ex)
+                {
+                    agentOSMeaningfulDetails[LibSslDetailsKey] += Environment.NewLine + ex.ToString();
+                }
+                finally
+                {
+                    telemetryRecord.AgentOSMeaningfulDetails = JsonConvert.SerializeObject(agentOSMeaningfulDetails);
+                }
+            }
+        }
+
+        private string GetVerb(IScanArguments argumentSet)
+        {
+            var verbAttribute = argumentSet.GetType().GetCustomAttribute();
+            return verbAttribute.Name;
+        }
+
+        private async Task Dispatch(IScanArguments arguments, CancellationToken cancellation)
+        {   
+            var scanResult = new ScanResult()
+            {
+                ResultCode = ProcessingResultCode.Error,
+            };
+
+            if (ArgumentHandlers == null)
+            {
+                Logger.LogError("No argument handling services were registered.");
+                return scanResult;
+            }
+
+            foreach (var handler in ArgumentHandlers)
+            {
+                if (handler.CanHandle(arguments))
+                {
+                    try
+                    {
+                        var timeout = arguments.Timeout == 0 ? TimeSpan.FromMilliseconds(-1) : TimeSpan.FromSeconds(arguments.Timeout);
+                        scanResult = await AsyncExecution.ExecuteWithTimeoutAsync(() => handler.Handle(arguments), timeout, cancellation);
+                    }
+                    catch (TimeoutException timeoutException)
+                    {
+                        Logger.LogError(timeoutException.Message);
+                        scanResult.ResultCode = ProcessingResultCode.TimeoutError;
+                    }
+
+                    return scanResult;
+                }
+            }
+
+            Logger.LogError("No handlers for the provided Argument Set were found.");
+            return scanResult;
+        }
+
+        private ScanResult SafelyExecute(BcdeExecutionTelemetryRecord record, Func wrappedInvocation)
+        {
+            try
+            {
+                return wrappedInvocation();
+            }
+            catch (Exception ae)
+            {
+                var result = new ScanResult() 
+                {
+                    ResultCode = ProcessingResultCode.Error,
+                };
+
+                var e = ae.GetBaseException();
+                if (e is InvalidUserInputException)
+                {
+                    Logger.LogError($"Something bad happened, is everything configured correctly?");
+                    Logger.LogException(e, isError: true, printException: true);
+
+                    record.ErrorMessage = e.ToString();
+                    result.ResultCode = ProcessingResultCode.InputError;
+
+                    return result;
+                }
+                else
+                {
+                    // On an exception, return error to dotnet core
+                    Logger.LogError($"There was an unexpected error: ");
+                    Logger.LogException(e, isError: true);
+
+                    StringBuilder errorMessage = new StringBuilder();
+                    errorMessage.AppendLine(e.ToString());
+                    if (e is ReflectionTypeLoadException refEx && refEx.LoaderExceptions != null)
+                    {
+                        foreach (var loaderException in refEx.LoaderExceptions)
+                        {
+                            var loaderExceptionString = loaderException.ToString();
+                            Logger.LogError(loaderExceptionString);
+                            errorMessage.AppendLine(loaderExceptionString);
+                        }
+                    }
+
+                    record.ErrorMessage = errorMessage.ToString();
+                    return result;
+                }
+            }
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Services/BcdeDevCommandService.cs b/src/Microsoft.ComponentDetection.Orchestrator/Services/BcdeDevCommandService.cs
new file mode 100644
index 000000000..f8955419c
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Services/BcdeDevCommandService.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Composition;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Contracts.BcdeModels;
+using Microsoft.ComponentDetection.Orchestrator.ArgumentSets;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Services
+{
+    [Export(typeof(IArgumentHandlingService))]
+    public class BcdeDevCommandService : ServiceBase, IArgumentHandlingService
+    {
+        [Import]
+        public IBcdeScanExecutionService BcdeScanExecutionService { get; set; }
+
+        public bool CanHandle(IScanArguments arguments)
+        {
+            return arguments is BcdeDevArguments;
+        }
+
+        public async Task Handle(IScanArguments arguments)
+        {
+            // Run BCDE with the given arguments 
+            var detectionArguments = arguments as BcdeArguments;
+
+            var result = await BcdeScanExecutionService.ExecuteScanAsync(detectionArguments);
+            var detectedComponents = result.ComponentsFound.ToList();
+            foreach (var detectedComponent in detectedComponents)
+            {
+                Console.WriteLine(detectedComponent.Component.Id);
+            }
+
+            // TODO: Get vulnerabilities from GH Advisories 
+            return result;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Services/BcdeScanCommandService.cs b/src/Microsoft.ComponentDetection.Orchestrator/Services/BcdeScanCommandService.cs
new file mode 100644
index 000000000..8e19b0a78
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Services/BcdeScanCommandService.cs
@@ -0,0 +1,59 @@
+using System.Composition;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Common;
+using Microsoft.ComponentDetection.Contracts.BcdeModels;
+using Microsoft.ComponentDetection.Orchestrator.ArgumentSets;
+using Newtonsoft.Json;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Services
+{
+    [Export(typeof(IArgumentHandlingService))]
+    public class BcdeScanCommandService : ServiceBase, IArgumentHandlingService
+    {
+        public const string ManifestRelativePath = "ScanManifest_{timestamp}.json";
+
+        [Import]
+        public IFileWritingService FileWritingService { get; set; }
+
+        [Import]
+        public IBcdeScanExecutionService BcdeScanExecutionService { get; set; }
+
+        public bool CanHandle(IScanArguments arguments)
+        {
+            return arguments is BcdeArguments;
+        }
+
+        public async Task Handle(IScanArguments arguments)
+        {
+            BcdeArguments bcdeArguments = (BcdeArguments)arguments;
+            var result = await BcdeScanExecutionService.ExecuteScanAsync(bcdeArguments);
+            WriteComponentManifest(bcdeArguments, result);
+            return result;
+        }
+
+        private void WriteComponentManifest(IDetectionArguments detectionArguments, ScanResult scanResult)
+        {
+            FileInfo userRequestedManifestPath = null;
+
+            if (detectionArguments.ManifestFile != null)
+            {
+                Logger.LogInfo($"Scan Manifest file: {detectionArguments.ManifestFile.FullName}");
+                userRequestedManifestPath = detectionArguments.ManifestFile;
+            }
+            else
+            {
+                Logger.LogInfo($"Scan Manifest file: {FileWritingService.ResolveFilePath(ManifestRelativePath)}");
+            }
+
+            if (userRequestedManifestPath == null)
+            {
+                FileWritingService.AppendToFile(ManifestRelativePath, JsonConvert.SerializeObject(scanResult, Formatting.Indented));
+            }
+            else
+            {
+                FileWritingService.WriteFile(userRequestedManifestPath, JsonConvert.SerializeObject(scanResult, Formatting.Indented));
+            }
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Services/BcdeScanExecutionService.cs b/src/Microsoft.ComponentDetection.Orchestrator/Services/BcdeScanExecutionService.cs
new file mode 100644
index 000000000..0fbb0fc14
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Services/BcdeScanExecutionService.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.BcdeModels;
+using Microsoft.ComponentDetection.Orchestrator.ArgumentSets;
+using Microsoft.ComponentDetection.Orchestrator.Exceptions;
+using Microsoft.ComponentDetection.Orchestrator.Services.GraphTranslation;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Services
+{
+    [Export(typeof(IBcdeScanExecutionService))]
+    public class BcdeScanExecutionService : ServiceBase, IBcdeScanExecutionService
+    {
+        [Import]
+        public IDetectorRegistryService DetectorRegistryService { get; set; }
+
+        [Import]
+        public IDetectorProcessingService DetectorProcessingService { get; set; }
+
+        [Import]
+        public IDetectorRestrictionService DetectorRestrictionService { get; set; }
+
+        [ImportMany]
+        public IEnumerable> GraphTranslationServices { get; set; }
+
+        private DetectorRestrictions GetDetectorRestrictions(IDetectionArguments detectionArguments)
+        {
+            var detectorRestrictions = new DetectorRestrictions
+            {
+                AllowedDetectorIds = detectionArguments.DetectorsFilter,
+                AllowedDetectorCategories = detectionArguments.DetectorCategories,
+            };
+
+            if (detectionArguments.DetectorArgs != null && detectionArguments.DetectorArgs.Any())
+            {
+                var args = ArgumentHelper.GetDetectorArgs(detectionArguments.DetectorArgs);
+                var allEnabledDetectorIds = args.Where(x => string.Equals("EnableIfDefaultOff", x.Value, StringComparison.OrdinalIgnoreCase) || string.Equals("Enable", x.Value, StringComparison.OrdinalIgnoreCase));
+                detectorRestrictions.ExplicitlyEnabledDetectorIds = new HashSet(allEnabledDetectorIds.Select(x => x.Key), StringComparer.OrdinalIgnoreCase);
+            }
+
+            return detectorRestrictions;
+        }
+
+        public async Task ExecuteScanAsync(IDetectionArguments detectionArguments)
+        {
+            Logger.LogCreateLoggingGroup();
+            var initialDetectors = DetectorRegistryService.GetDetectors(detectionArguments.AdditionalPluginDirectories, detectionArguments.AdditionalDITargets).ToImmutableList();
+
+            if (!initialDetectors.Any())
+            {
+                throw new NoDetectorsFoundException();
+            }
+
+            DetectorRestrictions detectorRestrictions = GetDetectorRestrictions(detectionArguments);
+            var detectors = DetectorRestrictionService.ApplyRestrictions(detectorRestrictions, initialDetectors).ToImmutableList();
+
+            Logger.LogVerbose($"Finished applying restrictions to detectors.");
+            Logger.LogCreateLoggingGroup();
+
+            var processingResult = await DetectorProcessingService.ProcessDetectorsAsync(detectionArguments, detectors, detectorRestrictions);
+
+            var graphTranslationService = GraphTranslationServices.OrderBy(gts => gts.Metadata.Priority).Last().Value;
+
+            var scanResult = graphTranslationService.GenerateScanResultFromProcessingResult(processingResult, detectionArguments);
+
+            scanResult.DetectorsInScan = detectors.Select(x => ConvertToContract(x)).ToList();
+            scanResult.ResultCode = processingResult.ResultCode;
+
+            return scanResult;
+        }
+
+        private static Detector ConvertToContract(IComponentDetector detector)
+        {
+            return new Detector
+            {
+                DetectorId = detector.Id,
+                IsExperimental = detector is IExperimentalDetector,
+                Version = detector.Version,
+                SupportedComponentTypes = detector.SupportedComponentTypes,
+            };
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorListingCommandService.cs b/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorListingCommandService.cs
new file mode 100644
index 000000000..01fdae645
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorListingCommandService.cs
@@ -0,0 +1,44 @@
+using System.Composition;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.BcdeModels;
+using Microsoft.ComponentDetection.Orchestrator.ArgumentSets;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Services
+{
+    [Export(typeof(IArgumentHandlingService))]
+    public class DetectorListingCommandService : ServiceBase, IArgumentHandlingService
+    {
+        [Import]
+        public IDetectorRegistryService DetectorRegistryService { get; set; }
+
+        public bool CanHandle(IScanArguments arguments)
+        {
+            return arguments is ListDetectionArgs;
+        }
+
+        public async Task Handle(IScanArguments arguments)
+        {
+            await ListDetectorsAsync(arguments as IListDetectionArgs);
+            return new ScanResult() 
+            {
+                ResultCode = ProcessingResultCode.Success,
+            };
+        }
+
+        private async Task ListDetectorsAsync(IScanArguments listArguments)
+        {
+            var detectors = DetectorRegistryService.GetDetectors(listArguments.AdditionalPluginDirectories, listArguments.AdditionalDITargets);
+            if (detectors.Any())
+            {
+                foreach (var detector in detectors)
+                {
+                    Logger.LogInfo($"{detector.Id}");
+                }
+            }
+
+            return await Task.FromResult(ProcessingResultCode.Success);
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorProcessingResult.cs b/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorProcessingResult.cs
new file mode 100644
index 000000000..429145975
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorProcessingResult.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+using Microsoft.ComponentDetection.Common.DependencyGraph;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.BcdeModels;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Services
+{
+    public class DetectorProcessingResult
+    {
+        public ProcessingResultCode ResultCode { get; set; }
+
+        public Dictionary ContainersDetailsMap { get; set; }
+
+        public IEnumerable<(IComponentDetector detector, ComponentRecorder recorder)> ComponentRecorders { get; set; }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorProcessingService.cs b/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorProcessingService.cs
new file mode 100644
index 000000000..04ae9d514
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorProcessingService.cs
@@ -0,0 +1,327 @@
+using DotNet.Globbing;
+using Microsoft.ComponentDetection.Common;
+using Microsoft.ComponentDetection.Common.DependencyGraph;
+using Microsoft.ComponentDetection.Common.Telemetry.Records;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.BcdeModels;
+using Microsoft.ComponentDetection.Orchestrator.ArgumentSets;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Composition;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+using static System.Environment;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Services
+{
+    [Export(typeof(IDetectorProcessingService))]
+    [Shared]
+    public class DetectorProcessingService : ServiceBase, IDetectorProcessingService
+    {
+        /// 
+        /// Gets or sets the factory for handing back component streams to File detectors. Injected automatically by MEF composition.
+        /// 
+        [Import]
+        public IComponentStreamEnumerableFactory ComponentStreamEnumerableFactory { get; set; }
+
+        [Import]
+        public IObservableDirectoryWalkerFactory Scanner { get; set; }
+
+        public async Task ProcessDetectorsAsync(IDetectionArguments detectionArguments, IEnumerable detectors, DetectorRestrictions detectorRestrictions)
+        {
+            Logger.LogCreateLoggingGroup();
+            Logger.LogInfo($"Finding components...");
+
+            Stopwatch stopwatch = Stopwatch.StartNew();
+            var exitCode = ProcessingResultCode.Success;
+
+            // Run the scan on all protocol scanners and union the results
+            ConcurrentDictionary providerElapsedTime = new ConcurrentDictionary();
+            var detectorArguments = GetDetectorArgs(detectionArguments.DetectorArgs);
+
+            ExcludeDirectoryPredicate exclusionPredicate = IsOSLinuxOrMac()
+                ? GenerateDirectoryExclusionPredicate(detectionArguments.SourceDirectory.ToString(), detectionArguments.DirectoryExclusionList, detectionArguments.DirectoryExclusionListObsolete, allowWindowsPaths: false, ignoreCase: false)
+                : GenerateDirectoryExclusionPredicate(detectionArguments.SourceDirectory.ToString(), detectionArguments.DirectoryExclusionList, detectionArguments.DirectoryExclusionListObsolete, allowWindowsPaths: true, ignoreCase: true);
+
+            IEnumerable> scanTasks = detectors
+                .Select(async detector =>
+                {
+                    Stopwatch providerStopwatch = new Stopwatch();
+                    providerStopwatch.Start();
+
+                    var componentRecorder = new ComponentRecorder(Logger, !detector.NeedsAutomaticRootDependencyCalculation);
+
+                    var isExperimentalDetector = detector is IExperimentalDetector && !(detectorRestrictions.ExplicitlyEnabledDetectorIds?.Contains(detector.Id)).GetValueOrDefault();
+
+                    IEnumerable detectedComponents;
+                    ProcessingResultCode resultCode;
+                    IEnumerable containerDetails;
+                    IndividualDetectorScanResult result;
+                    using (var record = new DetectorExecutionTelemetryRecord())
+                    {
+                        result = await WithExperimentalScanGuards(
+                            () => detector.ExecuteDetectorAsync(new ScanRequest(detectionArguments.SourceDirectory, exclusionPredicate, Logger, detectorArguments, detectionArguments.DockerImagesToScan, componentRecorder)),
+                            isExperimentalDetector,
+                            record);
+
+                        // Make sure top level enumerables are at least empty and not null.
+                        result = CoalesceResult(result);
+
+                        detectedComponents = componentRecorder.GetDetectedComponents();
+                        resultCode = result.ResultCode;
+                        containerDetails = result.ContainerDetails;
+
+                        record.AdditionalTelemetryDetails = result.AdditionalTelemetryDetails != null ? JsonConvert.SerializeObject(result.AdditionalTelemetryDetails) : null;
+                        record.IsExperimental = isExperimentalDetector;
+                        record.DetectorId = detector.Id;
+                        record.DetectedComponentCount = detectedComponents.Count();
+                        var dependencyGraphs = componentRecorder.GetDependencyGraphsByLocation().Values;
+                        record.ExplicitlyReferencedComponentCount = dependencyGraphs.Select(dependencyGraph =>
+                        {
+                            return dependencyGraph.GetAllExplicitlyReferencedComponents();
+                        })
+                            .SelectMany(x => x)
+                            .Distinct()
+                            .Count();
+
+                        record.ReturnCode = (int)resultCode;
+                        record.StopExecutionTimer();
+                        providerElapsedTime.TryAdd(detector.Id + (isExperimentalDetector ? " (Beta)" : string.Empty), new DetectorRunResult
+                        {
+                            ExecutionTime = record.ExecutionTime.Value,
+                            ComponentsFoundCount = record.DetectedComponentCount.GetValueOrDefault(),
+                            ExplicitlyReferencedComponentCount = record.ExplicitlyReferencedComponentCount.GetValueOrDefault(),
+                            IsExperimental = isExperimentalDetector,
+                        });
+                    }
+
+                    if (exitCode < resultCode && !isExperimentalDetector)
+                    {
+                        exitCode = resultCode;
+                    }
+
+                    if (isExperimentalDetector)
+                    {
+                        return (new IndividualDetectorScanResult(), new ComponentRecorder(), detector);
+                    }
+                    else
+                    {
+                        return (result, componentRecorder, detector);
+                    }
+                }).ToList();
+
+            var results = await Task.WhenAll(scanTasks);
+
+            DetectorProcessingResult detectorProcessingResult = ConvertDetectorResultsIntoResult(results, exitCode);
+
+            var totalElapsedTime = stopwatch.Elapsed.TotalSeconds;
+            LogTabularOutput(Logger, providerElapsedTime, totalElapsedTime);
+
+            Logger.LogCreateLoggingGroup();
+            Logger.LogInfo($"Detection time: {totalElapsedTime} seconds.");
+
+            return detectorProcessingResult;
+        }
+
+        private IndividualDetectorScanResult CoalesceResult(IndividualDetectorScanResult individualDetectorScanResult)
+        {
+            if (individualDetectorScanResult == null)
+            {
+                individualDetectorScanResult = new IndividualDetectorScanResult();
+            }
+
+            if (individualDetectorScanResult.ContainerDetails == null)
+            {
+                individualDetectorScanResult.ContainerDetails = Enumerable.Empty();
+            }
+
+            // Additional telemetry details can safely be null
+            return individualDetectorScanResult;
+        }
+
+        private DetectorProcessingResult ConvertDetectorResultsIntoResult(IEnumerable<(IndividualDetectorScanResult result, ComponentRecorder recorder, IComponentDetector detector)> results, ProcessingResultCode exitCode)
+        {
+            return new DetectorProcessingResult
+            {
+                ComponentRecorders = results.Select(tuple => (tuple.detector, tuple.recorder)),
+                ContainersDetailsMap = results.SelectMany(x => x.result.ContainerDetails).ToDictionary(x => x.Id),
+                ResultCode = exitCode,
+            };
+        }
+
+        private async Task WithExperimentalScanGuards(Func> detectionTaskGenerator, bool isExperimentalDetector, DetectorExecutionTelemetryRecord telemetryRecord)
+        {
+            if (!isExperimentalDetector)
+            {
+                return await Task.Run(detectionTaskGenerator);
+            }
+
+            try
+            {
+                return await AsyncExecution.ExecuteWithTimeoutAsync(detectionTaskGenerator, TimeSpan.FromMinutes(2), CancellationToken.None);
+            }
+            catch (TimeoutException)
+            {
+                return new IndividualDetectorScanResult
+                {
+                    ResultCode = ProcessingResultCode.TimeoutError,
+                };
+            }
+            catch (Exception ex)
+            {
+                telemetryRecord.ExperimentalInformation = ex.ToString();
+                return new IndividualDetectorScanResult
+                {
+                    ResultCode = ProcessingResultCode.InputError,
+                };
+            }
+        }
+
+        private bool IsOSLinuxOrMac()
+        {
+            return OSVersion.Platform == PlatformID.MacOSX || OSVersion.Platform == PlatformID.Unix;
+        }
+
+        private void LogTabularOutput(ILogger logger, ConcurrentDictionary providerElapsedTime, double totalElapsedTime)
+        {
+            TabularStringFormat tsf = new TabularStringFormat(new Column[]
+                        {
+                            new Column { Header = "Component Detector Id", Width = 30 },
+                            new Column { Header = "Detection Time", Width = 30, Format = "{0:g2} seconds" },
+                            new Column { Header = "# Components Found", Width = 30, },
+                            new Column { Header = "# Explicitly Referenced", Width = 40 },
+                        });
+
+            var rows = providerElapsedTime.OrderBy(a => a.Key).Select(x =>
+            {
+                var componentResult = x.Value;
+                return new object[]
+                {
+                    x.Key,
+                    componentResult.ExecutionTime.TotalSeconds,
+                    componentResult.ComponentsFoundCount,
+                    componentResult.ExplicitlyReferencedComponentCount,
+                };
+            }).ToList();
+
+            rows.Add(new object[]
+            {
+                "Total",
+                totalElapsedTime,
+                providerElapsedTime.Sum(x => x.Value.ComponentsFoundCount),
+                providerElapsedTime.Sum(x => x.Value.ExplicitlyReferencedComponentCount),
+            });
+
+            foreach (var line in tsf.GenerateString(rows)
+                                    .Split(new string[] { Environment.NewLine }, StringSplitOptions.None))
+            {
+                Logger.LogInfo(line);
+            }
+        }
+
+        private static IDictionary GetDetectorArgs(IEnumerable detectorArgsList)
+        {
+            var detectorArgs = new Dictionary();
+
+            foreach (var arg in detectorArgsList)
+            {
+                var keyValue = arg.Split('=');
+
+                if (keyValue.Count() != 2)
+                {
+                    continue;
+                }
+
+                detectorArgs.Add(keyValue[0], keyValue[1]);
+            }
+
+            return detectorArgs;
+        }
+
+        public ExcludeDirectoryPredicate GenerateDirectoryExclusionPredicate(string originalSourceDirectory, IEnumerable directoryExclusionList, IEnumerable directoryExclusionListObsolete, bool allowWindowsPaths, bool ignoreCase = true)
+        {
+            if (directoryExclusionListObsolete?.Any() != true && directoryExclusionList?.Any() != true)
+            {
+                return (ReadOnlySpan nameOfDirectoryToConsider, ReadOnlySpan pathOfParentOfDirectoryToConsider) => false;
+            }
+
+            if (directoryExclusionListObsolete?.Any() == true)
+            {
+                var directories = directoryExclusionListObsolete
+
+                    // Note: directory info will *automatically* parent relative paths to the working directory of the current assembly. Hold on to your rear.
+                    .Select(relativeOrAbsoluteExclusionPath => new DirectoryInfo(relativeOrAbsoluteExclusionPath))
+                    .Select(exclusionDirectoryInfo => new
+                    {
+                        nameOfExcludedDirectory = exclusionDirectoryInfo.Name,
+                        pathOfParentOfDirectoryToExclude = exclusionDirectoryInfo.Parent.FullName,
+                        rootedLinuxSymlinkCompatibleRelativePathToExclude =
+                            Path.GetDirectoryName( // Get the parent of
+                                Path.IsPathRooted(exclusionDirectoryInfo.ToString())
+                                ? exclusionDirectoryInfo.ToString() // If rooted, just use the natural path
+                                : Path.Join(originalSourceDirectory, exclusionDirectoryInfo.ToString())), // If not rooted, join to sourceDir
+                    })
+                    .Distinct();
+
+                return (ReadOnlySpan nameOfDirectoryToConsiderSpan, ReadOnlySpan pathOfParentOfDirectoryToConsiderSpan) =>
+                {
+                    string pathOfParentOfDirectoryToConsider = pathOfParentOfDirectoryToConsiderSpan.ToString();
+                    string nameOfDirectoryToConsider = nameOfDirectoryToConsiderSpan.ToString();
+
+                    foreach (var valueTuple in directories)
+                    {
+                        var nameOfExcludedDirectory = valueTuple.nameOfExcludedDirectory;
+                        var pathOfParentOfDirectoryToExclude = valueTuple.pathOfParentOfDirectoryToExclude;
+
+                        if (nameOfDirectoryToConsider.Equals(nameOfExcludedDirectory, StringComparison.Ordinal)
+                        && (pathOfParentOfDirectoryToConsider.Equals(pathOfParentOfDirectoryToExclude, StringComparison.Ordinal)
+                            || pathOfParentOfDirectoryToConsider.ToString().Equals(valueTuple.rootedLinuxSymlinkCompatibleRelativePathToExclude, StringComparison.Ordinal)))
+                        {
+                            Logger.LogVerbose($"Excluding folder {Path.Combine(pathOfParentOfDirectoryToConsider.ToString(), nameOfDirectoryToConsider.ToString())}.");
+                            return true;
+                        }
+                    }
+
+                    return false;
+                };
+            }
+
+            Dictionary minimatchers = new Dictionary();
+
+            var globOptions = new GlobOptions()
+            {
+                Evaluation = new EvaluationOptions()
+                {
+                    CaseInsensitive = ignoreCase,
+                },
+            };
+
+            foreach (var directoryExclusion in directoryExclusionList)
+            {
+                minimatchers.Add(directoryExclusion, Glob.Parse(allowWindowsPaths ? directoryExclusion : /* [] escapes special chars */ directoryExclusion.Replace("\\", "[\\]"), globOptions));
+            }
+
+            return (name, directoryName) =>
+            {
+                var path = Path.Combine(directoryName.ToString(), name.ToString());
+
+                return minimatchers.Any(minimatcherKeyValue =>
+                {
+                    if (minimatcherKeyValue.Value.IsMatch(path))
+                    {
+                        Logger.LogVerbose($"Excluding folder {path} because it matched glob {minimatcherKeyValue.Key}.");
+                        return true;
+                    }
+
+                    return false;
+                });
+            };
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorRegistryService.cs b/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorRegistryService.cs
new file mode 100644
index 000000000..7ae676f4a
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorRegistryService.cs
@@ -0,0 +1,159 @@
+using System.Collections.Generic;
+using System.Composition;
+using System.Composition.Hosting;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Loader;
+using Microsoft.ComponentDetection.Common.Telemetry.Records;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.Internal;
+using Microsoft.ComponentDetection.Detectors;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Services
+{
+    // Note : This class isn't unit testable in a meaningful way. Be careful when making changes that you're sure you can test them manually. This class should remain very simple to help prevent future bugs.
+    [Export(typeof(IDetectorRegistryService))]
+    [Shared]
+    public class DetectorRegistryService : ServiceBase, IDetectorRegistryService
+    {
+        [Import]
+        public IDetectorDependencies DetectorDependencies { get; set; }
+
+        private IEnumerable ComponentDetectors { get; set; }
+
+        public IEnumerable GetDetectors(IEnumerable additionalSearchDirectories, IEnumerable extraDetectorAssemblies)
+        {
+            var executableLocation = Assembly.GetEntryAssembly().Location;
+            var searchPath = Path.Combine(Path.GetDirectoryName(executableLocation), "Plugins");
+
+            List directoriesToSearch = new List { new DirectoryInfo(searchPath) };
+
+            if (additionalSearchDirectories != null)
+            {
+                directoriesToSearch.AddRange(additionalSearchDirectories);
+            }
+
+            ComponentDetectors = GetComponentDetectors(directoriesToSearch, extraDetectorAssemblies);
+
+            if (!ComponentDetectors.Any())
+            {
+                Logger.LogError($"No component detectors were found in {searchPath} or other provided search paths.");
+            }
+
+            return ComponentDetectors;
+        }
+
+        public IEnumerable GetDetectors(Assembly assemblyToSearch, IEnumerable extraDetectorAssemblies)
+        {
+            Logger.LogInfo($"Attempting to load component detectors from {assemblyToSearch.FullName}");
+
+            var loadedDetectors = LoadComponentDetectorsFromAssemblies(new List { assemblyToSearch }, extraDetectorAssemblies);
+
+            var pluralPhrase = loadedDetectors.Count == 1 ? "detector was" : "detectors were";
+            Logger.LogInfo($"{loadedDetectors.Count()} {pluralPhrase} found in {assemblyToSearch.FullName}\n");
+
+            return loadedDetectors;
+        }
+
+        private IList GetComponentDetectors(IEnumerable searchPaths, IEnumerable extraDetectorAssemblies)
+        {
+            List detectors = new List();
+
+            using (var record = new LoadComponentDetectorsTelemetryRecord())
+            {
+                Logger.LogInfo($"Attempting to load default detectors");
+
+                var assembly = Assembly.GetAssembly(typeof(IComponentGovernanceOwnedDetectors));
+
+                var loadedDetectors = LoadComponentDetectorsFromAssemblies(new[] { assembly }, extraDetectorAssemblies);
+
+                var pluralPhrase = loadedDetectors.Count == 1 ? "detector was" : "detectors were";
+                Logger.LogInfo($"{loadedDetectors.Count()} {pluralPhrase} found in {assembly.GetName().Name}\n");
+
+                detectors.AddRange(loadedDetectors);
+
+                record.DetectorIds = string.Join(",", loadedDetectors.Select(x => x.Id));
+            }
+
+            foreach (var searchPath in searchPaths)
+            {
+                if (!searchPath.Exists)
+                {
+                    Logger.LogWarning($"Provided search path {searchPath.FullName} does not exist.");
+                    continue;
+                }
+
+                using var record = new LoadComponentDetectorsTelemetryRecord();
+
+                Logger.LogInfo($"Attempting to load component detectors from {searchPath}");
+
+                var assemblies = SafeLoadAssemblies(searchPath.GetFiles("*.dll", SearchOption.AllDirectories).Select(x => x.FullName));
+
+                var loadedDetectors = LoadComponentDetectorsFromAssemblies(assemblies, extraDetectorAssemblies);
+
+                var pluralPhrase = loadedDetectors.Count == 1 ? "detector was" : "detectors were";
+                Logger.LogInfo($"{loadedDetectors.Count()} {pluralPhrase} found in {searchPath}\n");
+
+                detectors.AddRange(loadedDetectors);
+
+                record.DetectorIds = string.Join(",", loadedDetectors.Select(x => x.Id));
+            }
+
+            return detectors;
+        }
+
+        private IList LoadComponentDetectorsFromAssemblies(IEnumerable assemblies, IEnumerable extraDetectorAssemblies)
+        {
+            new InjectionParameters(DetectorDependencies);
+            var configuration = new ContainerConfiguration()
+                .WithAssemblies(assemblies);
+
+            foreach (var detectorAssemblyPath in extraDetectorAssemblies)
+            {
+                var detectorAssembly = Assembly.LoadFrom(detectorAssemblyPath);
+                var detectorTypes = detectorAssembly.GetTypes().Where(x => typeof(IComponentDetector).IsAssignableFrom(x));
+                foreach (var detectorType in detectorTypes)
+                {
+                    configuration = configuration.WithPart(detectorType);
+                }
+            }
+
+            configuration = configuration
+                .WithPart(typeof(InjectionParameters));
+
+            using var container = configuration.CreateContainer();
+
+            return container.GetExports().ToList();
+        }
+
+        // Plugin producers may include files we have already loaded
+        private IList SafeLoadAssemblies(IEnumerable files)
+        {
+            List assemblyList = new List();
+
+            foreach (var file in files)
+            {
+                try
+                {
+                    var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
+
+                    assemblyList.Add(assembly);
+                }
+                catch (FileLoadException ex)
+                {
+                    if (ex.Message == "Assembly with same name is already loaded")
+                    {
+                        continue;
+                    }
+                    else
+                    {
+                        throw;
+                    }
+                }
+            }
+
+            return assemblyList;
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorRestrictionService.cs b/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorRestrictionService.cs
new file mode 100644
index 000000000..bd74f56ba
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorRestrictionService.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Collections.Generic;
+using System.Composition;
+using System.Linq;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Orchestrator.Exceptions;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Services
+{
+    [Export(typeof(IDetectorRestrictionService))]
+    public class DetectorRestrictionService : IDetectorRestrictionService
+    {
+        [Import]
+        public ILogger Logger { get; set; }
+
+        private IList oldDetectorIds = new List { "MSLicenseDevNpm", "MSLicenseDevNpmList", "MSLicenseNpm", "MSLicenseNpmList" };
+        private string newDetectorId = "NpmWithRoots";
+
+        public IEnumerable ApplyRestrictions(DetectorRestrictions argSpecifiedRestrictions, IEnumerable detectors)
+        {
+            // 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();
+
+            // If someone specifies an "allow list", use it, otherwise assume everything is allowed
+            if (argSpecifiedRestrictions.AllowedDetectorIds != null && argSpecifiedRestrictions.AllowedDetectorIds.Any())
+            {
+                IEnumerable allowedIds = argSpecifiedRestrictions.AllowedDetectorIds;
+
+                // If we have retired detectors in the arg specified list and don't have the new detector, add the new detector
+                if (allowedIds.Where(a => oldDetectorIds.Contains(a, StringComparer.OrdinalIgnoreCase)).Any() && !allowedIds.Contains(newDetectorId, StringComparer.OrdinalIgnoreCase))
+                {
+                    allowedIds = allowedIds.Concat(new string[] { newDetectorId });
+                }
+
+                detectors = detectors.Where(d => allowedIds.Contains(d.Id, StringComparer.OrdinalIgnoreCase)).ToList();
+
+                foreach (var id in allowedIds)
+                {
+                    if (!detectors.Select(d => d.Id).Contains(id, StringComparer.OrdinalIgnoreCase))
+                    {
+                        if (!oldDetectorIds.Contains(id, StringComparer.OrdinalIgnoreCase))
+                        {
+                            throw new InvalidDetectorFilterException($"Detector '{id}' was not found");
+                        }
+                        else
+                        {
+                            Logger.LogWarning($"The detector '{id}' has been phased out, we will run the '{newDetectorId}' detector which replaced its functionality.");
+                        }
+                    }
+                }
+            }
+
+            var allCategoryName = Enum.GetName(typeof(DetectorClass), DetectorClass.All);
+            var detectorCategories = argSpecifiedRestrictions.AllowedDetectorCategories;
+
+            // If someone specifies an "allow list", use it, otherwise assume everything is allowed
+            if (detectorCategories != null && detectorCategories.Any() && !detectorCategories.Contains(allCategoryName))
+            {
+                detectors = detectors.Where(x =>
+                {
+                    if (x.Categories != null)
+                    {
+                        // If a detector specifies the "All" category or its categories intersect with the requested categories.
+                        return x.Categories.Contains(allCategoryName) || detectorCategories.Intersect(x.Categories).Any();
+                    }
+
+                    return false;
+                }).ToList();
+                if (detectors.Count() == 0)
+                {
+                    throw new InvalidDetectorCategoriesException($"Categories {string.Join(",", detectorCategories)} did not match any available detectors.");
+                }
+            }
+
+            if (argSpecifiedRestrictions.ExplicitlyEnabledDetectorIds != null && argSpecifiedRestrictions.ExplicitlyEnabledDetectorIds.Any())
+            {
+                detectors = detectors.Union(defaultOffDetectors.Where(x => argSpecifiedRestrictions.ExplicitlyEnabledDetectorIds.Contains(x.Id))).ToList();
+            }
+
+            return detectors;
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Services/GraphTranslation/DefaultGraphTranslationService.cs b/src/Microsoft.ComponentDetection.Orchestrator/Services/GraphTranslation/DefaultGraphTranslationService.cs
new file mode 100644
index 000000000..24616deab
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Services/GraphTranslation/DefaultGraphTranslationService.cs
@@ -0,0 +1,214 @@
+using System;
+using System.Collections.Generic;
+using System.Composition;
+using System.IO;
+using System.Linq;
+using Microsoft.ComponentDetection.Common;
+using Microsoft.ComponentDetection.Common.DependencyGraph;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.BcdeModels;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Orchestrator.ArgumentSets;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Services.GraphTranslation
+{
+    [Export(typeof(IGraphTranslationService))]
+    [ExportMetadata("Priority", 0)]
+    public class DefaultGraphTranslationService : ServiceBase, IGraphTranslationService
+    {
+        public ScanResult GenerateScanResultFromProcessingResult(DetectorProcessingResult detectorProcessingResult, IDetectionArguments detectionArguments)
+        {
+            var recorderDetectorPairs = detectorProcessingResult.ComponentRecorders;
+
+            var unmergedComponents = GatherSetOfDetectedComponentsUnmerged(recorderDetectorPairs, detectionArguments.SourceDirectory);
+
+            var mergedComponents = FlattenAndMergeComponents(unmergedComponents);
+
+            return new DefaultGraphScanResult
+            {
+                ComponentsFound = mergedComponents.Select(x => ConvertToContract(x)).ToList(),
+                ContainerDetailsMap = detectorProcessingResult.ContainersDetailsMap,
+                DependencyGraphs = GraphTranslationUtility.AccumulateAndConvertToContract(recorderDetectorPairs
+                                                                    .Select(tuple => tuple.recorder)
+                                                                    .Where(x => x != null)
+                                                                    .Select(x => x.GetDependencyGraphsByLocation())),
+            };
+        }
+
+        private IEnumerable GatherSetOfDetectedComponentsUnmerged(IEnumerable<(IComponentDetector detector, ComponentRecorder recorder)> recorderDetectorPairs, DirectoryInfo rootDirectory)
+        {
+            return recorderDetectorPairs
+                .Where(recorderDetectorPair => recorderDetectorPair.recorder != null)
+                .SelectMany(recorderDetectorPair =>
+            {
+                var detector = recorderDetectorPair.detector;
+                var componentRecorder = recorderDetectorPair.recorder;
+                var detectedComponents = componentRecorder.GetDetectedComponents();
+                var dependencyGraphsByLocation = componentRecorder.GetDependencyGraphsByLocation();
+
+                // Note that it looks like we are building up detected components functionally, but they are not immutable -- the code is just written 
+                //  to look like a pipeline.
+                foreach (var component in detectedComponents)
+                {
+                    // Reinitialize properties that might still be getting populated in ways we don't want to support, because the data is authoritatively stored in the graph.
+                    component.FilePaths?.Clear();
+
+                    // Information about each component is relative to all of the graphs it is present in, so we take all graphs containing a given component and apply the graph data.
+                    foreach (var graphKvp in dependencyGraphsByLocation.Where(x => x.Value.Contains(component.Component.Id)))
+                    {
+                        var location = graphKvp.Key;
+                        var dependencyGraph = graphKvp.Value;
+
+                        // Calculate roots of the component
+                        AddRootsToDetectedComponent(component, dependencyGraph, componentRecorder);
+                        component.DevelopmentDependency = MergeDevDependency(component.DevelopmentDependency, dependencyGraph.IsDevelopmentDependency(component.Component.Id));
+                        component.DetectedBy = detector;
+
+                        // Return in a format that allows us to add the additional files for the components
+                        var locations = dependencyGraph.GetAdditionalRelatedFiles();
+                        locations.Add(location);
+
+                        var relativePaths = MakeFilePathsRelative(Logger, rootDirectory, locations);
+
+                        foreach (var additionalRelatedFile in relativePaths ?? Enumerable.Empty())
+                        {
+                            component.AddComponentFilePath(additionalRelatedFile);
+                        }
+                    }
+                }
+
+                return detectedComponents;
+            }).ToList();
+        }
+
+        private List FlattenAndMergeComponents(IEnumerable components)
+        {
+            List flattenedAndMergedComponents = new List();
+            foreach (var grouping in components.GroupBy(x => x.Component.Id + x.DetectedBy.Id))
+            {
+                flattenedAndMergedComponents.Add(MergeComponents(grouping));
+            }
+
+            return flattenedAndMergedComponents;
+        }
+
+        private bool? MergeDevDependency(bool? left, bool? right)
+        {
+            if (left == null)
+            {
+                return right;
+            }
+
+            if (right != null)
+            {
+                return left.Value && right.Value;
+            }
+
+            return left;
+        }
+
+        private DetectedComponent MergeComponents(IEnumerable enumerable)
+        {
+            if (enumerable.Count() == 1)
+            {
+                return enumerable.First();
+            }
+
+            // Multiple detected components for the same logical component id -- this happens when different files see the same component. This code should go away when we get all
+            //  mutable data out of detected component -- we can just take any component.
+            var firstComponent = enumerable.First();
+            foreach (var nextComponent in enumerable.Skip(1))
+            {
+                foreach (var filePath in nextComponent.FilePaths ?? Enumerable.Empty())
+                {
+                    firstComponent.AddComponentFilePath(filePath);
+                }
+
+                foreach (var root in nextComponent.DependencyRoots ?? Enumerable.Empty())
+                {
+                    firstComponent.DependencyRoots.Add(root);
+                }
+
+                firstComponent.DevelopmentDependency = MergeDevDependency(firstComponent.DevelopmentDependency, nextComponent.DevelopmentDependency);
+
+                if (nextComponent.ContainerDetailIds.Count > 0)
+                {
+                    foreach (var containerDetailId in nextComponent.ContainerDetailIds)
+                    {
+                        firstComponent.ContainerDetailIds.Add(containerDetailId);
+                    }
+                }
+            }
+
+            return firstComponent;
+        }
+
+        private void AddRootsToDetectedComponent(DetectedComponent detectedComponent, IDependencyGraph dependencyGraph, IComponentRecorder componentRecorder)
+        {
+            detectedComponent.DependencyRoots = detectedComponent.DependencyRoots ?? new HashSet(new ComponentComparer());
+
+            if (dependencyGraph == null)
+            {
+                return;
+            }
+
+            var roots = dependencyGraph.GetExplicitReferencedDependencyIds(detectedComponent.Component.Id);
+
+            foreach (var rootId in roots)
+            {
+                detectedComponent.DependencyRoots.Add(componentRecorder.GetComponent(rootId));
+            }
+        }
+
+        private HashSet MakeFilePathsRelative(ILogger logger, DirectoryInfo rootDirectory, HashSet filePaths)
+        {
+            if (rootDirectory == null)
+            {
+                return null;
+            }
+
+            // Make relative Uri needs a trailing separator to ensure that we turn "directory we are scanning" into "/"
+            string rootDirectoryFullName = rootDirectory.FullName;
+            if (!rootDirectory.FullName.EndsWith(Path.DirectorySeparatorChar.ToString()) && !rootDirectory.FullName.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
+            {
+                rootDirectoryFullName += Path.DirectorySeparatorChar;
+            }
+
+            var rootUri = new Uri(rootDirectoryFullName);
+            var relativePathSet = new HashSet();
+            foreach (var path in filePaths)
+            {
+                try
+                {
+                    var relativePath = rootUri.MakeRelativeUri(new Uri(path)).ToString();
+                    if (!relativePath.StartsWith("/"))
+                    {
+                        relativePath = "/" + relativePath;
+                    }
+
+                    relativePathSet.Add(relativePath);
+                }
+                catch (UriFormatException)
+                {
+                    logger.LogVerbose($"The path: {path} could not be resolved relative to the root {rootUri}");
+                }
+            }
+
+            return relativePathSet;
+        }
+
+        private ScannedComponent ConvertToContract(DetectedComponent component)
+        {
+            return new ScannedComponent
+            {
+                DetectorId = component.DetectedBy.Id,
+                IsDevelopmentDependency = component.DevelopmentDependency,
+                LocationsFoundAt = component.FilePaths,
+                Component = component.Component,
+                TopLevelReferrers = component.DependencyRoots,
+                ContainerDetailIds = component.ContainerDetailIds,
+                ContainerLayerIds = component.ContainerLayerIds,
+            };
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Services/GraphTranslation/GraphTranslationServiceMetadata.cs b/src/Microsoft.ComponentDetection.Orchestrator/Services/GraphTranslation/GraphTranslationServiceMetadata.cs
new file mode 100644
index 000000000..a3b6d2371
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Services/GraphTranslation/GraphTranslationServiceMetadata.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Services.GraphTranslation
+{
+    public class GraphTranslationServiceMetadata
+    {
+        /// 
+        /// Gets the priority level for the exported service.
+        /// This allows the importer of the graph translation service to pick the most preferred service.
+        /// 
+        [DefaultValue(0)]
+        public int Priority { get; }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Services/GraphTranslation/GraphTranslationUtility.cs b/src/Microsoft.ComponentDetection.Orchestrator/Services/GraphTranslation/GraphTranslationUtility.cs
new file mode 100644
index 000000000..aa950f2df
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Services/GraphTranslation/GraphTranslationUtility.cs
@@ -0,0 +1,78 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.BcdeModels;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Services.GraphTranslation
+{
+    public static class GraphTranslationUtility
+    {
+        public static DependencyGraphCollection AccumulateAndConvertToContract(IEnumerable> dependencyGraphs)
+        {
+            if (dependencyGraphs == null)
+            {
+                return null;
+            }
+
+            var model = new DependencyGraphCollection();
+            foreach (var graphsByLocation in dependencyGraphs)
+            {
+                foreach (var graphByLocation in graphsByLocation)
+                {
+                    if (!model.TryGetValue(graphByLocation.Key, out var graphWithMetadata))
+                    {
+                        model[graphByLocation.Key] = graphWithMetadata = new DependencyGraphWithMetadata
+                        {
+                            ExplicitlyReferencedComponentIds = new HashSet(),
+                            Graph = new DependencyGraph(),
+                            DevelopmentDependencies = new HashSet(),
+                            Dependencies = new HashSet(),
+                        };
+                    }
+
+                    foreach (var componentId in graphByLocation.Value.GetComponents())
+                    {
+                        var componentDependencies = graphByLocation.Value.GetDependenciesForComponent(componentId);
+
+                        // We set dependencies to null basically to make the serialized output look more consistent (instead of empty arrays). If another location gets merged that has dependencies, it needs to create and set the key to non-null.
+                        if (!graphWithMetadata.Graph.TryGetValue(componentId, out HashSet dependencies))
+                        {
+                            dependencies = componentDependencies != null && componentDependencies.Any() ? new HashSet() : null;
+                            graphWithMetadata.Graph[componentId] = dependencies;
+                        }
+                        else if (dependencies == null && componentDependencies != null && componentDependencies.Any())
+                        {
+                            // The explicit case where new data is found in a later graph for a given component at a location, and it is adding dependencies.
+                            graphWithMetadata.Graph[componentId] = dependencies = new HashSet();
+                        }
+
+                        foreach (var dependentComponentId in componentDependencies)
+                        {
+                            dependencies.Add(dependentComponentId);
+                        }
+
+                        if (graphByLocation.Value.IsComponentExplicitlyReferenced(componentId))
+                        {
+                            graphWithMetadata.ExplicitlyReferencedComponentIds.Add(componentId);
+                        }
+
+                        var isDevelopmentDependency = graphByLocation.Value.IsDevelopmentDependency(componentId);
+                        if (isDevelopmentDependency.HasValue)
+                        {
+                            if (isDevelopmentDependency == false)
+                            {
+                                graphWithMetadata.Dependencies.Add(componentId);
+                            }
+                            else
+                            {
+                                graphWithMetadata.DevelopmentDependencies.Add(componentId);
+                            }
+                        }
+                    }
+                }
+            }
+
+            return model;
+        }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Services/GraphTranslation/IGraphTranslationService.cs b/src/Microsoft.ComponentDetection.Orchestrator/Services/GraphTranslation/IGraphTranslationService.cs
new file mode 100644
index 000000000..56bc12b55
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Services/GraphTranslation/IGraphTranslationService.cs
@@ -0,0 +1,10 @@
+using Microsoft.ComponentDetection.Contracts.BcdeModels;
+using Microsoft.ComponentDetection.Orchestrator.ArgumentSets;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Services.GraphTranslation
+{
+    public interface IGraphTranslationService
+    {
+        ScanResult GenerateScanResultFromProcessingResult(DetectorProcessingResult detectorProcessingResult, IDetectionArguments detectionArguments);
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Services/IArgumentHandlingService.cs b/src/Microsoft.ComponentDetection.Orchestrator/Services/IArgumentHandlingService.cs
new file mode 100644
index 000000000..48cef70aa
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Services/IArgumentHandlingService.cs
@@ -0,0 +1,13 @@
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Contracts.BcdeModels;
+using Microsoft.ComponentDetection.Orchestrator.ArgumentSets;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Services
+{
+    public interface IArgumentHandlingService
+    {
+        bool CanHandle(IScanArguments arguments);
+
+        Task Handle(IScanArguments arguments);
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Services/IBcdeScanExecutionService.cs b/src/Microsoft.ComponentDetection.Orchestrator/Services/IBcdeScanExecutionService.cs
new file mode 100644
index 000000000..f242ccdd1
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Services/IBcdeScanExecutionService.cs
@@ -0,0 +1,11 @@
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Contracts.BcdeModels;
+using Microsoft.ComponentDetection.Orchestrator.ArgumentSets;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Services
+{
+    public interface IBcdeScanExecutionService
+    {
+        Task ExecuteScanAsync(IDetectionArguments detectionArguments);
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Services/IDetectorProcessingService.cs b/src/Microsoft.ComponentDetection.Orchestrator/Services/IDetectorProcessingService.cs
new file mode 100644
index 000000000..175af98fe
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Services/IDetectorProcessingService.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Orchestrator.ArgumentSets;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Services
+{
+    public interface IDetectorProcessingService
+    {
+        Task ProcessDetectorsAsync(IDetectionArguments detectionArguments, IEnumerable detectors, DetectorRestrictions detectorRestrictions);
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Services/IDetectorRegistryService.cs b/src/Microsoft.ComponentDetection.Orchestrator/Services/IDetectorRegistryService.cs
new file mode 100644
index 000000000..6d47529bf
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Services/IDetectorRegistryService.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using Microsoft.ComponentDetection.Contracts;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Services
+{
+    public interface IDetectorRegistryService
+    {
+        IEnumerable GetDetectors(IEnumerable additionalSearchDirectories, IEnumerable extraDetectorAssemblies);
+
+        IEnumerable GetDetectors(Assembly assemblyToSearch, IEnumerable extraDetectorAssemblies);
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Services/IDetectorRestrictionService.cs b/src/Microsoft.ComponentDetection.Orchestrator/Services/IDetectorRestrictionService.cs
new file mode 100644
index 000000000..4358dbb02
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Services/IDetectorRestrictionService.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+using Microsoft.ComponentDetection.Contracts;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Services
+{
+    public interface IDetectorRestrictionService
+    {
+        IEnumerable ApplyRestrictions(DetectorRestrictions restrictions, IEnumerable detectors);
+    }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Services/ServiceBase.cs b/src/Microsoft.ComponentDetection.Orchestrator/Services/ServiceBase.cs
new file mode 100644
index 000000000..3ac287da5
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Services/ServiceBase.cs
@@ -0,0 +1,11 @@
+using System.Composition;
+using Microsoft.ComponentDetection.Contracts;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Services
+{
+    public abstract class ServiceBase
+    {
+        [Import]
+        public ILogger Logger { get; set; }
+    }
+}
diff --git a/src/Microsoft.ComponentDetection/LoaderInteropConstants.cs b/src/Microsoft.ComponentDetection/LoaderInteropConstants.cs
new file mode 100644
index 000000000..ebee737d9
--- /dev/null
+++ b/src/Microsoft.ComponentDetection/LoaderInteropConstants.cs
@@ -0,0 +1,10 @@
+using Microsoft.ComponentDetection.Orchestrator;
+
+namespace Microsoft.ComponentDetection.Loader
+{
+    public static class LoaderInteropConstants
+    {
+        public static readonly string OrchestratorAssemblyModule = typeof(Orchestrator.Orchestrator).Assembly.GetName().Name;
+        public static readonly string OrchestratorTypeName = typeof(Orchestrator.Orchestrator).FullName;
+    }
+}
diff --git a/src/Microsoft.ComponentDetection/Microsoft.ComponentDetection.csproj b/src/Microsoft.ComponentDetection/Microsoft.ComponentDetection.csproj
new file mode 100644
index 000000000..34a3da7c1
--- /dev/null
+++ b/src/Microsoft.ComponentDetection/Microsoft.ComponentDetection.csproj
@@ -0,0 +1,20 @@
+
+
+    
+        Exe
+        win-x64;linux-x64;osx-x64
+        $(MSBuildThisFileDirectory)..\..\
+    
+
+    
+        
+        
+        
+        
+    
+
+    
+        
+    
+
+
diff --git a/src/Microsoft.ComponentDetection/Program.cs b/src/Microsoft.ComponentDetection/Program.cs
new file mode 100644
index 000000000..3ecaf971d
--- /dev/null
+++ b/src/Microsoft.ComponentDetection/Program.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Diagnostics;
+using System.Linq;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Orchestrator;
+
+namespace Microsoft.ComponentDetection.Loader
+{
+    public class Program
+    {
+        public static void Main(string[] args)
+        {
+            try
+            {
+                AppDomain.CurrentDomain.ProcessExit += (not, used) =>
+                {
+                    Console.WriteLine($"Process terminating.");
+                };
+
+                if (args.Any(x => string.Equals(x, "--Debug", StringComparison.OrdinalIgnoreCase)))
+                {
+                    Console.WriteLine($"Waiting for debugger attach. PID: {Process.GetCurrentProcess().Id}");
+
+                    while (!Debugger.IsAttached)
+                    {
+                        System.Threading.Tasks.Task.Delay(1000).GetAwaiter().GetResult();
+                    }
+                }
+
+                var orchestrator = new Orchestrator.Orchestrator();
+
+                var result = orchestrator.Load(args);
+
+                int exitCode = (int)result.ResultCode;
+                if (result.ResultCode == ProcessingResultCode.Error || result.ResultCode == ProcessingResultCode.InputError)
+                {
+                    exitCode = -1;
+                }
+                
+                Console.WriteLine($"Execution finished, status: {exitCode}.");
+
+                // force an exit, not letting any lingering threads not responding.
+                Environment.Exit(exitCode);
+            }
+            catch (ArgumentException ae)
+            {
+                Console.Error.WriteLine(ae.ToString());
+                Environment.Exit(-1);
+            }
+        }
+    }
+}
diff --git a/stylecop.json b/stylecop.json
new file mode 100644
index 000000000..705ef6e0c
--- /dev/null
+++ b/stylecop.json
@@ -0,0 +1,16 @@
+{
+  "$schema":
+    "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
+  "settings": {
+    "orderingRules": {
+      "usingDirectivesPlacement": "outsideNamespace",
+      "systemUsingDirectivesFirst": false,
+      "blankLinesBetweenUsingGroups": "require"
+    },
+    "documentationRules": {
+      "companyName": "Microsoft Corporation",
+      "copyrightText": "Copyright (c) {companyName}. All rights reserved.\n\nLicensed under the MIT license.",
+      "xmlHeader": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/test/Directory.Build.props b/test/Directory.Build.props
new file mode 100644
index 000000000..c7586f802
--- /dev/null
+++ b/test/Directory.Build.props
@@ -0,0 +1,18 @@
+
+
+    
+
+    
+        
+        
+        
+        
+        
+    
+
+	
+		$(MSBuildThisFileDirectory)../analyzers.ruleset
+		true
+		NU1608,NU5119,NU5100
+	
+
\ No newline at end of file
diff --git a/test/Microsoft.ComponentDetection.Common.Tests/BaseDetectionTelemetryRecordTests.cs b/test/Microsoft.ComponentDetection.Common.Tests/BaseDetectionTelemetryRecordTests.cs
new file mode 100644
index 000000000..203a25df2
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Common.Tests/BaseDetectionTelemetryRecordTests.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Reflection;
+using System.Runtime.Serialization;
+using Microsoft.ComponentDetection.Common.Telemetry.Records;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.ComponentDetection.Common.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class BaseDetectionTelemetryRecordTests
+    {
+        private Type[] recordTypes;
+
+        [TestInitialize]
+        public void Initialize()
+        {
+            // this only discovers types in a single assembly, since that's the current situation!
+            recordTypes = typeof(BaseDetectionTelemetryRecord).Assembly.GetTypes()
+                .Where(type => typeof(BaseDetectionTelemetryRecord).IsAssignableFrom(type))
+                .Where(type => !type.IsAbstract)
+                .ToArray();
+        }
+
+        [TestMethod]
+        public void UniqueRecordNames()
+        {
+            var dic = new Dictionary();
+
+            foreach (var type in recordTypes)
+            {
+                var inst = Activator.CreateInstance(type) as IDetectionTelemetryRecord;
+                Assert.IsNotNull(inst);
+
+                var recordName = inst.RecordName;
+
+                Assert.IsTrue(!string.IsNullOrEmpty(recordName), $"RecordName not set for {type.FullName}!");
+
+                if (dic.ContainsKey(recordName))
+                {
+                    Assert.Fail($"Duplicate RecordName:`{recordName}` found for {type.FullName} and {dic[recordName].FullName}!");
+                }
+                else
+                {
+                    dic.Add(recordName, type);
+                }
+            }
+        }
+
+        [TestMethod]
+        public void SerializableProperties()
+        {
+            var serializableTypes = new HashSet(new[]
+            {
+                typeof(string),
+                typeof(string[]),
+                typeof(bool),
+                typeof(int?),
+                typeof(TimeSpan?),
+                typeof(HttpStatusCode),
+            });
+
+            foreach (var type in recordTypes)
+            {
+                foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
+                {
+                    if (!serializableTypes.Contains(property.PropertyType) &&
+                        Attribute.GetCustomAttribute(property.PropertyType, typeof(DataContractAttribute)) == null)
+                    {
+                        Assert.Fail(
+                            $"Type {property.PropertyType} on {type.Name}.{property.Name} is not allowed! " +
+                            "Add it to the list if it serializes properly to JSON!");
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Common.Tests/CommandLineInvocationServiceTests.cs b/test/Microsoft.ComponentDetection.Common.Tests/CommandLineInvocationServiceTests.cs
new file mode 100644
index 000000000..ed1eaff7a
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Common.Tests/CommandLineInvocationServiceTests.cs
@@ -0,0 +1,115 @@
+using System;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.ComponentDetection.TestsUtilities;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.ComponentDetection.Common.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class CommandLineInvocationServiceTests
+    {
+        private CommandLineInvocationService commandLineService;
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            commandLineService = new CommandLineInvocationService();
+        }
+
+        [SkipTestIfNotWindows]
+        public async Task ShowsCmdExeAsExecutable()
+        {
+            Assert.IsTrue(await commandLineService.CanCommandBeLocated("cmd.exe", default, "/C"));
+        }
+
+        [SkipTestIfNotWindows]
+        public async Task FallbackWorksIfBadCommandsAreFirst()
+        {
+            Assert.IsTrue(await commandLineService.CanCommandBeLocated("57AB44A4-885A-47F4-866C-41417133B983", new[] { "fakecommandexecutable.exe", "cmd.exe" }, "/C"));
+        }
+
+        [SkipTestIfNotWindows]
+        public async Task ReturnsFalseIfNoValidCommandIsFound()
+        {
+            Assert.IsFalse(await commandLineService.CanCommandBeLocated("57AB44A4-885A-47F4-866C-41417133B983", new[] { "fakecommandexecutable.exe" }, "/C"));
+        }
+
+        [SkipTestIfNotWindows]
+        public async Task ReturnsStandardOutput()
+        {
+            var isLocated = await commandLineService.CanCommandBeLocated("cmd.exe", default, "/C");
+            Assert.IsTrue(isLocated);
+            var taskResult = await commandLineService.ExecuteCommand("cmd.exe", default, "/C echo Expected Output");
+            Assert.AreEqual(0, taskResult.ExitCode);
+            Assert.AreEqual(string.Empty, taskResult.StdErr);
+            Assert.AreEqual("Expected Output", taskResult.StdOut.Replace(System.Environment.NewLine, string.Empty));
+        }
+
+        [SkipTestIfNotWindows]
+        public async Task ExecutesCommandEvenWithLargeStdOut()
+        {
+            var isLocated = await commandLineService.CanCommandBeLocated("cmd.exe", default, "/C");
+            Assert.IsTrue(isLocated);
+            StringBuilder largeStringBuilder = new StringBuilder();
+            while (largeStringBuilder.Length < 8100) // Cmd.exe command limit is in the 8100s
+            {
+                largeStringBuilder.Append("Some sample text");
+            }
+
+            var taskResult = await commandLineService.ExecuteCommand("cmd.exe", default, $"/C echo {largeStringBuilder.ToString()}");
+            Assert.AreEqual(0, taskResult.ExitCode);
+            Assert.AreEqual(string.Empty, taskResult.StdErr);
+            Assert.IsTrue(taskResult.StdOut.Length > 8099, taskResult.StdOut.Length < 100 ? $"Stdout was '{taskResult.StdOut}', which is shorter than 8100 chars" : $"Length was {taskResult.StdOut.Length}, which is less than 8100");
+        }
+
+        [SkipTestIfNotWindows]
+        public async Task ExecutesCommandCapturingErrorOutput()
+        {
+            var isLocated = await commandLineService.CanCommandBeLocated("cmd.exe", default, "/C");
+            Assert.IsTrue(isLocated);
+            StringBuilder largeStringBuilder = new StringBuilder();
+            while (largeStringBuilder.Length < 9000) // Pick a command that is "too big" for cmd.
+            {
+                largeStringBuilder.Append("Some sample text");
+            }
+
+            var taskResult = await commandLineService.ExecuteCommand("cmd.exe", default, $"/C echo {largeStringBuilder.ToString()}");
+            Assert.AreEqual(1, taskResult.ExitCode);
+            Assert.IsTrue(taskResult.StdErr.Contains("too long"), $"Expected '{taskResult.StdErr}' to contain 'too long'");
+            Assert.AreEqual(string.Empty, taskResult.StdOut);
+        }
+
+        [SkipTestIfNotWindows]
+        public async Task ExecutesInAWorkingDirectory()
+        {
+            var isLocated = await commandLineService.CanCommandBeLocated("cmd.exe", default, "/C");
+            Assert.IsTrue(isLocated);
+            string tempDirectoryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+            var tempDirectory = Directory.CreateDirectory(tempDirectoryPath);
+
+            var taskResult = await commandLineService.ExecuteCommand("cmd.exe", default, workingDirectory: tempDirectory, "/C cd");
+            taskResult.ExitCode.Should().Be(0);
+            taskResult.StdOut.Should().Contain(tempDirectoryPath);
+        }
+
+        [SkipTestIfNotWindows]
+        public async Task ThrowsIfWorkingDirectoryDoesNotExist()
+        {
+            var isLocated = await commandLineService.CanCommandBeLocated("cmd.exe", default, "/C");
+            Assert.IsTrue(isLocated);
+
+            var tempDirectory = new DirectoryInfo(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()));
+
+            Func action = async () => await commandLineService.ExecuteCommand("cmd.exe", default, workingDirectory: tempDirectory, "/C cd");
+
+            await action.Should()
+                .ThrowAsync()
+                .WithMessage("ExecuteCommand was called with a working directory that could not be located: *");
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Common.Tests/ComponentRecorderTests.cs b/test/Microsoft.ComponentDetection.Common.Tests/ComponentRecorderTests.cs
new file mode 100644
index 000000000..f04ab88d2
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Common.Tests/ComponentRecorderTests.cs
@@ -0,0 +1,214 @@
+using System;
+using System.Collections.Generic;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Common.DependencyGraph;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.ComponentDetection.Common.Tests
+{
+    [TestClass]
+    public class ComponentRecorderTests
+    {
+        private ComponentRecorder componentRecorder;
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            componentRecorder = new ComponentRecorder();
+        }
+
+        [TestMethod]
+        public void RegisterUsage_RegisterNewDetectedComponent_NodeInTheGraphIsCreated()
+        {
+            var location = "location";
+
+            var singleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder(location);
+
+            var detectedComponent = new DetectedComponent(new NpmComponent("test", "1.0.0"));
+            singleFileComponentRecorder.RegisterUsage(detectedComponent);
+            singleFileComponentRecorder.GetComponent(detectedComponent.Component.Id).Should().NotBeNull();
+
+            var dependencyGraph = componentRecorder.GetDependencyGraphForLocation(location);
+
+            dependencyGraph.GetDependenciesForComponent(detectedComponent.Component.Id).Should().NotBeNull();
+        }
+
+        [TestMethod]
+        public void RegisterUsage_NewDetectedComponentHasParent_NewRelationshipIsInserted()
+        {
+            var location = "location";
+
+            var singleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder(location);
+
+            var detectedComponent = new DetectedComponent(new NpmComponent("test", "1.0.0"));
+            var parentComponent = new DetectedComponent(new NpmComponent("test2", "2.0.0"));
+
+            singleFileComponentRecorder.RegisterUsage(parentComponent);
+            singleFileComponentRecorder.RegisterUsage(detectedComponent, parentComponentId: parentComponent.Component.Id);
+
+            var dependencyGraph = componentRecorder.GetDependencyGraphForLocation(location);
+
+            dependencyGraph.GetDependenciesForComponent(parentComponent.Component.Id).Should().Contain(detectedComponent.Component.Id);
+        }
+
+        [TestMethod]
+        public void RegisterUsage_DetectedComponentIsNull_ArgumentNullExceptionIsThrown()
+        {
+            var singleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder("location");
+
+            Action action = () => singleFileComponentRecorder.RegisterUsage(null);
+
+            action.Should().Throw();
+        }
+
+        [TestMethod]
+        public void RegisterUsage_DetectedComponentWithNullComponent_ArgumentExceptionIsThrown()
+        {
+            var singleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder("location");
+            var detectedComponent = new DetectedComponent(null);
+
+            Action action = () => singleFileComponentRecorder.RegisterUsage(detectedComponent);
+
+            action.Should().Throw();
+        }
+
+        [TestMethod]
+        public void RegisterUsage_DetectedComponentExistAndUpdateFunctionIsNull_NotExceptionIsThrown()
+        {
+            var singleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder("location");
+            var detectedComponent = new DetectedComponent(new NpmComponent("test", "1.0.0"));
+            singleFileComponentRecorder.RegisterUsage(detectedComponent);
+
+            Action action = () => singleFileComponentRecorder.RegisterUsage(detectedComponent);
+
+            action.Should().NotThrow();
+        }
+
+        [TestMethod]
+        public void CreateComponentsingleFileComponentRecorderForLocation_LocationIsNull_ArgumentNullExceptionIsThrown()
+        {
+            Action action = () => componentRecorder.CreateSingleFileComponentRecorder(null);
+            action.Should().Throw();
+
+            action = () => componentRecorder.CreateSingleFileComponentRecorder(string.Empty);
+            action.Should().Throw();
+
+            action = () => componentRecorder.CreateSingleFileComponentRecorder("  ");
+            action.Should().Throw();
+        }
+
+        [TestMethod]
+        public void GetComponent_ComponentNotExist_NullIsReturned()
+        {
+            componentRecorder.CreateSingleFileComponentRecorder("someMockLocation").GetComponent("nonexistedcomponentId").Should().BeNull();
+        }
+
+        [TestMethod]
+        public void GetDetectedComponents_AreComponentsRegistered_ComponentsAreReturned()
+        {
+            var singleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder("location");
+            var detectedComponent1 = new DetectedComponent(new NpmComponent("test", "1.0.0"));
+            var detectedComponent2 = new DetectedComponent(new NpmComponent("test", "2.0.0"));
+
+            singleFileComponentRecorder.RegisterUsage(detectedComponent1);
+            singleFileComponentRecorder.RegisterUsage(detectedComponent2);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+
+            detectedComponents.Should().HaveCount(2);
+            detectedComponents.Should().Contain(detectedComponent1);
+            detectedComponents.Should().Contain(detectedComponent2);
+        }
+
+        [TestMethod]
+        public void GetDetectedComponents_NoComponentsAreRegistered_EmptyCollectionIsReturned()
+        {
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+
+            detectedComponents.Should().NotBeNull();
+            detectedComponents.Should().BeEmpty();
+        }
+
+        [TestMethod]
+        public void GetAllDependencyGraphs_ReturnsImmutableDictionaryWithContents()
+        {
+            // Setup an initial, simple graph.
+            var singleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder("/some/location");
+
+            // We want to take a look at how the class is used by it's friends
+            var internalsView = (ComponentRecorder.SingleFileComponentRecorder)singleFileComponentRecorder;
+            var graph = internalsView.DependencyGraph;
+            var component1 = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "component1" };
+            graph.AddComponent(component1);
+            var component2 = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "component2" };
+            graph.AddComponent(component2);
+
+            component2.DependedOnByIds.Add(component1.Id);
+            component1.DependencyIds.Add(component2.Id);
+
+            // Get readonly content from graph
+            var allGraphs = componentRecorder.GetDependencyGraphsByLocation();
+            var expectedGraph = allGraphs["/some/location"];
+
+            // Verify content looks correct
+            expectedGraph.Should().NotBeNull();
+            var allComponents = expectedGraph.GetComponents();
+            allComponents.Should().Contain(component1.Id);
+            allComponents.Should().Contain(component2.Id);
+
+            var component1Deps = expectedGraph.GetDependenciesForComponent(component1.Id);
+            component1Deps.Should().Contain(component2.Id);
+
+            var component2Deps = expectedGraph.GetDependenciesForComponent(component2.Id);
+            component2Deps.Should().BeEmpty();
+
+            // Verify dictionary is immutable (type enforces, make sure we can't interact with a downcasted dictionary)
+            var asDictionary = allGraphs as IDictionary;
+            asDictionary.Should().NotBeNull();
+            Action attemptedSet = () => asDictionary["/some/location"] = null;
+            attemptedSet.Should().Throw();
+
+            Action attemptedClear = () => asDictionary.Clear();
+            attemptedClear.Should().Throw();
+        }
+
+        [TestMethod]
+        public void GetAllDependencyGraphs_ReturnedGraphsAreImmutable()
+        {
+            // Setup an initial, simple graph.
+            var singleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder("/some/location");
+
+            // We want to take a look at how the class is used by it's friends
+            var internalsView = (ComponentRecorder.SingleFileComponentRecorder)singleFileComponentRecorder;
+            var graph = internalsView.DependencyGraph;
+
+            var component1 = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "component1" };
+            graph.AddComponent(component1);
+            var component2 = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "component2" };
+            graph.AddComponent(component2);
+
+            component2.DependedOnByIds.Add(component1.Id);
+            component1.DependencyIds.Add(component2.Id);
+
+            // Get readonly content from graph
+            var allGraphs = componentRecorder.GetDependencyGraphsByLocation();
+            var expectedGraph = allGraphs["/some/location"];
+
+            // Verify content looks correct
+            var component1Deps = expectedGraph.GetDependenciesForComponent(component1.Id);
+            component1Deps.Should().NotBeEmpty();
+
+            // Verify componentId set can't be interacted with after downcasting
+            var asCollection = component1Deps as ICollection;
+            asCollection.Should().NotBeNull();
+
+            Action attemptedClear = () => asCollection.Clear();
+            attemptedClear.Should().Throw();
+
+            Action attemptedAdd = () => asCollection.Add("should't work");
+            attemptedAdd.Should().Throw();
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Common.Tests/ComponentStreamEnumerableTests.cs b/test/Microsoft.ComponentDetection.Common.Tests/ComponentStreamEnumerableTests.cs
new file mode 100644
index 000000000..67bd18c1c
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Common.Tests/ComponentStreamEnumerableTests.cs
@@ -0,0 +1,85 @@
+using System.IO;
+using System.Linq;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Microsoft.ComponentDetection.Common.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class ComponentStreamEnumerableTests
+    {
+        private Mock loggerMock;
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            loggerMock = new Mock();
+        }
+
+        [TestMethod]
+        public void GetEnumerator_WorksOverExpectedFiles()
+        {
+            var tempFileOne = Path.GetTempFileName();
+            var tempFileTwo = Path.GetTempFileName();
+            var enumerable = new ComponentStreamEnumerable(
+                new[]
+            {
+                new MatchedFile
+                {
+                    File = new FileInfo(tempFileOne),
+                    Pattern = "Some Pattern",
+                },
+                new MatchedFile
+                {
+                    File = new FileInfo(tempFileTwo),
+                    Pattern = "Some Pattern",
+                },
+            }, loggerMock.Object);
+
+            enumerable.Count()
+                .Should().Be(2);
+            foreach (var file in enumerable)
+            {
+                file.Stream
+                    .Should().NotBeNull();
+                file.Pattern
+                    .Should().NotBeNull();
+                file.Location
+                    .Should().BeOneOf(tempFileOne, tempFileTwo);
+            }
+        }
+
+        [TestMethod]
+        public void GetEnumerator_LogsAndBreaksEnumerationWhenFileIsMissing()
+        {
+            var tempFileOne = Path.GetTempFileName();
+            var tempFileTwo = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+            var tempFileThree = Path.GetTempFileName();
+            File.Delete(tempFileTwo);
+            loggerMock.Setup(x => x.LogWarning(Match.Create(message => message.Contains("not exist"))));
+            var enumerable = new ComponentStreamEnumerable(
+                new[]
+            {
+                new MatchedFile
+                {
+                    File = new FileInfo(tempFileOne),
+                    Pattern = "Some Pattern",
+                },
+                new MatchedFile
+                {
+                    File = new FileInfo(tempFileTwo),
+                    Pattern = "Some Pattern",
+                },
+            }, loggerMock.Object).ToList();
+
+            enumerable.Count()
+                .Should().Be(1);
+
+            loggerMock.VerifyAll();
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Common.Tests/ConsoleWritingServiceTests.cs b/test/Microsoft.ComponentDetection.Common.Tests/ConsoleWritingServiceTests.cs
new file mode 100644
index 000000000..d2a0e0443
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Common.Tests/ConsoleWritingServiceTests.cs
@@ -0,0 +1,34 @@
+using System;
+using System.IO;
+using FluentAssertions;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.ComponentDetection.Common.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class ConsoleWritingServiceTests
+    {
+        private static TestContext testContext;
+
+        [AssemblyInitialize]
+        public static void AssemblyInitialize(TestContext inputTestContext)
+        {
+            testContext = inputTestContext;
+        }
+
+        [TestMethod]
+        public void Write_Writes()
+        {
+            var service = new ConsoleWritingService();
+            var guid = Guid.NewGuid().ToString();
+            var writer = new StringWriter();
+            Console.SetOut(writer);
+            service.Write(guid);
+            var obj = new object();
+            writer.ToString()
+                .Should().Contain(guid);
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Common.Tests/CreateDirectoryTraversalStructure.ps1 b/test/Microsoft.ComponentDetection.Common.Tests/CreateDirectoryTraversalStructure.ps1
new file mode 100644
index 000000000..305d6b8c8
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Common.Tests/CreateDirectoryTraversalStructure.ps1
@@ -0,0 +1,77 @@
+$testTreeRootName = [System.IO.Path]::Combine([System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "test-tree"), [System.Guid]::NewGuid())
+
+if (Test-Path $testTreeRootName) {
+    Remove-Item $testTreeRootName
+}
+
+function CreateValidFileTree {
+    param (
+        [string]$rootPath
+    )
+    Push-Location -Path $rootPath
+    New-Item -ItemType Directory -Path ./a/b/c/d/e/f
+    Out-File -FilePath ./a/a.txt
+    Out-File -FilePath ./a/b/b.txt
+    Out-File -FilePath ./a/b/c/c.txt
+    Out-File -FilePath ./a/b/c/d/d.txt
+    Out-File -FilePath ./a/b/c/d/e/e.txt
+    Out-File -FilePath ./a/b/c/d/e/f/f.txt
+    Pop-Location
+}
+
+New-item -ItemType Directory -Path $testTreeRootName
+
+Push-Location -Path $testTreeRootName
+
+New-Item -ItemType Directory -Path ./root/cycle
+CreateValidFileTree ./root/cycle
+New-Item -ItemType SymbolicLink -Path ./root/cycle/a/b/c/d/link-to-b -Target ./root/cycle/a/b
+
+New-Item -ItemType Directory -Path ./root/recursive-links
+CreateValidFileTree ./root/recursive-links
+New-Item -ItemType Directory -Path ./root/recursive-links/link-to-a
+New-Item -ItemType SymbolicLink -Path ./root/recursive-links/link-to-b -Target './root/recursive-links/link-to-a'
+Remove-Item ./root/recursive-links/link-to-a
+New-Item -ItemType SymbolicLink -Path ./root/recursive-links/link-to-a -Target './root/recursive-links/link-to-b'
+
+New-Item -ItemType Directory -Path ./outside-root
+CreateValidFileTree ./outside-root
+
+New-Item -ItemType Directory -Path ./root/valid-links
+CreateValidFileTree ./root/valid-links
+New-Item -ItemType SymbolicLink -Path ./root/valid-links/first-link-to-b -Target ./root/valid-links/a/b
+New-Item -ItemType SymbolicLink -Path ./root/valid-links/second-link-to-b -Target ./root/valid-links/a/b
+New-Item -ItemType SymbolicLink -Path ./root/valid-links/first-link-to-outside -Target ./outside-root/a/b
+New-Item -ItemType SymbolicLink -Path ./root/valid-links/second-link-to-outside -Target ./outside-root
+
+New-Item -ItemType SymbolicLink -Path ./outside-root/link-to-root-parent -Target ./
+New-Item -ItemType SymbolicLink -Path ./outside-root/link-to-root -Target ./root
+
+if ([System.Environment]::OSVersion.Platform -eq "Win32NT") {
+    New-Item -ItemType Directory -Path ./outside-root-two
+    CreateValidFileTree ./outside-root-two
+
+    New-Item -ItemType Directory -Path ./root/junctions
+    New-Item -ItemType Junction -Path ./root/junctions/unknown-files-junction -Target ./outside-root-two
+    New-Item -ItemType Junction -Path ./root/junctions/known-files-junction -Target ./outside-root
+    
+    New-Item -ItemType Directory -Path ./outside-junction-cycles-a
+    CreateValidFileTree ./outside-junction-cycles-a
+    
+    New-Item -ItemType Directory -Path ./outside-junction-cycles-b
+    CreateValidFileTree ./outside-junction-cycles-b
+    
+    New-Item -ItemType Directory -Path ./root/junction-cycles
+    CreateValidFileTree ./root/junction-cycles
+    
+    New-Item -ItemType Junction -Path ./root/junction-cycles/a/b/c/d/e/f/junction-to-outside-q -Target ./outside-junction-cycles-a
+    New-Item -ItemType Junction -Path ./root/junction-cycles/a/b/c/d/e/f/junction-to-outside-r -Target ./outside-junction-cycles-b
+    New-Item -ItemType Junction -Path ./root/junction-cycles/a/b/c/d/e/f/junction-to-outside-s -Target ./outside-junction-cycles-a
+    New-Item -ItemType Junction -Path ./root/junction-cycles/a/b/c/d/e/f/junction-to-outside-t -Target ./outside-junction-cycles-b
+    New-Item -ItemType Junction -Path ./outside-junction-cycles-a/junction-to-outside-b -Target ./outside-junction-cycles-b
+    New-Item -ItemType Junction -Path ./outside-junction-cycles-b/junction-to-outside-a -Target ./outside-junction-cycles-a/a/b
+    New-Item -ItemType Junction -Path ./outside-junction-cycles-b/junction-to-outside-a-b -Target ./outside-junction-cycles-a/junction-to-outside-b
+}
+
+Write-Host "##vso[task.setvariable variable=COMPONENT_DETECTION_SYMLINK_TEST]$testTreeRootName"
+Pop-Location
\ No newline at end of file
diff --git a/test/Microsoft.ComponentDetection.Common.Tests/DependencyGraphTests.cs b/test/Microsoft.ComponentDetection.Common.Tests/DependencyGraphTests.cs
new file mode 100644
index 000000000..ec7018d53
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Common.Tests/DependencyGraphTests.cs
@@ -0,0 +1,354 @@
+using System;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.ComponentDetection.Common.Tests
+{
+    [TestClass]
+    public class DependencyGraphTests
+    {
+        private DependencyGraph.DependencyGraph dependencyGraph;
+
+        [TestInitialize]
+        public void TestInitializer()
+        {
+            // Default value of true -- some tests will create their own, though.
+            dependencyGraph = new DependencyGraph.DependencyGraph(true);
+        }
+
+        [TestMethod]
+        public void AddComponent_ParentComponentIdIsPresent_DependencyRelationIsAdded()
+        {
+            var componentA = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentA" };
+            var componentB = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentB" };
+            var componentC = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentC" };
+            var componentD = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentD" };
+
+            dependencyGraph.AddComponent(componentD);
+            dependencyGraph.AddComponent(componentB, parentComponentId: componentD.Id);
+            dependencyGraph.AddComponent(componentC, parentComponentId: componentB.Id);
+            dependencyGraph.AddComponent(componentA, parentComponentId: componentB.Id);
+            dependencyGraph.AddComponent(componentA, parentComponentId: componentC.Id);
+
+            var componentAChildren = dependencyGraph.GetDependenciesForComponent(componentA.Id);
+            componentAChildren.Should().HaveCount(0);
+
+            var componentBChildren = dependencyGraph.GetDependenciesForComponent(componentB.Id);
+            componentBChildren.Should().HaveCount(2);
+            componentBChildren.Should().Contain(componentA.Id);
+            componentBChildren.Should().Contain(componentC.Id);
+
+            var componentCChildren = dependencyGraph.GetDependenciesForComponent(componentC.Id);
+            componentCChildren.Should().HaveCount(1);
+            componentCChildren.Should().Contain(componentA.Id);
+
+            var componentDChildren = dependencyGraph.GetDependenciesForComponent(componentD.Id);
+            componentDChildren.Should().HaveCount(1);
+            componentDChildren.Should().Contain(componentB.Id);
+        }
+
+        [TestMethod]
+        public void AddComponent_parentComponentIdIsNotPresent_AdditionTakePlaceWithoutThrowing()
+        {
+            var componentA = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentA", IsExplicitReferencedDependency = true };
+
+            Action action = () => dependencyGraph.AddComponent(componentA);
+            action.Should().NotThrow();
+
+            dependencyGraph.Contains(componentA.Id).Should().BeTrue();
+        }
+
+        [TestMethod]
+        public void AddComponent_ComponentIsNull_ArgumentNullExceptionIsThrow()
+        {
+            Action action = () => dependencyGraph.AddComponent(null);
+
+            action.Should().Throw();
+        }
+
+        [TestMethod]
+        public void AddComponent_ComponentHasNoId_ArgumentNullExceptionIsThrow()
+        {
+            var component = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = null };
+            Action action = () => dependencyGraph.AddComponent(component);
+            action.Should().Throw();
+
+            component = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = string.Empty };
+            action = () => dependencyGraph.AddComponent(component);
+            action.Should().Throw();
+
+            component = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "   " };
+            action = () => dependencyGraph.AddComponent(component);
+            action.Should().Throw();
+        }
+
+        [TestMethod]
+        public void AddComponent_ParentComponentWasNotAddedPreviously_ArgumentExceptionIsThrown()
+        {
+            var componentA = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentA" };
+
+            Action action = () => dependencyGraph.AddComponent(componentA, parentComponentId: "nonexistingComponent");
+
+            action.Should().Throw();
+        }
+
+        [TestMethod]
+        public void GetExplicitReferencedDependencyIds_ComponentsWereAddedSpecifyingRoot_RootsAreReturned()
+        {
+            var componentA = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentA", IsExplicitReferencedDependency = true };
+            var componentB = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentB" };
+            var componentC = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentC" };
+            var componentD = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentD" };
+            var componentE = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentE", IsExplicitReferencedDependency = true };
+            var componentF = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentF" };
+
+            dependencyGraph.AddComponent(componentA);
+            dependencyGraph.AddComponent(componentB, componentA.Id);
+            dependencyGraph.AddComponent(componentC, componentB.Id);
+            dependencyGraph.AddComponent(componentE);
+            dependencyGraph.AddComponent(componentD, componentE.Id);
+            dependencyGraph.AddComponent(componentC, componentD.Id);
+            dependencyGraph.AddComponent(componentF, componentC.Id);
+
+            var rootsForComponentA = dependencyGraph.GetExplicitReferencedDependencyIds(componentA.Id);
+            rootsForComponentA.Should().HaveCount(1);
+
+            var rootsForComponentE = dependencyGraph.GetExplicitReferencedDependencyIds(componentE.Id);
+            rootsForComponentE.Should().HaveCount(1);
+
+            var rootsForComponentB = dependencyGraph.GetExplicitReferencedDependencyIds(componentB.Id);
+            rootsForComponentB.Should().HaveCount(1);
+            rootsForComponentB.Should().Contain(componentA.Id);
+
+            var rootsForComponentD = dependencyGraph.GetExplicitReferencedDependencyIds(componentD.Id);
+            rootsForComponentD.Should().HaveCount(1);
+            rootsForComponentD.Should().Contain(componentE.Id);
+
+            var rootsForComponentC = dependencyGraph.GetExplicitReferencedDependencyIds(componentC.Id);
+            rootsForComponentC.Should().HaveCount(2);
+            rootsForComponentC.Should().Contain(componentA.Id);
+            rootsForComponentC.Should().Contain(componentE.Id);
+
+            var rootsForComponentF = dependencyGraph.GetExplicitReferencedDependencyIds(componentF.Id);
+            rootsForComponentF.Should().HaveCount(2);
+            rootsForComponentF.Should().Contain(componentA.Id);
+            rootsForComponentF.Should().Contain(componentE.Id);
+        }
+
+        [TestMethod]
+        public void GetExplicitReferencedDependencyIds_ComponentsWereAddedWithoutSpecifyingRoot_RootsAreEmpty()
+        {
+            var componentA = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentA" };
+            var componentB = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentB" };
+
+            dependencyGraph.AddComponent(componentA);
+            dependencyGraph.AddComponent(componentB, componentA.Id);
+
+            var rootsForComponentA = dependencyGraph.GetExplicitReferencedDependencyIds(componentA.Id);
+            rootsForComponentA.Should().HaveCount(0);
+
+            var rootsForComponentB = dependencyGraph.GetExplicitReferencedDependencyIds(componentB.Id);
+            rootsForComponentB.Should().HaveCount(0);
+        }
+
+        [TestMethod]
+        public void GetExplicitReferencedDependencyIds_ComponentIsRoot_ARootIsRootOfItSelf()
+        {
+            var componentA = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentA", IsExplicitReferencedDependency = true };
+            dependencyGraph.AddComponent(componentA);
+
+            var aRoots = dependencyGraph.GetExplicitReferencedDependencyIds(componentA.Id);
+            aRoots.Should().HaveCount(1);
+            aRoots.Should().Contain(componentA.Id);
+        }
+
+        [TestMethod]
+        public void GetExplicitReferencedDependencyIds_RootHasParent_ReturnItselfAndItsParents()
+        {
+            var componentA = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentA", IsExplicitReferencedDependency = true };
+            var componentB = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentB", IsExplicitReferencedDependency = true };
+            var componentC = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentC", IsExplicitReferencedDependency = true };
+
+            dependencyGraph.AddComponent(componentA);
+            dependencyGraph.AddComponent(componentB, componentA.Id);
+            dependencyGraph.AddComponent(componentC, componentB.Id);
+
+            var aRoots = dependencyGraph.GetExplicitReferencedDependencyIds(componentA.Id);
+            aRoots.Should().HaveCount(1);
+            aRoots.Should().Contain(componentA.Id);
+
+            var bRoots = dependencyGraph.GetExplicitReferencedDependencyIds(componentB.Id);
+            bRoots.Should().HaveCount(2);
+            bRoots.Should().Contain(componentA.Id);
+            bRoots.Should().Contain(componentB.Id);
+
+            var cRoots = dependencyGraph.GetExplicitReferencedDependencyIds(componentC.Id);
+            cRoots.Should().HaveCount(3);
+            cRoots.Should().Contain(componentA.Id);
+            cRoots.Should().Contain(componentB.Id);
+            cRoots.Should().Contain(componentC.Id);
+        }
+
+        [TestMethod]
+        public void GetExplicitReferencedDependencyIds_InsertionOrderNotAffectedRoots()
+        {
+            var componentA = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentA", IsExplicitReferencedDependency = true };
+            var componentB = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentB" };
+            var componentC = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentC", IsExplicitReferencedDependency = true };
+
+            dependencyGraph.AddComponent(componentA);
+            dependencyGraph.AddComponent(componentB, componentA.Id);
+            dependencyGraph.AddComponent(componentC);
+            dependencyGraph.AddComponent(componentA, componentC.Id);
+
+            componentB = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentB", IsExplicitReferencedDependency = true };
+            dependencyGraph.AddComponent(componentB);
+
+            var aRoots = dependencyGraph.GetExplicitReferencedDependencyIds(componentA.Id);
+            aRoots.Should().HaveCount(2);
+            aRoots.Should().Contain(componentA.Id);
+            aRoots.Should().Contain(componentC.Id);
+
+            var bRoots = dependencyGraph.GetExplicitReferencedDependencyIds(componentB.Id);
+            bRoots.Should().HaveCount(3);
+            bRoots.Should().Contain(componentA.Id);
+            bRoots.Should().Contain(componentB.Id);
+            bRoots.Should().Contain(componentC.Id);
+
+            var cRoots = dependencyGraph.GetExplicitReferencedDependencyIds(componentC.Id);
+            cRoots.Should().HaveCount(1);
+            cRoots.Should().Contain(componentC.Id);
+        }
+
+        [TestMethod]
+        public void GetExplicitReferencedDependencyIds_UseManualSelectionTurnedOff_ComponentsWithNoParentsAreSelectedAsExplicitReferencedDependencies()
+        {
+            dependencyGraph = new DependencyGraph.DependencyGraph(false);
+            var componentA = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentA" };
+            var componentB = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentB" };
+            var componentC = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentC" };
+
+            dependencyGraph.AddComponent(componentA);
+            dependencyGraph.AddComponent(componentB, componentA.Id);
+            dependencyGraph.AddComponent(componentC);
+            dependencyGraph.AddComponent(componentA, componentC.Id);
+
+            var aRoots = dependencyGraph.GetExplicitReferencedDependencyIds(componentA.Id);
+            aRoots.Should().HaveCount(1);
+            aRoots.Should().Contain(componentC.Id);
+            ((IDependencyGraph)dependencyGraph).IsComponentExplicitlyReferenced(componentA.Id).Should().BeFalse();
+
+            var bRoots = dependencyGraph.GetExplicitReferencedDependencyIds(componentB.Id);
+            bRoots.Should().HaveCount(1);
+            bRoots.Should().Contain(componentC.Id);
+            ((IDependencyGraph)dependencyGraph).IsComponentExplicitlyReferenced(componentB.Id).Should().BeFalse();
+
+            var cRoots = dependencyGraph.GetExplicitReferencedDependencyIds(componentC.Id);
+            cRoots.Should().HaveCount(1);
+            cRoots.Should().Contain(componentC.Id);
+            ((IDependencyGraph)dependencyGraph).IsComponentExplicitlyReferenced(componentC.Id).Should().BeTrue();
+        }
+
+        [TestMethod]
+        public void GetExplicitReferencedDependencyIds_UseManualSelectionTurnedOff_PropertyIsExplicitReferencedDependencyIsIgnored()
+        {
+            dependencyGraph = new DependencyGraph.DependencyGraph(false);
+            var componentA = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentA", IsExplicitReferencedDependency = true };
+            var componentB = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentB", IsExplicitReferencedDependency = true };
+            var componentC = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentC", IsExplicitReferencedDependency = true };
+
+            dependencyGraph.AddComponent(componentA);
+            dependencyGraph.AddComponent(componentB, componentA.Id);
+            dependencyGraph.AddComponent(componentC);
+            dependencyGraph.AddComponent(componentA, componentC.Id);
+
+            var aRoots = dependencyGraph.GetExplicitReferencedDependencyIds(componentA.Id);
+            aRoots.Should().HaveCount(1);
+            aRoots.Should().Contain(componentC.Id);
+            ((IDependencyGraph)dependencyGraph).IsComponentExplicitlyReferenced(componentA.Id).Should().BeFalse();
+
+            var bRoots = dependencyGraph.GetExplicitReferencedDependencyIds(componentB.Id);
+            bRoots.Should().HaveCount(1);
+            bRoots.Should().Contain(componentC.Id);
+            ((IDependencyGraph)dependencyGraph).IsComponentExplicitlyReferenced(componentB.Id).Should().BeFalse();
+
+            var cRoots = dependencyGraph.GetExplicitReferencedDependencyIds(componentC.Id);
+            cRoots.Should().HaveCount(1);
+            cRoots.Should().Contain(componentC.Id);
+            ((IDependencyGraph)dependencyGraph).IsComponentExplicitlyReferenced(componentC.Id).Should().BeTrue();
+        }
+
+        [TestMethod]
+        public void GetExplicitReferencedDependencyIds_NullComponentId_ArgumentNullExceptionIsThrown()
+        {
+            Action action = () => dependencyGraph.GetExplicitReferencedDependencyIds(null);
+            action.Should().Throw();
+
+            action = () => dependencyGraph.GetExplicitReferencedDependencyIds(string.Empty);
+            action.Should().Throw();
+
+            action = () => dependencyGraph.GetExplicitReferencedDependencyIds("   ");
+            action.Should().Throw();
+        }
+
+        [TestMethod]
+        public void GetExplicitReferencedDependencyIds_ComponentIdIsNotRegisteredInGraph_ArgumentExceptionIsThrown()
+        {
+            Action action = () => dependencyGraph.GetExplicitReferencedDependencyIds("nonExistingId");
+            action.Should().Throw();
+        }
+
+        [TestMethod]
+        public void IsDevelopmentDependency_ReturnsAsExpected()
+        {
+            var componentA = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentA", IsDevelopmentDependency = true };
+            var componentB = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentB", IsDevelopmentDependency = false };
+            var componentC = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentC" };
+
+            dependencyGraph.AddComponent(componentA);
+            dependencyGraph.AddComponent(componentB, componentA.Id);
+            dependencyGraph.AddComponent(componentC);
+            dependencyGraph.AddComponent(componentA, componentC.Id);
+
+            dependencyGraph.IsDevelopmentDependency(componentA.Id).Should().Be(true);
+            dependencyGraph.IsDevelopmentDependency(componentB.Id).Should().Be(false);
+            dependencyGraph.IsDevelopmentDependency(componentC.Id).Should().Be(null);
+        }
+
+        [TestMethod]
+        public void IsDevelopmentDependency_ReturnsAsExpected_AfterMerge()
+        {
+            var componentA = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentA", IsDevelopmentDependency = true };
+            var componentB = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentB", IsDevelopmentDependency = false };
+            var componentC = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentC" };
+
+            dependencyGraph.AddComponent(componentA);
+            dependencyGraph.AddComponent(componentB, componentA.Id);
+            dependencyGraph.AddComponent(componentC);
+            dependencyGraph.AddComponent(componentA, componentC.Id);
+
+            var componentANewValue = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentA", IsDevelopmentDependency = false };
+            var componentBNewValue = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentB", IsDevelopmentDependency = true };
+            var componentCNewValue = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentC", IsDevelopmentDependency = true };
+            dependencyGraph.AddComponent(componentANewValue);
+            dependencyGraph.AddComponent(componentBNewValue);
+            dependencyGraph.AddComponent(componentCNewValue);
+
+            dependencyGraph.IsDevelopmentDependency(componentA.Id).Should().Be(false);
+            dependencyGraph.IsDevelopmentDependency(componentB.Id).Should().Be(false);
+            dependencyGraph.IsDevelopmentDependency(componentC.Id).Should().Be(true);
+
+            var componentANullValue = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentA" };
+            var componentBNullValue = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentB" };
+            var componentCNullValue = new DependencyGraph.DependencyGraph.ComponentRefNode { Id = "componentC" };
+            dependencyGraph.AddComponent(componentANullValue);
+            dependencyGraph.AddComponent(componentBNullValue);
+            dependencyGraph.AddComponent(componentCNullValue);
+
+            dependencyGraph.IsDevelopmentDependency(componentA.Id).Should().Be(false);
+            dependencyGraph.IsDevelopmentDependency(componentB.Id).Should().Be(false);
+            dependencyGraph.IsDevelopmentDependency(componentC.Id).Should().Be(true);
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Common.Tests/DockerServiceTests.cs b/test/Microsoft.ComponentDetection.Common.Tests/DockerServiceTests.cs
new file mode 100644
index 000000000..155fbcaa1
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Common.Tests/DockerServiceTests.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.ComponentDetection.TestsUtilities;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.ComponentDetection.Common.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class DockerServiceTests
+    {
+        private DockerService dockerService;
+
+        private const string TestImage = "governancecontainerregistry.azurecr.io/testcontainers/hello-world:latest";
+        
+        private const string TestImageWithBaseDetails = "governancecontainerregistry.azurecr.io/testcontainers/dockertags_test:testtag";
+        
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            dockerService = new DockerService();
+        }
+        
+        [TestMethod]
+        public async Task DockerService_CanPingDocker()
+        {
+            var canPingDocker = await dockerService.CanPingDockerAsync();
+            Assert.IsTrue(canPingDocker);
+        }
+
+        [SkipTestOnWindows]
+        public async Task DockerService_CanRunLinuxContainersAsync()
+        {
+            var isLinuxContainerModeEnabled = await dockerService.CanRunLinuxContainersAsync();
+            Assert.IsTrue(isLinuxContainerModeEnabled);
+        }
+        
+        [SkipTestOnWindows]
+        public async Task DockerService_CanPullImage()
+        {
+            Func action = async () => await dockerService.TryPullImageAsync(TestImage);
+            await action.Should().NotThrowAsync();
+        }
+        
+        [SkipTestOnWindows]
+        public async Task DockerService_CanInspectImage()
+        {
+            await dockerService.TryPullImageAsync(TestImage);
+            var details = await dockerService.InspectImageAsync(TestImage);
+            details.Should().NotBeNull();
+            details.Tags.Should().Contain("governancecontainerregistry.azurecr.io/testcontainers/hello-world:latest");
+        }
+
+        [SkipTestOnWindows]
+        public async Task DockerService_PopulatesBaseImageAndLayerDetails()
+        {
+            await dockerService.TryPullImageAsync(TestImageWithBaseDetails);
+            var details = await dockerService.InspectImageAsync(TestImageWithBaseDetails);
+
+            details.Should().NotBeNull();
+            details.Tags.Should().Contain("governancecontainerregistry.azurecr.io/testcontainers/dockertags_test:testtag");
+            var expectedImageId = "sha256:8a311790d0b3414e97ed7b31c6ddf1711f980f4fca83b6ecb6becfa8c1867bfe";
+            var expectedCreatedAt = DateTime.Parse("2021-07-28 19:25:20.3307716");
+
+            details.Should().NotBeNull();
+            details.Id.Should().BeGreaterThan(0);
+            details.ImageId.Should().BeEquivalentTo(expectedImageId);
+            details.CreatedAt.ToUniversalTime().Should().Be(expectedCreatedAt);
+            details.BaseImageDigest.Should().Be("sha256:5c8908bc326c0b7c4f0f8059bbde31a92826446a88e6d7c7f6024b4d33fec545");
+            details.BaseImageRef.Should().Be("ubuntu:precise-20151020");
+            details.Layers.Should().HaveCount(4);
+        }
+        
+        [SkipTestOnWindows]
+        public async Task DockerService_CanCreateAndRunImage()
+        {
+            var (stdout, stderr) = await dockerService.CreateAndRunContainerAsync(TestImage, new List());
+            stdout.Should().StartWith("\nHello from Docker!");
+            stderr.Should().BeEmpty();
+        }
+    }
+}
\ No newline at end of file
diff --git a/test/Microsoft.ComponentDetection.Common.Tests/FileEnumerationTests.cs b/test/Microsoft.ComponentDetection.Common.Tests/FileEnumerationTests.cs
new file mode 100644
index 000000000..06403ef1f
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Common.Tests/FileEnumerationTests.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.InteropServices;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Microsoft.ComponentDetection.Common.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class FileEnumerationTests
+    {
+        [TestMethod]
+        public void CanListAllFiles()
+        {
+            var testDirectory = Environment.GetEnvironmentVariable("COMPONENT_DETECTION_SYMLINK_TEST");
+            if (string.IsNullOrWhiteSpace(testDirectory))
+            {
+                Assert.Inconclusive("Test directory environment variable isn't set. Not testing");
+            }
+
+            var loggerMock = new Mock();
+
+            var pathUtility = new PathUtilityService();
+            var sfe = new SafeFileEnumerable(new DirectoryInfo(Path.Combine(testDirectory, "root")), new[] { "*" }, loggerMock.Object,
+                pathUtility, (name, directoryName) => false, true);
+
+            var foundFiles = new List();
+            foreach (var f in sfe)
+            {
+                foundFiles.Add(f.File.FullName);
+            }
+
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                Assert.AreEqual(48, foundFiles.Count);
+            }
+            else
+            {
+                Assert.AreEqual(24, foundFiles.Count);
+            }
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Common.Tests/FileWritingServiceTests.cs b/test/Microsoft.ComponentDetection.Common.Tests/FileWritingServiceTests.cs
new file mode 100644
index 000000000..2ee9dfc25
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Common.Tests/FileWritingServiceTests.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Globalization;
+using System.IO;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Common.Exceptions;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.ComponentDetection.Common.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class FileWritingServiceTests
+    {
+        private FileWritingService serviceUnderTest;
+        private string tempFolder;
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            serviceUnderTest = new FileWritingService();
+
+            // Get a temp file and repurpose it as a temp folder
+            var tempFile = Path.GetTempFileName();
+            File.Delete(tempFile);
+            Directory.CreateDirectory(tempFile);
+            tempFolder = tempFile;
+
+            serviceUnderTest.Init(tempFolder);
+        }
+
+        [TestCleanup]
+        public void TestCleanup()
+        {
+            Directory.Delete(tempFolder, true);
+        }
+
+        [TestMethod]
+        public void AppendToFile_AppendsToFiles()
+        {
+            var relativeDir = "someOtherFileName.txt";
+            var fileLocation = Path.Combine(tempFolder, relativeDir);
+            File.Create(fileLocation).Dispose();
+            serviceUnderTest.AppendToFile(relativeDir, "someSampleText");
+            var text = File.ReadAllText(Path.Combine(tempFolder, relativeDir));
+            text
+                .Should().Be("someSampleText");
+        }
+
+        [TestMethod]
+        public void WriteFile_CreatesAFile()
+        {
+            var relativeDir = "someFileName.txt";
+            serviceUnderTest.WriteFile(relativeDir, "sampleText");
+            var text = File.ReadAllText(Path.Combine(tempFolder, relativeDir));
+            text
+                .Should().Be("sampleText");
+        }
+
+        [TestMethod]
+        public void WriteFile_AppendToFile_WorkWithTemplatizedPaths()
+        {
+            var relativeDir = "somefile_{timestamp}.txt";
+            serviceUnderTest.WriteFile(relativeDir, "sampleText");
+            serviceUnderTest.AppendToFile(relativeDir, "sampleText2");
+            var files = Directory.GetFiles(tempFolder);
+            files
+                .Should().NotBeEmpty();
+            File.ReadAllText(files[0])
+                .Should().Contain($"sampleTextsampleText2");
+            VerifyTimestamp(files[0], "somefile_", ".txt");
+        }
+
+        [TestMethod]
+        public void ResolveFilePath_ResolvedTemplatizedPaths()
+        {
+            var relativeDir = "someOtherFile_{timestamp}.txt";
+            serviceUnderTest.WriteFile(relativeDir, string.Empty);
+            var fullPath = serviceUnderTest.ResolveFilePath(relativeDir);
+            VerifyTimestamp(fullPath, "someOtherFile_", ".txt");
+        }
+
+        [TestMethod]
+        public void InitLogger_FailsOnDirectoryThatDoesNotExist()
+        {
+            var relativeDir = Guid.NewGuid();
+            var actualServiceUnderTest = new FileWritingService();
+            Action action = () => actualServiceUnderTest.Init(Path.Combine(serviceUnderTest.BasePath, relativeDir.ToString()));
+
+            action.Should().Throw();
+        }
+
+        private void VerifyTimestamp(string fullPath, string prefix, string suffix)
+        {
+            var fileName = Path.GetFileName(fullPath);
+            fileName
+                .Should().StartWith(prefix)
+                .And.EndWith(suffix);
+            var timestamp = fileName.Substring(prefix.Length, FileWritingService.TimestampFormatString.Length);
+            var dateTime = DateTime.ParseExact(timestamp, FileWritingService.TimestampFormatString, CultureInfo.InvariantCulture);
+            dateTime.Should().BeCloseTo(DateTime.Now, TimeSpan.FromMilliseconds(10000));
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Common.Tests/LoggerTests.cs b/test/Microsoft.ComponentDetection.Common.Tests/LoggerTests.cs
new file mode 100644
index 000000000..e167a4bc2
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Common.Tests/LoggerTests.cs
@@ -0,0 +1,282 @@
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Microsoft.ComponentDetection.Common.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class LoggerTests
+    {
+        private Mock fileWritingServiceMock;
+        private Mock consoleWritingServiceMock;
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            consoleWritingServiceMock = new Mock();
+            fileWritingServiceMock = new Mock();
+        }
+
+        [TestCleanup]
+        public void TestCleanup()
+        {
+            consoleWritingServiceMock.VerifyAll();
+            fileWritingServiceMock.VerifyAll();
+        }
+
+        private Logger CreateLogger(VerbosityMode verbosityMode)
+        {
+            var serviceUnderTest = new Logger
+            {
+                ConsoleWriter = consoleWritingServiceMock.Object,
+                FileWritingService = fileWritingServiceMock.Object,
+            };
+
+            serviceUnderTest.Init(verbosityMode);
+
+            // We're not explicitly testing init behavior here, so we reset mock expecations. Another test should verify these.
+            consoleWritingServiceMock.Invocations.Clear();
+            fileWritingServiceMock.Invocations.Clear();
+            return serviceUnderTest;
+        }
+
+        [TestMethod]
+        public void LogCreateLoggingGroup_HandlesFailedInit()
+        {
+            var logger = new Logger
+            {
+                ConsoleWriter = consoleWritingServiceMock.Object,
+                FileWritingService = null,
+            };
+
+            // This should throw an exception while setting up the file writing service, but handle it
+            logger.Init(VerbosityMode.Normal);
+
+            consoleWritingServiceMock.Invocations.Clear();
+            consoleWritingServiceMock.Setup(x => x.Write(Environment.NewLine));
+
+            // This should not fail, despite not initializing the file writing service
+            logger.LogCreateLoggingGroup();
+
+            // As a result of handling the file writing service failure, the verbosity should now be Verbose
+            var verboseMessage = "verboseMessage";
+            var expectedMessage = $"[VERBOSE] {verboseMessage} {Environment.NewLine}";
+            consoleWritingServiceMock.Setup(x => x.Write(expectedMessage));
+
+            logger.LogVerbose(verboseMessage);
+        }
+
+        [TestMethod]
+        public void LogCreateLoggingGroup_WritesOnNormal()
+        {
+            var logger = CreateLogger(VerbosityMode.Normal);
+            consoleWritingServiceMock.Setup(x => x.Write(Environment.NewLine));
+            fileWritingServiceMock.Setup(x => x.AppendToFile(Logger.LogRelativePath, Environment.NewLine));
+            logger.LogCreateLoggingGroup();
+        }
+
+        [TestMethod]
+        public void LogCreateLoggingGroup_SkipsConsoleOnQuiet()
+        {
+            var logger = CreateLogger(VerbosityMode.Quiet);
+            fileWritingServiceMock.Setup(x => x.AppendToFile(Logger.LogRelativePath, Environment.NewLine));
+            logger.LogCreateLoggingGroup();
+        }
+
+        [TestMethod]
+        public void LogWarning_WritesOnNormal()
+        {
+            var logger = CreateLogger(VerbosityMode.Normal);
+            var warningMessage = "warningMessage";
+            var expectedMessage = $"[WARN] {warningMessage} {Environment.NewLine}";
+            consoleWritingServiceMock.Setup(x => x.Write(expectedMessage));
+            fileWritingServiceMock.Setup(x => x.AppendToFile(Logger.LogRelativePath, expectedMessage));
+            logger.LogWarning(warningMessage);
+        }
+
+        [TestMethod]
+        public void LogWarning_SkipsConsoleOnQuiet()
+        {
+            var logger = CreateLogger(VerbosityMode.Quiet);
+            var warningMessage = "warningMessage";
+            var expectedMessage = $"[WARN] {warningMessage} {Environment.NewLine}";
+            fileWritingServiceMock.Setup(x => x.AppendToFile(Logger.LogRelativePath, expectedMessage));
+            logger.LogWarning(warningMessage);
+        }
+
+        [TestMethod]
+        public void LogInfo_WritesOnNormal()
+        {
+            var logger = CreateLogger(VerbosityMode.Normal);
+            var infoMessage = "informationalMessage";
+            var expectedMessage = $"[INFO] {infoMessage} {Environment.NewLine}";
+            consoleWritingServiceMock.Setup(x => x.Write(expectedMessage));
+            fileWritingServiceMock.Setup(x => x.AppendToFile(Logger.LogRelativePath, expectedMessage));
+            logger.LogInfo(infoMessage);
+        }
+
+        [TestMethod]
+        public void LogInfo_SkipsConsoleOnQuiet()
+        {
+            var logger = CreateLogger(VerbosityMode.Quiet);
+            var infoMessage = "informationalMessage";
+            var expectedMessage = $"[INFO] {infoMessage} {Environment.NewLine}";
+            fileWritingServiceMock.Setup(x => x.AppendToFile(Logger.LogRelativePath, expectedMessage));
+            logger.LogInfo(infoMessage);
+        }
+
+        [TestMethod]
+        public void LogVerbose_WritesOnVerbose()
+        {
+            var logger = CreateLogger(VerbosityMode.Verbose);
+            var verboseMessage = "verboseMessage";
+            var expectedMessage = $"[VERBOSE] {verboseMessage} {Environment.NewLine}";
+            consoleWritingServiceMock.Setup(x => x.Write(expectedMessage));
+            fileWritingServiceMock.Setup(x => x.AppendToFile(Logger.LogRelativePath, expectedMessage));
+            logger.LogVerbose(verboseMessage);
+        }
+
+        [TestMethod]
+        public void LogVerbose_SkipsConsoleOnNormal()
+        {
+            var logger = CreateLogger(VerbosityMode.Normal);
+            var verboseMessage = "verboseMessage";
+            var expectedMessage = $"[VERBOSE] {verboseMessage} {Environment.NewLine}";
+            fileWritingServiceMock.Setup(x => x.AppendToFile(Logger.LogRelativePath, expectedMessage));
+            logger.LogVerbose(verboseMessage);
+        }
+
+        [TestMethod]
+        public void LogError_WritesOnQuiet()
+        {
+            var logger = CreateLogger(VerbosityMode.Quiet);
+            var errorMessage = "errorMessage";
+            var expectedMessage = $"[ERROR] {errorMessage} {Environment.NewLine}";
+            consoleWritingServiceMock.Setup(x => x.Write(expectedMessage));
+            fileWritingServiceMock.Setup(x => x.AppendToFile(Logger.LogRelativePath, expectedMessage));
+            logger.LogError(errorMessage);
+        }
+
+        [TestMethod]
+        public void LogFailedReadingFile_WritesOnVerbose()
+        {
+            var logger = CreateLogger(VerbosityMode.Verbose);
+            var filePath = "some/bad/file/path";
+            var error = new UnauthorizedAccessException("Some unauthorized access error");
+
+            var consoleSequence = new MockSequence();
+            consoleWritingServiceMock.InSequence(consoleSequence).Setup(x => x.Write(Environment.NewLine));
+            consoleWritingServiceMock.InSequence(consoleSequence).Setup(x => x.Write(
+                Match.Create(message => message.StartsWith("[VERBOSE]") && message.Contains(filePath))));
+            consoleWritingServiceMock.InSequence(consoleSequence).Setup(x => x.Write(
+                Match.Create(message => message.StartsWith("[INFO]") && message.Contains(error.Message))));
+
+            var fileSequence = new MockSequence();
+            fileWritingServiceMock.InSequence(fileSequence).Setup(x => x.AppendToFile(
+                Logger.LogRelativePath,
+                Match.Create(message => message.StartsWith("[VERBOSE]") && message.Contains(filePath))));
+            fileWritingServiceMock.InSequence(fileSequence).Setup(x => x.AppendToFile(
+                Logger.LogRelativePath,
+                Match.Create(message => message.StartsWith("[INFO]") && message.Contains(error.Message))));
+
+            logger.LogFailedReadingFile(filePath, error);
+        }
+
+        [TestMethod]
+        public void LogFailedReadingFile_SkipsConsoleOnQuiet()
+        {
+            var logger = CreateLogger(VerbosityMode.Quiet);
+            var filePath = "some/bad/file/path";
+            var error = new UnauthorizedAccessException("Some unauthorized access error");
+
+            var fileSequence = new MockSequence();
+            fileWritingServiceMock.InSequence(fileSequence).Setup(x => x.AppendToFile(
+                Logger.LogRelativePath,
+                Match.Create(message => message.StartsWith("[VERBOSE]") && message.Contains(filePath))));
+            fileWritingServiceMock.InSequence(fileSequence).Setup(x => x.AppendToFile(
+                Logger.LogRelativePath,
+                Match.Create(message => message.StartsWith("[INFO]") && message.Contains(error.Message))));
+
+            logger.LogFailedReadingFile(filePath, error);
+        }
+
+        [TestMethod]
+        public void LogException_WritesOnQuietIfError()
+        {
+            var logger = CreateLogger(VerbosityMode.Quiet);
+            var error = new UnauthorizedAccessException("Some unauthorized access error");
+
+            consoleWritingServiceMock.Setup(x => x.Write(
+                Match.Create(message => message.StartsWith("[ERROR]") && message.Contains(error.Message))));
+
+            fileWritingServiceMock.Setup(x => x.AppendToFile(
+                Logger.LogRelativePath,
+                Match.Create(message => message.StartsWith("[ERROR]") && message.Contains(error.ToString()))));
+
+            logger.LogException(error, true);
+        }
+
+        [TestMethod]
+        public void LogException_DoesNotLogFullExceptionByDefault()
+        {
+            var logger = CreateLogger(VerbosityMode.Quiet);
+            var error = new UnauthorizedAccessException("Some unauthorized access error");
+
+            consoleWritingServiceMock.Setup(x => x.Write(
+                Match.Create(message => message.StartsWith("[ERROR]") && message.Contains(error.Message) && !message.Contains(error.ToString()))));
+
+            fileWritingServiceMock.Setup(x => x.AppendToFile(
+                Logger.LogRelativePath,
+                Match.Create(message => message.StartsWith("[ERROR]") && message.Contains(error.ToString()))));
+
+            logger.LogException(error, true);
+        }
+
+        [TestMethod]
+        public void LogException_LogsFullExceptionOnRequest()
+        {
+            var logger = CreateLogger(VerbosityMode.Quiet);
+            var error = new UnauthorizedAccessException("Some unauthorized access error");
+
+            consoleWritingServiceMock.Setup(x => x.Write(
+                Match.Create(message => message.StartsWith("[ERROR]") && message.Contains(error.ToString()))));
+
+            fileWritingServiceMock.Setup(x => x.AppendToFile(
+                Logger.LogRelativePath,
+                Match.Create(message => message.StartsWith("[ERROR]") && message.Contains(error.ToString()))));
+
+            logger.LogException(error, true, printException: true);
+        }
+
+        [TestMethod]
+        public void LogException_SkipsConsoleIfNotErrorAndNormalLogging()
+        {
+            var logger = CreateLogger(VerbosityMode.Normal);
+            var error = new UnauthorizedAccessException("Some unauthorized access error");
+
+            fileWritingServiceMock.Setup(x => x.AppendToFile(
+                Logger.LogRelativePath,
+                Match.Create(message => message.StartsWith("[INFO]") && message.Contains(error.ToString()))));
+
+            logger.LogException(error, false);
+        }
+
+        [TestMethod]
+        public void LogException_WritesEverythingIfNotErrorAndVerboseLogging()
+        {
+            var logger = CreateLogger(VerbosityMode.Verbose);
+            var error = new UnauthorizedAccessException("Some unauthorized access error");
+
+            consoleWritingServiceMock.Setup(x => x.Write(
+                Match.Create(message => message.StartsWith("[INFO]") && message.Contains(error.Message))));
+
+            fileWritingServiceMock.Setup(x => x.AppendToFile(
+                Logger.LogRelativePath,
+                Match.Create(message => message.StartsWith("[INFO]") && message.Contains(error.Message))));
+
+            logger.LogException(error, false);
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Common.Tests/Microsoft.ComponentDetection.Common.Tests.csproj b/test/Microsoft.ComponentDetection.Common.Tests/Microsoft.ComponentDetection.Common.Tests.csproj
new file mode 100644
index 000000000..f0417b056
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Common.Tests/Microsoft.ComponentDetection.Common.Tests.csproj
@@ -0,0 +1,11 @@
+
+  
+    AnyCPU
+  
+  
+    AnyCPU
+  
+  
+    
+  
+
diff --git a/test/Microsoft.ComponentDetection.Common.Tests/SafeFileEnumerableTests.cs b/test/Microsoft.ComponentDetection.Common.Tests/SafeFileEnumerableTests.cs
new file mode 100644
index 000000000..35b4fff30
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Common.Tests/SafeFileEnumerableTests.cs
@@ -0,0 +1,161 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Microsoft.ComponentDetection.Common.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class SafeFileEnumerableTests
+    {
+        private Mock loggerMock;
+
+        private Mock pathUtilityServiceMock;
+
+        private string temporaryDirectory;
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            loggerMock = new Mock();
+            pathUtilityServiceMock = new Mock();
+            temporaryDirectory = GetTemporaryDirectory();
+        }
+
+        [TestCleanup]
+        public void TestCleanup()
+        {
+            CleanupTemporaryDirectory(temporaryDirectory);
+        }
+
+        [TestMethod]
+        public void GetEnumerator_WorksOverExpectedFiles()
+        {
+            var subDir = Directory.CreateDirectory(Path.Combine(temporaryDirectory, "SubDir"));
+            string name = string.Format("{0}.txt", Guid.NewGuid());
+
+            var file0 = Path.Combine(temporaryDirectory, name);
+            var subFile0 = Path.Combine(temporaryDirectory, "SubDir", name);
+
+            File.Create(file0).Close();
+            File.Create(subFile0).Close();
+
+            IEnumerable searchPatterns = new List { name };
+
+            pathUtilityServiceMock.Setup(x => x.ResolvePhysicalPath(It.IsAny())).Returns((s) => s);
+            pathUtilityServiceMock.Setup(x => x.MatchesPattern(name, name)).Returns(true);
+
+            var enumerable = new SafeFileEnumerable(new DirectoryInfo(temporaryDirectory), searchPatterns, loggerMock.Object, pathUtilityServiceMock.Object, (directoryName, span) => false, true);
+
+            int filesFound = 0;
+            foreach (var file in enumerable)
+            {
+                file.File.FullName.Should().BeOneOf(file0, subFile0);
+                filesFound++;
+            }
+
+            filesFound.Should().Be(2);
+        }
+
+        [TestMethod]
+        public void GetEnumerator_IgnoresSubDirectories()
+        {
+            var subDir = Directory.CreateDirectory(Path.Combine(temporaryDirectory, "SubDir"));
+            string name = string.Format("{0}.txt", Guid.NewGuid());
+
+            var file0 = Path.Combine(temporaryDirectory, name);
+
+            File.Create(file0).Close();
+            File.Create(Path.Combine(temporaryDirectory, "SubDir", name)).Close();
+
+            IEnumerable searchPatterns = new List { name };
+
+            pathUtilityServiceMock.Setup(x => x.MatchesPattern(name, name)).Returns(true);
+
+            var enumerable = new SafeFileEnumerable(new DirectoryInfo(temporaryDirectory), searchPatterns, loggerMock.Object, pathUtilityServiceMock.Object, (directoryName, span) => false, false);
+
+            int filesFound = 0;
+            foreach (var file in enumerable)
+            {
+                file.File.FullName.Should().BeOneOf(file0);
+                filesFound++;
+            }
+
+            filesFound.Should().Be(1);
+        }
+
+        [TestMethod]
+        public void GetEnumerator_CallsSymlinkCode()
+        {
+            Assert.Inconclusive("Need actual symlinks to accurately test this");
+            var subDir = Directory.CreateDirectory(Path.Combine(temporaryDirectory, "SubDir"));
+            string name = string.Format("{0}.txt", Guid.NewGuid());
+            File.Create(Path.Combine(temporaryDirectory, name)).Close();
+            File.Create(Path.Combine(temporaryDirectory, "SubDir", name)).Close();
+
+            IEnumerable searchPatterns = new List { name };
+
+            var enumerable = new SafeFileEnumerable(new DirectoryInfo(temporaryDirectory), searchPatterns, loggerMock.Object, pathUtilityServiceMock.Object, (directoryName, span) => false, true);
+
+            foreach (var file in enumerable)
+            {
+            }
+
+            pathUtilityServiceMock.Verify(x => x.ResolvePhysicalPath(temporaryDirectory), Times.AtLeastOnce);
+        }
+
+        [TestMethod]
+        public void GetEnumerator_DuplicatePathIgnored()
+        {
+            Assert.Inconclusive("Need actual symlinks to accurately test this");
+            Environment.SetEnvironmentVariable("GovernanceSymlinkAwareMode", bool.TrueString, EnvironmentVariableTarget.Process);
+
+            var subDir = Directory.CreateDirectory(Path.Combine(temporaryDirectory, "SubDir"));
+            var fakeSymlink = Directory.CreateDirectory(Path.Combine(temporaryDirectory, "FakeSymlink"));
+            string name = string.Format("{0}.txt", Guid.NewGuid());
+            string canary = string.Format("{0}.txt", Guid.NewGuid());
+            File.Create(Path.Combine(temporaryDirectory, name)).Close();
+            File.Create(Path.Combine(temporaryDirectory, "SubDir", name)).Close();
+            File.Create(Path.Combine(temporaryDirectory, "FakeSymlink", canary)).Close();
+
+            pathUtilityServiceMock.Setup(x => x.ResolvePhysicalPath(temporaryDirectory)).Returns(temporaryDirectory);
+            pathUtilityServiceMock.Setup(x => x.ResolvePhysicalPath(subDir.FullName)).Returns(subDir.FullName);
+            pathUtilityServiceMock.Setup(x => x.ResolvePhysicalPath(fakeSymlink.FullName)).Returns(subDir.FullName);
+
+            IEnumerable searchPatterns = new List { name };
+
+            var enumerable = new SafeFileEnumerable(new DirectoryInfo(temporaryDirectory), searchPatterns, loggerMock.Object, pathUtilityServiceMock.Object, (directoryName, span) => false, true);
+
+            foreach (var file in enumerable)
+            {
+                file.File.FullName.Should().NotBe(Path.Combine(temporaryDirectory, "FakeSymlink", canary));
+            }
+
+            pathUtilityServiceMock.Verify(x => x.ResolvePhysicalPath(temporaryDirectory), Times.AtLeastOnce);
+        }
+
+        private string GetTemporaryDirectory()
+        {
+            string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+            Directory.CreateDirectory(tempDirectory);
+            return tempDirectory;
+        }
+
+        private void CleanupTemporaryDirectory(string directory)
+        {
+            try
+            {
+                Directory.Delete(directory, true);
+            }
+            catch
+            {
+                // Swallow
+            }
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Common.Tests/TabularStringFormatTests.cs b/test/Microsoft.ComponentDetection.Common.Tests/TabularStringFormatTests.cs
new file mode 100644
index 000000000..2b3b18b6e
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Common.Tests/TabularStringFormatTests.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Linq;
+using FluentAssertions;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.ComponentDetection.Common.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class TabularStringFormatTests
+    {
+        private Column[] columns;
+        private TabularStringFormat tsf;
+        private object[][] rows;
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            columns = new Column[]
+            {
+                new Column { Header = "ColumnA", Width = 50, Format = null },
+                new Column { Header = "ColumnB", Width = 60, Format = "prefix{0}suffix" },
+                new Column { Header = "ColumnC", Width = 30, Format = null },
+            };
+
+            rows = new[]
+            {
+                // One row
+                new[] { "a", "b", "c" },
+            };
+
+            tsf = new TabularStringFormat(columns);
+        }
+
+        [TestMethod]
+        public void GenerateString_AllRowsObeyHeaderLength()
+        {
+            var generatedString = tsf.GenerateString(rows);
+
+            // Column width + border characters, one per column + one to 'close' the table.
+            var lineLength = columns.Sum(x => x.Width) + columns.Length + 1;
+            var splitStrings = generatedString.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
+            foreach (var line in splitStrings)
+            {
+                line.Should().HaveLength(lineLength);
+            }
+        }
+
+        [TestMethod]
+        public void GenerateString_ColumnHeadersAreWritten()
+        {
+            var generatedString = tsf.GenerateString(rows);
+
+            var splitStrings = generatedString.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
+
+            // Second row has the headers
+            var headerCells = splitStrings[1].Split(new[] { TabularStringFormat.DefaultVerticalLineChar }, StringSplitOptions.RemoveEmptyEntries);
+            for (int i = 0; i < columns.Length; i++)
+            {
+                headerCells[i]
+                    .Should().Contain(columns[i].Header);
+            }
+        }
+
+        [TestMethod]
+        public void GenerateString_RowContentsAreWritten()
+        {
+            var generatedString = tsf.GenerateString(rows);
+            var splitStrings = generatedString.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
+
+            // Fourth row should have some info
+            var rowCells = splitStrings[3].Split(new[] { TabularStringFormat.DefaultVerticalLineChar }, StringSplitOptions.RemoveEmptyEntries);
+            for (int i = 0; i < columns.Length; i++)
+            {
+                rowCells[i]
+                    .Should().Contain(rows[0][i].ToString());
+            }
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Contracts.Tests/DetectedComponentTests.cs b/test/Microsoft.ComponentDetection.Contracts.Tests/DetectedComponentTests.cs
new file mode 100644
index 000000000..f1bec9631
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Contracts.Tests/DetectedComponentTests.cs
@@ -0,0 +1,29 @@
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.ComponentDetection.Contracts.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class DetectedComponentTests
+    {
+        [TestMethod]
+        public void AddComponentFilePath_AddsPathsCorrectly()
+        {
+            string componentName = "express";
+            string componentVersion = "1.0.0";
+            string filePathToAdd = @"C:\some\fake\file\path.txt";
+
+            DetectedComponent component = new DetectedComponent(new NpmComponent(componentName, componentVersion));
+
+            Assert.IsNotNull(component.FilePaths);
+            Assert.AreEqual(0, component.FilePaths.Count);
+
+            component.AddComponentFilePath(filePathToAdd);
+
+            Assert.AreEqual(1, component.FilePaths.Count);
+            Assert.IsTrue(component.FilePaths.Contains(filePathToAdd));
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Contracts.Tests/Microsoft.ComponentDetection.Contracts.Tests.csproj b/test/Microsoft.ComponentDetection.Contracts.Tests/Microsoft.ComponentDetection.Contracts.Tests.csproj
new file mode 100644
index 000000000..454bdac5b
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Contracts.Tests/Microsoft.ComponentDetection.Contracts.Tests.csproj
@@ -0,0 +1,20 @@
+
+
+    
+      AnyCPU
+    
+
+    
+      AnyCPU
+    
+
+    
+        
+    
+
+    
+        
+        
+    
+
+
diff --git a/test/Microsoft.ComponentDetection.Contracts.Tests/PurlGenerationTests.cs b/test/Microsoft.ComponentDetection.Contracts.Tests/PurlGenerationTests.cs
new file mode 100644
index 000000000..2ee6383e5
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Contracts.Tests/PurlGenerationTests.cs
@@ -0,0 +1,113 @@
+using FluentAssertions;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.ComponentDetection.Contracts.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class PurlGenerationTests
+    {
+        [TestMethod]
+        public void NpmPackageNameShouldBeLowerCase()
+        {
+            // According to the spec package name should not have uppercase letters
+            // https://github.com/package-url/purl-spec/blame/180c46d266c45aa2bd81a2038af3f78e87bb4a25/README.rst#L656
+            var npmComponent = new NpmComponent("TEST", "1.2.3");
+            npmComponent.PackageUrl.Name.Should().Be("test");
+        }
+
+        [TestMethod]
+        public void GoPackageShouldPreferHashOverVersion()
+        {
+            // Commit should be used in place of version when available
+            // https://github.com/package-url/purl-spec/blame/180c46d266c45aa2bd81a2038af3f78e87bb4a25/README.rst#L610
+            var goComponent = new GoComponent("test", "1.2.3", "deadbeef");
+            goComponent.PackageUrl.Version.Should().Be("deadbeef");
+        }
+
+        [TestMethod]
+        public void PipPackageShouldBeModified()
+        {
+            // Package name should be lowercased and replace '_' with '-'
+            // https://github.com/package-url/purl-spec/blame/180c46d266c45aa2bd81a2038af3f78e87bb4a25/README.rst#L680
+            var pipComponent = new PipComponent("CHANGE_ME", "1.2.3");
+            pipComponent.PackageUrl.Name.Should().Be("change-me");
+        }
+
+        [TestMethod]
+        public void DebianAndUbuntuAreDebType()
+        {
+            // Ubuntu and debian are "deb" component types
+            // https://github.com/package-url/purl-spec/blame/180c46d266c45aa2bd81a2038af3f78e87bb4a25/README.rst#L537
+            var ubuntuComponent = new LinuxComponent("Ubuntu", "18.04", "bash", "1");
+            var debianComponent = new LinuxComponent("Debian", "buster", "bash", "1");
+
+            ubuntuComponent.PackageUrl.Type.Should().Be("deb");
+            debianComponent.PackageUrl.Type.Should().Be("deb");
+        }
+
+        [TestMethod]
+        public void CentOsFedoraAndRHELAreRpmType()
+        {
+            // CentOS, Fedora and RHEL use "rpm" component types
+            // https://github.com/package-url/purl-spec/blame/180c46d266c45aa2bd81a2038af3f78e87bb4a25/README.rst#L693
+            var centosComponent = new LinuxComponent("CentOS", "18.04", "bash", "1");
+            var fedoraComponent = new LinuxComponent("Fedora", "18.04", "bash", "1");
+            var rhelComponent = new LinuxComponent("Red Hat Enterprise Linux", "18.04", "bash", "1");
+
+            centosComponent.PackageUrl.Type.Should().Be("rpm");
+            fedoraComponent.PackageUrl.Type.Should().Be("rpm");
+            rhelComponent.PackageUrl.Type.Should().Be("rpm");
+        }
+
+        [TestMethod]
+        public void AlpineAndUnknownDoNotHavePurls()
+        {
+            // Alpine is not yet defined
+            // https://github.com/package-url/purl-spec/blame/180c46d266c45aa2bd81a2038af3f78e87bb4a25/README.rst#L711
+            var alpineComponent = new LinuxComponent("Alpine", "3.13", "bash", "1");
+            var unknownLinuxComponent = new LinuxComponent("Linux", "0", "bash", "1'");
+
+            alpineComponent.PackageUrl.Should().BeNull();
+            unknownLinuxComponent.PackageUrl.Should().BeNull();
+        }
+
+        [TestMethod]
+        public void DistroNamesAreLowerCased()
+        {
+            // Distros must be lower cased for both deb and rpm
+            // https://github.com/package-url/purl-spec/blame/180c46d266c45aa2bd81a2038af3f78e87bb4a25/README.rst#L537
+            // https://github.com/package-url/purl-spec/blame/180c46d266c45aa2bd81a2038af3f78e87bb4a25/README.rst#L694
+            var ubuntuComponent = new LinuxComponent("UbUnTu", "18.04", "bash", "1");
+            var fedoraComponent = new LinuxComponent("FeDoRa", "22", "bash", "1");
+
+            ubuntuComponent.PackageUrl.Namespace.Should().Be("ubuntu");
+            fedoraComponent.PackageUrl.Namespace.Should().Be("fedora");
+        }
+
+        [TestMethod]
+        public void CocoaPodNameShouldSupportPurl()
+        {
+            // https://github.com/package-url/purl-spec/blob/b8ddd39a6d533b8895f3b741f2e62e2695d82aa4/PURL-TYPES.rst#cocoapods
+            var packageOne = new PodComponent("AFNetworking", "4.0.1");
+            var packageTwo = new PodComponent("MapsIndoors", "3.24.0");
+            var packageThree = new PodComponent("googleUtilities", "7.5.2");
+
+            packageOne.PackageUrl.Type.Should().Be("cocoapods");
+            packageOne.PackageUrl.ToString().Should().Be("pkg:cocoapods/afnetworking@4.0.1");
+            packageTwo.PackageUrl.ToString().Should().Be("pkg:cocoapods/mapsindoors@3.24.0");
+            packageThree.PackageUrl.ToString().Should().Be("pkg:cocoapods/googleutilities@7.5.2");
+        }
+
+        [TestMethod]
+        public void CocoaPodNameShouldPurlWithCustomQualifier()
+        {
+            // https://github.com/package-url/purl-spec/blob/b8ddd39a6d533b8895f3b741f2e62e2695d82aa4/PURL-TYPES.rst#cocoapods
+            var packageOne = new PodComponent("AFNetworking", "4.0.1", "https://custom_repo.example.com/path/to/repo/specs.git");
+
+            packageOne.PackageUrl.ToString().Should().Be("pkg:cocoapods/afnetworking@4.0.1?repository_url=https://custom_repo.example.com/path/to/repo/specs.git");
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Contracts.Tests/ScanResultSerializationTests.cs b/test/Microsoft.ComponentDetection.Contracts.Tests/ScanResultSerializationTests.cs
new file mode 100644
index 000000000..31b830f2e
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Contracts.Tests/ScanResultSerializationTests.cs
@@ -0,0 +1,115 @@
+using System.Linq;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Contracts.BcdeModels;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.ComponentDetection.Contracts.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class ScanResultSerializationTests
+    {
+        private ScanResult scanResultUnderTest;
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            scanResultUnderTest = new ScanResult
+            {
+                ResultCode = ProcessingResultCode.PartialSuccess,
+                ComponentsFound = new[]
+                {
+                    new ScannedComponent
+                    {
+                        Component = new NpmComponent("SampleNpmComponent", "1.2.3"),
+                        DetectorId = "NpmDetectorId",
+                        IsDevelopmentDependency = true,
+                        LocationsFoundAt = new[]
+                        {
+                            "some/location",
+                        },
+                        TopLevelReferrers = new[]
+                        {
+                            new NpmComponent("RootNpmComponent", "4.5.6"),
+                        },
+                    },
+                },
+                DetectorsInScan = new[]
+                {
+                    new Detector
+                    {
+                        DetectorId = "NpmDetectorId",
+                        IsExperimental = true,
+                        SupportedComponentTypes = new[]
+                        {
+                            ComponentType.Npm,
+                        },
+                        Version = 2,
+                    },
+                },
+            };
+        }
+
+        [TestMethod]
+        public void ScanResultSerialization_HappyPath()
+        {
+            var serializedResult = JsonConvert.SerializeObject(scanResultUnderTest);
+            var actual = JsonConvert.DeserializeObject(serializedResult);
+
+            actual.ResultCode.Should().Be(ProcessingResultCode.PartialSuccess);
+            actual.ComponentsFound.Count().Should().Be(1);
+            var actualDetectedComponent = actual.ComponentsFound.First();
+            actualDetectedComponent.DetectorId.Should().Be("NpmDetectorId");
+            actualDetectedComponent.IsDevelopmentDependency.Should().Be(true);
+            actualDetectedComponent.LocationsFoundAt.Contains("some/location").Should().Be(true);
+
+            var npmComponent = actualDetectedComponent.Component as NpmComponent;
+            npmComponent.Should().NotBeNull();
+            npmComponent.Name.Should().Be("SampleNpmComponent");
+            npmComponent.Version.Should().Be("1.2.3");
+
+            var rootNpmComponent = actualDetectedComponent.TopLevelReferrers.First() as NpmComponent;
+            rootNpmComponent.Should().NotBeNull();
+            rootNpmComponent.Name.Should().Be("RootNpmComponent");
+            rootNpmComponent.Version.Should().Be("4.5.6");
+
+            var actualDetector = actual.DetectorsInScan.First();
+            actualDetector.DetectorId.Should().Be("NpmDetectorId");
+            actualDetector.IsExperimental.Should().Be(true);
+            actualDetector.Version.Should().Be(2);
+            actualDetector.SupportedComponentTypes.Single().Should().Be(ComponentType.Npm);
+        }
+
+        [TestMethod]
+        public void ScanResultSerialization_ExpectedJsonFormat()
+        {
+            var serializedResult = JsonConvert.SerializeObject(scanResultUnderTest);
+            JObject json = JObject.Parse(serializedResult);
+
+            json.Value("resultCode").Should().Be("PartialSuccess");
+            var foundComponent = json["componentsFound"].First();
+
+            foundComponent.Value("detectorId").Should().Be("NpmDetectorId");
+            foundComponent.Value("isDevelopmentDependency").Should().Be(true);
+            foundComponent["locationsFoundAt"].First().Value().Should().Be("some/location");
+            foundComponent["component"].Value("type").Should().Be("Npm");
+            foundComponent["component"].Value("name").Should().Be("SampleNpmComponent");
+            foundComponent["component"].Value("version").Should().Be("1.2.3");
+
+            var rootComponent = foundComponent["topLevelReferrers"].First();
+            rootComponent.Value("type").Should().Be("Npm");
+            rootComponent.Value("name").Should().Be("RootNpmComponent");
+            rootComponent.Value("version").Should().Be("4.5.6");
+
+            var detector = json["detectorsInScan"].First();
+            detector.Value("detectorId").Should().Be("NpmDetectorId");
+            detector.Value("version").Should().Be(2);
+            detector.Value("isExperimental").Should().Be(true);
+            detector["supportedComponentTypes"].First().Value().Should().Be("Npm");
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Contracts.Tests/TypedComponentSerializationTests.cs b/test/Microsoft.ComponentDetection.Contracts.Tests/TypedComponentSerializationTests.cs
new file mode 100644
index 000000000..d9be70d59
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Contracts.Tests/TypedComponentSerializationTests.cs
@@ -0,0 +1,180 @@
+using System;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Newtonsoft.Json;
+
+namespace Microsoft.ComponentDetection.Contracts.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class TypedComponentSerializationTests
+    {
+        [TestMethod]
+        public void TypedComponent_Serialization_Other()
+        {
+            TypedComponent.TypedComponent tc = new OtherComponent("SomeOtherComponent", "1.2.3", new Uri("https://sampleurl.com"), "SampleHash");
+            var result = JsonConvert.SerializeObject(tc);
+            var deserializedTC = JsonConvert.DeserializeObject(result);
+            deserializedTC.Should().BeOfType(typeof(OtherComponent));
+            var otherComponent = (OtherComponent)deserializedTC;
+            otherComponent.Name.Should().Be("SomeOtherComponent");
+            otherComponent.Version.Should().Be("1.2.3");
+            otherComponent.DownloadUrl.Should().Be(new Uri("https://sampleurl.com"));
+            otherComponent.Hash.Should().Be("SampleHash");
+        }
+
+        [TestMethod]
+        public void TypedComponent_Serialization_NuGet()
+        {
+            TypedComponent.TypedComponent tc = new NuGetComponent("SomeNuGetComponent", "1.2.3");
+            var result = JsonConvert.SerializeObject(tc);
+            var deserializedTC = JsonConvert.DeserializeObject(result);
+            deserializedTC.Should().BeOfType(typeof(NuGetComponent));
+            var nugetComponent = (NuGetComponent)deserializedTC;
+            nugetComponent.Name.Should().Be("SomeNuGetComponent");
+            nugetComponent.Version.Should().Be("1.2.3");
+        }
+
+        [TestMethod]
+        public void TypedComponent_Serialization_Npm()
+        {
+            TypedComponent.TypedComponent tc = new NpmComponent("SomeNpmComponent", "1.2.3");
+            var result = JsonConvert.SerializeObject(tc);
+            var deserializedTC = JsonConvert.DeserializeObject(result);
+            deserializedTC.Should().BeOfType(typeof(NpmComponent));
+            var npmComponent = (NpmComponent)deserializedTC;
+            npmComponent.Name.Should().Be("SomeNpmComponent");
+            npmComponent.Version.Should().Be("1.2.3");
+        }
+
+        [TestMethod]
+        public void TypedComponent_Serialization_Npm_WithHash()
+        {
+            TypedComponent.TypedComponent tc = new NpmComponent("SomeNpmComponent", "1.2.3", "sha1-placeholder");
+            var result = JsonConvert.SerializeObject(tc);
+            var deserializedTC = JsonConvert.DeserializeObject(result);
+            deserializedTC.Should().BeOfType(typeof(NpmComponent));
+            var npmComponent = (NpmComponent)deserializedTC;
+            npmComponent.Name.Should().Be("SomeNpmComponent");
+            npmComponent.Version.Should().Be("1.2.3");
+            npmComponent.Hash.Should().Be("sha1-placeholder");
+        }
+
+        [TestMethod]
+        public void TypedComponent_Serialization_Maven()
+        {
+            TypedComponent.TypedComponent tc = new MavenComponent("SomeGroupId", "SomeArtifactId", "1.2.3");
+            var result = JsonConvert.SerializeObject(tc);
+            var deserializedTC = JsonConvert.DeserializeObject(result);
+            deserializedTC.Should().BeOfType(typeof(MavenComponent));
+            var mavenComponent = (MavenComponent)deserializedTC;
+            mavenComponent.GroupId.Should().Be("SomeGroupId");
+            mavenComponent.ArtifactId.Should().Be("SomeArtifactId");
+            mavenComponent.Version.Should().Be("1.2.3");
+        }
+
+        [TestMethod]
+        public void TypedComponent_Serialization_Git()
+        {
+            TypedComponent.TypedComponent tc = new GitComponent(new Uri("http://some.com/git/url.git"), "SomeHash");
+            var result = JsonConvert.SerializeObject(tc);
+            var deserializedTC = JsonConvert.DeserializeObject(result);
+            deserializedTC.Should().BeOfType(typeof(GitComponent));
+            var gitComponent = (GitComponent)deserializedTC;
+            gitComponent.RepositoryUrl.Should().Be(new Uri("http://some.com/git/url.git"));
+            gitComponent.CommitHash.Should().Be("SomeHash");
+        }
+
+        [TestMethod]
+        public void TypedComponent_Serialization_RubyGems()
+        {
+            TypedComponent.TypedComponent tc = new RubyGemsComponent("SomeGem", "1.2.3", "SampleSource");
+            var result = JsonConvert.SerializeObject(tc);
+            var deserializedTC = JsonConvert.DeserializeObject(result);
+            deserializedTC.Should().BeOfType(typeof(RubyGemsComponent));
+            var rubyGemComponent = (RubyGemsComponent)deserializedTC;
+            rubyGemComponent.Name.Should().Be("SomeGem");
+            rubyGemComponent.Version.Should().Be("1.2.3");
+            rubyGemComponent.Source.Should().Be("SampleSource");
+        }
+
+        [TestMethod]
+        public void TypedComponent_Serialization_Cargo()
+        {
+            TypedComponent.TypedComponent tc = new CargoComponent("SomeCargoPackage", "1.2.3");
+            var result = JsonConvert.SerializeObject(tc);
+            var deserializedTC = JsonConvert.DeserializeObject(result);
+            deserializedTC.Should().BeOfType(typeof(CargoComponent));
+            var cargoComponent = (CargoComponent)deserializedTC;
+            cargoComponent.Name.Should().Be("SomeCargoPackage");
+            cargoComponent.Version.Should().Be("1.2.3");
+        }
+
+        [TestMethod]
+        public void TypedComponent_Serialization_Pip()
+        {
+            TypedComponent.TypedComponent tc = new PipComponent("SomePipPackage", "1.2.3");
+            var result = JsonConvert.SerializeObject(tc);
+            var deserializedTC = JsonConvert.DeserializeObject(result);
+            deserializedTC.Should().BeOfType(typeof(PipComponent));
+            var pipComponent = (PipComponent)deserializedTC;
+            pipComponent.Name.Should().Be("SomePipPackage");
+            pipComponent.Version.Should().Be("1.2.3");
+        }
+
+        [TestMethod]
+        public void TypedComponent_Serialization_Go()
+        {
+            TypedComponent.TypedComponent tc = new GoComponent("SomeGoPackage", "1.2.3", "SomeHash");
+            var result = JsonConvert.SerializeObject(tc);
+            var deserializedTC = JsonConvert.DeserializeObject(result);
+            deserializedTC.Should().BeOfType(typeof(GoComponent));
+            var goComponent = (GoComponent)deserializedTC;
+            goComponent.Name.Should().Be("SomeGoPackage");
+            goComponent.Version.Should().Be("1.2.3");
+            goComponent.Hash.Should().Be("SomeHash");
+        }
+
+        [TestMethod]
+        public void TypedComponent_Serialization_DockerImage()
+        {
+            TypedComponent.TypedComponent tc = new DockerImageComponent("SomeImageHash", "SomeImageName", "SomeImageTag");
+            var result = JsonConvert.SerializeObject(tc);
+            var deserializedTC = JsonConvert.DeserializeObject(result);
+            deserializedTC.Should().BeOfType(typeof(DockerImageComponent));
+            var dockerImageComponent = (DockerImageComponent)deserializedTC;
+            dockerImageComponent.Digest.Should().Be("SomeImageHash");
+            dockerImageComponent.Name.Should().Be("SomeImageName");
+            dockerImageComponent.Tag.Should().Be("SomeImageTag");
+        }
+
+        [TestMethod]
+        public void TypedComponent_Serialization_PodComponent()
+        {
+            TypedComponent.TypedComponent tc = new PodComponent("SomePodName", "SomePodVersion", "SomeSpecRepo");
+            var result = JsonConvert.SerializeObject(tc);
+            var deserializedTC = JsonConvert.DeserializeObject(result);
+            deserializedTC.Should().BeOfType(typeof(PodComponent));
+            var podComponent = (PodComponent)deserializedTC;
+            podComponent.Name.Should().Be("SomePodName");
+            podComponent.Version.Should().Be("SomePodVersion");
+            podComponent.SpecRepo.Should().Be("SomeSpecRepo");
+        }
+
+        [TestMethod]
+        public void TypedComponent_Serialization_LinuxComponent()
+        {
+            TypedComponent.TypedComponent tc = new LinuxComponent("SomeLinuxDistribution", "SomeLinuxRelease", "SomeLinuxComponentName", "SomeLinuxComponentVersion");
+            var result = JsonConvert.SerializeObject(tc);
+            var deserializedTC = JsonConvert.DeserializeObject(result);
+            deserializedTC.Should().BeOfType(typeof(LinuxComponent));
+            var linuxComponent = (LinuxComponent)deserializedTC;
+            linuxComponent.Distribution.Should().Be("SomeLinuxDistribution");
+            linuxComponent.Release.Should().Be("SomeLinuxRelease");
+            linuxComponent.Name.Should().Be("SomeLinuxComponentName");
+            linuxComponent.Version.Should().Be("SomeLinuxComponentVersion");
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/GoComponentDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/GoComponentDetectorTests.cs
new file mode 100644
index 000000000..a48a1273d
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/GoComponentDetectorTests.cs
@@ -0,0 +1,321 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Common.DependencyGraph;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Detectors.Go;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Microsoft.ComponentDetection.TestsUtilities;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class GoComponentDetectorTests
+    {
+        private DetectorTestUtility detectorTestUtility;
+        private Mock commandLineMock;
+        private ScanRequest scanRequest;
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            commandLineMock = new Mock();
+            var loggerMock = new Mock();
+
+            var detector = new GoComponentDetector
+            {
+                CommandLineInvocationService = commandLineMock.Object,
+                Logger = loggerMock.Object,
+            };
+
+            var tempPath = Path.GetTempPath();
+            var detectionPath = Path.Combine(tempPath, Guid.NewGuid().ToString());
+            Directory.CreateDirectory(detectionPath);
+
+            scanRequest = new ScanRequest(new DirectoryInfo(detectionPath), (name, directoryName) => false, loggerMock.Object, null, null, new ComponentRecorder());
+
+            detectorTestUtility = DetectorTestUtilityCreator.Create()
+                                                            .WithScanRequest(scanRequest)
+                                                            .WithDetector(detector);
+
+            commandLineMock.Setup(x => x.CanCommandBeLocated("go", null, It.IsAny(), It.IsAny()))
+                .ReturnsAsync(false);
+        }
+
+        [TestMethod]
+        public async Task TestGoModDetectorWithValidFile_ReturnsSuccessfully()
+        {
+            var goMod =
+@"module github.com/Azure/azure-storage-blob-go
+
+require (
+    github.com/Azure/azure-pipeline-go v0.2.1
+    github.com/kr/pretty v0.1.0 // indirect
+    gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127
+    github.com/dgrijalva/jwt-go v3.2.0+incompatible
+)";
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("go.mod", goMod)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(4, detectedComponents.Count());
+
+            var discoveredComponents = detectedComponents.ToArray();
+            discoveredComponents.Where(component => component.Component.Id == "github.com/Azure/azure-pipeline-go v0.2.1 - Go").Count().Should().Be(1);
+            discoveredComponents.Where(component => component.Component.Id == "github.com/dgrijalva/jwt-go v3.2.0+incompatible - Go").Count().Should().Be(1);
+            discoveredComponents.Where(component => component.Component.Id == "gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 - Go").Count().Should().Be(1);
+            discoveredComponents.Where(component => component.Component.Id == "github.com/kr/pretty v0.1.0 - Go").Count().Should().Be(1);
+        }
+
+        [TestMethod]
+        public async Task TestGoSumDetectorWithValidFile_ReturnsSuccessfully()
+        {
+            var goSum =
+@"
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+)";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("go.sum", goSum)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(6, detectedComponents.Count());
+            List typedComponents = detectedComponents.Select(d => d.Component).ToList();
+            Assert.IsTrue(typedComponents.Contains(
+                new GoComponent("github.com/golang/mock", "v1.1.1", "h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=")));
+            Assert.IsTrue(typedComponents.Contains(
+                new GoComponent("github.com/golang/mock", "v1.2.0", "h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=")));
+            Assert.IsTrue(typedComponents.Contains(
+                new GoComponent("github.com/golang/protobuf", "v0.0.0-20161109072736-4bd1920723d7", "h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=")));
+            Assert.IsTrue(typedComponents.Contains(
+                new GoComponent("github.com/golang/protobuf", "v1.2.0", "h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=")));
+            Assert.IsTrue(typedComponents.Contains(
+                new GoComponent("github.com/golang/protobuf", "v1.3.1", "h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=")));
+            Assert.IsTrue(typedComponents.Contains(
+                new GoComponent("github.com/golang/protobuf", "v1.3.2", "h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=")));
+        }
+
+        [TestMethod]
+        public async Task TestGoModDetector_MultipleSpaces_ReturnsSuccessfully()
+        {
+            var goMod =
+@"module github.com/Azure/azure-storage-blob-go
+
+require (
+    github.com/Azure/azure-pipeline-go      v0.2.1
+    github.com/kr/pretty    v0.1.0 // indirect
+    gopkg.in/check.v1   v1.0.0-20180628173108-788fd7840127
+    github.com/dgrijalva/jwt-go     v3.2.0+incompatible
+)";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("go.mod", goMod)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(4, detectedComponents.Count());
+
+            var discoveredComponents = detectedComponents.ToArray();
+            discoveredComponents.Where(component => component.Component.Id == "github.com/Azure/azure-pipeline-go v0.2.1 - Go").Count().Should().Be(1);
+            discoveredComponents.Where(component => component.Component.Id == "github.com/dgrijalva/jwt-go v3.2.0+incompatible - Go").Count().Should().Be(1);
+            discoveredComponents.Where(component => component.Component.Id == "gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 - Go").Count().Should().Be(1);
+            discoveredComponents.Where(component => component.Component.Id == "github.com/kr/pretty v0.1.0 - Go").Count().Should().Be(1);
+        }
+
+        [TestMethod]
+        public async Task TestGoModDetector_ComponentsWithMultipleLocations_ReturnsSuccessfully()
+        {
+            var goMod1 =
+@"module github.com/Azure/azure-storage-blob-go
+
+require (
+    github.com/Azure/azure-pipeline-go v0.2.1
+    github.com/kr/pretty v0.1.0 // indirect
+    gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127
+    github.com/Azure/go-autorest v10.15.2+incompatible
+)";
+            var goMod2 =
+@"module github.com/Azure/azure-storage-blob-go
+
+require (
+    github.com/Azure/azure-pipeline-go v0.2.1
+    github.com/kr/pretty v0.1.0 // indirect
+    gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127
+    github.com/Azure/go-autorest v10.15.2+incompatible
+)";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("go.mod", goMod1)
+                                                    .WithFile("go.mod", goMod2, fileLocation: Path.Join(Path.GetTempPath(), "another-location", "go.mod"))
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(4, componentRecorder.GetDetectedComponents().Count());
+
+            var dependencyGraphs = componentRecorder.GetDependencyGraphsByLocation();
+            Assert.IsTrue(dependencyGraphs.Keys.Count() == 2);
+
+            var firstGraph = dependencyGraphs.Values.First();
+            var secondGraph = dependencyGraphs.Values.Skip(1).First();
+
+            firstGraph.GetComponents().Should().BeEquivalentTo(secondGraph.GetComponents());
+        }
+
+        [TestMethod]
+        public async Task TestGoModDetectorInvalidFiles_DoesNotFail()
+        {
+            string invalidGoMod =
+@"     #/bin/sh
+lorem ipsum
+four score and seven bugs ago
+$#26^#25%4";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("go.mod", invalidGoMod)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(0, componentRecorder.GetDetectedComponents().Count());
+        }
+
+        [TestMethod]
+        public async Task TestGoSumDetection_TwoEntriesForTheSameComponent_ReturnsSuccessfully()
+        {
+            var goSum =
+@"
+github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM=
+github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4=
+)";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("go.sum", goSum)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(1, detectedComponents.Count());
+        }
+
+        [TestMethod]
+        public async Task TestGoModDetector_DetectorOnlyDetectInsideRequireSection()
+        {
+            var goMod =
+@"module github.com/Azure/azure-storage-blob-go
+
+require (
+    github.com/Azure/azure-pipeline-go v0.2.1
+    github.com/kr/pretty v0.1.0 // indirect
+)
+replace (
+	github.com/Azure/go-autorest => github.com/Azure/go-autorest v13.3.2+incompatible
+	github.com/docker/distribution => github.com/docker/distribution v0.0.0-20191216044856-a8371794149d
+)
+";
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("go.mod", goMod)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(2, detectedComponents.Count());
+
+            var discoveredComponents = detectedComponents.ToArray();
+            discoveredComponents.Where(component => component.Component.Id == "github.com/Azure/azure-pipeline-go v0.2.1 - Go").Count().Should().Be(1);
+            discoveredComponents.Where(component => component.Component.Id == "github.com/kr/pretty v0.1.0 - Go").Count().Should().Be(1);
+        }
+
+        [TestMethod]
+        public async Task TestGoDetector_GoCommandNotFound()
+        {
+            commandLineMock.Setup(x => x.CanCommandBeLocated("go", null, It.IsAny(), It.IsAny()))
+                .ReturnsAsync(false);
+
+            await TestGoSumDetectorWithValidFile_ReturnsSuccessfully();
+        }
+
+        [TestMethod]
+        public async Task TestGoDetector_GoCommandThrows()
+        {
+            commandLineMock.Setup(x => x.CanCommandBeLocated("go", null, It.IsAny(), It.IsAny()))
+                .ReturnsAsync(() => throw new Exception("Some horrible error occured"));
+
+            await TestGoSumDetectorWithValidFile_ReturnsSuccessfully();
+        }
+
+        [TestMethod]
+        public async Task TestGoDetector_GoGraphCommandFails()
+        {
+            commandLineMock.Setup(x => x.CanCommandBeLocated("go", null, It.IsAny(), It.IsAny()))
+                .ReturnsAsync(true);
+
+            commandLineMock.Setup(x => x.ExecuteCommand("go mod graph", null, It.IsAny(), It.IsAny()))
+                .ReturnsAsync(new CommandLineExecutionResult
+                {
+                    ExitCode = 1,
+                });
+
+            await TestGoSumDetectorWithValidFile_ReturnsSuccessfully();
+        }
+
+        [TestMethod]
+        public async Task TestGoDetector_GoGraphCommandThrows()
+        {
+            commandLineMock.Setup(x => x.CanCommandBeLocated("go", null, It.IsAny(), It.IsAny()))
+                .ReturnsAsync(true);
+
+            commandLineMock.Setup(x => x.ExecuteCommand("go mod graph", null, It.IsAny(), It.IsAny()))
+                .ReturnsAsync(() => throw new Exception("Some horrible error occured"));
+
+            await TestGoSumDetectorWithValidFile_ReturnsSuccessfully();
+        }
+
+        [TestMethod]
+        public async Task TestGoDetector_GoGraphHappyPath()
+        {
+            var goGraph = "example.com/mainModule some-package@v1.2.3\nsome-package@v1.2.3 other@v1.0.0\nsome-package@v1.2.3 test@v2.0.0\ntest@v2.0.0 a@v1.5.0";
+
+            commandLineMock.Setup(x => x.CanCommandBeLocated("go", null, It.IsAny(), It.IsAny()))
+                .ReturnsAsync(true);
+
+            commandLineMock.Setup(x => x.ExecuteCommand("go", null, It.IsAny(), new[] { "mod", "graph" }))
+                .ReturnsAsync(new CommandLineExecutionResult
+                {
+                    ExitCode = 0,
+                    StdOut = goGraph,
+                });
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("go.mod", string.Empty)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(0, detectedComponents.Count());
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/GoComponentTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/GoComponentTests.cs
new file mode 100644
index 000000000..3042d3a56
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/GoComponentTests.cs
@@ -0,0 +1,97 @@
+using System;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class GoComponentTests
+    {
+        private static readonly string TestName = Guid.NewGuid().ToString();
+        private static readonly string TestVersion = Guid.NewGuid().ToString();
+        private static readonly string TestHash = Guid.NewGuid().ToString();
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+        }
+
+        [TestMethod]
+        public void ConstructorTest_NameVersion()
+        {
+            GoComponent goComponent = new GoComponent(TestName, TestVersion);
+            Assert.AreEqual(TestName, goComponent.Name);
+            Assert.AreEqual(TestVersion, goComponent.Version);
+            Assert.AreEqual(string.Empty, goComponent.Hash);
+            Assert.AreEqual($"{TestName} {TestVersion} - Go", goComponent.Id);
+        }
+
+        [TestMethod]
+        [ExpectedException(typeof(ArgumentNullException))]
+        public void ConstructorTest_NameVersion_NullVersion()
+        {
+            GoComponent goComponent = new GoComponent(TestName, null);
+        }
+
+        [TestMethod]
+        [ExpectedException(typeof(ArgumentNullException))]
+        public void ConstructorTest_NameVersion_NullName()
+        {
+            GoComponent goComponent = new GoComponent(null, TestVersion);
+        }
+
+        [TestMethod]
+        public void ConstructorTest_NameVersionHash()
+        {
+            GoComponent goComponent = new GoComponent(TestName, TestVersion, TestHash);
+            Assert.AreEqual(TestName, goComponent.Name);
+            Assert.AreEqual(TestVersion, goComponent.Version);
+            Assert.AreEqual(TestHash, goComponent.Hash);
+            Assert.AreEqual($"{TestName} {TestVersion} - Go", goComponent.Id);
+        }
+
+        [TestMethod]
+        [ExpectedException(typeof(ArgumentNullException))]
+        public void ConstructorTest_NameVersionHash_NullVersion()
+        {
+            GoComponent goComponent = new GoComponent(TestName, null, TestHash);
+        }
+
+        [TestMethod]
+        [ExpectedException(typeof(ArgumentNullException))]
+        public void ConstructorTest_NameVersionHash_NullName()
+        {
+            GoComponent goComponent = new GoComponent(null, TestVersion, TestHash);
+        }
+
+        [TestMethod]
+        [ExpectedException(typeof(ArgumentNullException))]
+        public void ConstructorTest_NameVersionHash_NullHash()
+        {
+            GoComponent goComponent = new GoComponent(TestName, TestVersion, null);
+        }
+
+        [TestMethod]
+        public void TestEquals()
+        {
+            GoComponent goComponent1 = new GoComponent(TestName, TestVersion, TestHash);
+            GoComponent goComponent2 = new GoComponent(TestName, TestVersion, TestHash);
+            GoComponent goComponent3 = new GoComponent(TestName, TestVersion, Guid.NewGuid().ToString());
+            Assert.IsTrue(goComponent1.Equals(goComponent2));
+            Assert.IsTrue(((object)goComponent1).Equals(goComponent2));
+
+            Assert.IsFalse(goComponent1.Equals(goComponent3));
+            Assert.IsFalse(((object)goComponent1).Equals(goComponent3));
+        }
+
+        [TestMethod]
+        public void TestGetHashCode()
+        {
+            GoComponent goComponent1 = new GoComponent(TestName, TestVersion, TestHash);
+            GoComponent goComponent2 = new GoComponent(TestName, TestVersion, TestHash);
+            Assert.IsTrue(goComponent1.GetHashCode() == goComponent2.GetHashCode());
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/GradleComponentDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/GradleComponentDetectorTests.cs
new file mode 100644
index 000000000..33428084f
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/GradleComponentDetectorTests.cs
@@ -0,0 +1,228 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Detectors.Gradle;
+using Microsoft.ComponentDetection.Detectors.Tests.Utilities;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Microsoft.ComponentDetection.TestsUtilities;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class GradleComponentDetectorTests
+    {
+        private DetectorTestUtility detectorTestUtility;
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            detectorTestUtility = DetectorTestUtilityCreator.Create();
+        }
+
+        [TestMethod]
+        public async Task TestGradleDetectorWithNoFiles_ReturnsSuccessfully()
+        {
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(0, componentRecorder.GetDetectedComponents().Count());
+        }
+
+        [TestMethod]
+        public async Task TestGradleDetectorWithValidFile_DetectsComponentsSuccessfully()
+        {
+            string validFileOne =
+@"org.springframework:spring-beans:5.0.5.RELEASE
+org.springframework:spring-core:5.0.5.RELEASE
+org.springframework:spring-jcl:5.0.5.RELEASE";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("gradle.lockfile", validFileOne)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var discoveredComponents = componentRecorder.GetDetectedComponents().Select(c => (MavenComponent)c.Component).OrderBy(c => c.ArtifactId).ToList();
+
+            Assert.AreEqual(3, discoveredComponents.Count);
+
+            Assert.AreEqual("org.springframework", discoveredComponents[0].GroupId);
+            Assert.AreEqual("spring-beans", discoveredComponents[0].ArtifactId);
+            Assert.AreEqual("5.0.5.RELEASE", discoveredComponents[0].Version);
+
+            Assert.AreEqual("org.springframework", discoveredComponents[1].GroupId);
+            Assert.AreEqual("spring-core", discoveredComponents[1].ArtifactId);
+            Assert.AreEqual("5.0.5.RELEASE", discoveredComponents[1].Version);
+
+            Assert.AreEqual("org.springframework", discoveredComponents[2].GroupId);
+            Assert.AreEqual("spring-jcl", discoveredComponents[2].ArtifactId);
+            Assert.AreEqual("5.0.5.RELEASE", discoveredComponents[2].Version);
+        }
+
+        [TestMethod]
+        public async Task TestGradleDetectorWithValidSingleLockfilePerProject_DetectsComponentsSuccessfully()
+        {
+            string validFileOne =
+@"org.springframework:spring-beans:5.0.5.RELEASE=lintClassPath
+org.springframework:spring-core:5.0.5.RELEASE=debugCompile,releaseCompile
+org.springframework:spring-jcl:5.0.5.RELEASE=lintClassPath,debugCompile,releaseCompile";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("gradle.lockfile", validFileOne)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            var discoveredComponents = detectedComponents.Select(c => (MavenComponent)c.Component).OrderBy(c => c.ArtifactId).ToList();
+
+            Assert.AreEqual(3, discoveredComponents.Count);
+
+            Assert.AreEqual("org.springframework", discoveredComponents[0].GroupId);
+            Assert.AreEqual("spring-beans", discoveredComponents[0].ArtifactId);
+            Assert.AreEqual("5.0.5.RELEASE", discoveredComponents[0].Version);
+
+            Assert.AreEqual("org.springframework", discoveredComponents[1].GroupId);
+            Assert.AreEqual("spring-core", discoveredComponents[1].ArtifactId);
+            Assert.AreEqual("5.0.5.RELEASE", discoveredComponents[1].Version);
+
+            Assert.AreEqual("org.springframework", discoveredComponents[2].GroupId);
+            Assert.AreEqual("spring-jcl", discoveredComponents[2].ArtifactId);
+            Assert.AreEqual("5.0.5.RELEASE", discoveredComponents[2].Version);
+        }
+
+        [TestMethod]
+        public async Task TestGradleDetectorWithValidFiles_ReturnsSuccessfully()
+        {
+            string validFileOne =
+@"org.springframework:spring-beans:5.0.5.RELEASE
+org.springframework:spring-core:5.0.5.RELEASE
+org.springframework:spring-jcl:5.0.5.RELEASE";
+
+            string validFileTwo =
+@"com.fasterxml.jackson.core:jackson-annotations:2.8.0
+com.fasterxml.jackson.core:jackson-core:2.8.10
+com.fasterxml.jackson.core:jackson-databind:2.8.11.3
+org.msgpack:msgpack-core:0.8.16
+org.springframework:spring-jcl:5.0.5.RELEASE";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("gradle.lockfile", validFileOne)
+                                                    .WithFile("gradle2.lockfile", validFileTwo)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(7, componentRecorder.GetDetectedComponents().Count());
+
+            var dependencyGraphs = componentRecorder.GetDependencyGraphsByLocation();
+            dependencyGraphs.Keys.Count().Should().Be(2);
+
+            var graph1 = dependencyGraphs.Values.Where(dependencyGraph => dependencyGraph.GetComponents().Count() == 3).Single();
+            var graph2 = dependencyGraphs.Values.Where(dependencyGraph => dependencyGraph.GetComponents().Count() == 5).Single();
+
+            var expectedComponents = new List
+            {
+                // Graph 1
+                "org.springframework spring-jcl 5.0.5.RELEASE - Maven",
+                "org.springframework spring-beans 5.0.5.RELEASE - Maven",
+                "org.springframework spring-core 5.0.5.RELEASE - Maven",
+
+                // Graph 2
+                "org.msgpack msgpack-core 0.8.16 - Maven",
+                "org.springframework spring-jcl 5.0.5.RELEASE - Maven",
+                "com.fasterxml.jackson.core jackson-core 2.8.10 - Maven",
+                "com.fasterxml.jackson.core jackson-annotations 2.8.0 - Maven",
+                "com.fasterxml.jackson.core jackson-databind 2.8.11.3 - Maven",
+            };
+
+            foreach (var componentId in expectedComponents)
+            {
+                var component = componentRecorder.GetComponent(componentId);
+                component.Should().NotBeNull();
+            }
+        }
+
+        [TestMethod]
+        public async Task TestGradleDetector_SameComponentDifferentLocations_DifferentLocationsAreSaved()
+        {
+            string validFileOne =
+@"org.springframework:spring-beans:5.0.5.RELEASE";
+
+            string validFileTwo =
+"org.springframework:spring-beans:5.0.5.RELEASE";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("gradle.lockfile", validFileOne)
+                                                    .WithFile("gradle2.lockfile", validFileTwo)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(1, componentRecorder.GetDetectedComponents().Count());
+
+            componentRecorder.ForOneComponent(componentRecorder.GetDetectedComponents().First().Component.Id, x =>
+            {
+                Enumerable.Count(x.AllFileLocations).Should().Be(2);
+            });
+
+            var dependencyGraphs = componentRecorder.GetDependencyGraphsByLocation();
+            dependencyGraphs.Keys.Count().Should().Be(2);
+
+            var graph1 = dependencyGraphs.Values.First();
+            var graph2 = dependencyGraphs.Values.Skip(1).First();
+
+            graph1.GetComponents().Should().BeEquivalentTo(graph2.GetComponents());
+        }
+
+        [TestMethod]
+        public async Task TestGradleDetectorWithInvalidAndValidFiles_ReturnsSuccessfully()
+        {
+            string validFileTwo =
+@"com.fasterxml.jackson.core:jackson-annotations:2.8.0
+com.fasterxml.jackson.core:jackson-core:2.8.10
+com.fasterxml.jackson.core:jackson-databind:2.8.11.3
+org.msgpack:msgpack-core:0.8.16
+org.springframework:spring-jcl:5.0.5.RELEASE";
+
+            string invalidFileOne =
+@"     #/bin/sh
+lorem ipsum
+four score and seven bugs ago
+$#26^#25%4";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("gradle.lockfile", invalidFileOne)
+                                                    .WithFile("gradle2.lockfile", validFileTwo)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(5, componentRecorder.GetDetectedComponents().Count());
+
+            var dependencyGraphs = componentRecorder.GetDependencyGraphsByLocation();
+            dependencyGraphs.Keys.Count().Should().Be(1);
+
+            var graph2 = dependencyGraphs.Values.Single();
+
+            var expectedComponents = new List
+            {
+                // Graph 2
+                "org.msgpack msgpack-core 0.8.16 - Maven",
+                "org.springframework spring-jcl 5.0.5.RELEASE - Maven",
+                "com.fasterxml.jackson.core jackson-core 2.8.10 - Maven",
+                "com.fasterxml.jackson.core jackson-annotations 2.8.0 - Maven",
+                "com.fasterxml.jackson.core jackson-databind 2.8.11.3 - Maven",
+            };
+
+            foreach (var componentId in expectedComponents)
+            {
+                var component = componentRecorder.GetComponent(componentId);
+                component.Should().NotBeNull();
+            }
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/GradleTestUtilities.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/GradleTestUtilities.cs
new file mode 100644
index 000000000..f29074381
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/GradleTestUtilities.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Reactive.Linq;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.Internal;
+using Moq;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    public static class GradleTestUtilities
+    {
+        public static IComponentStreamEnumerableFactory GetMockComponentStreamEnumerableFactory(IEnumerable streams, IEnumerable patterns = null)
+        {
+            var mock = new Mock();
+            mock.Setup(x => x.GetComponentStreams(It.IsAny(), patterns ?? It.IsAny>(), It.IsAny(), It.IsAny())).Returns(streams);
+
+            return mock.Object;
+        }
+
+        public static IObservableDirectoryWalkerFactory GetDirectoryWalker(IEnumerable processRequests, IEnumerable patterns = null)
+        {
+            var mock = new Mock();
+            mock.Setup(x => x.Initialize(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>()));
+            mock.Setup(x => x.GetFilteredComponentStreamObservable(It.IsAny(), It.IsAny>(), It.IsAny())).Returns(() => processRequests.ToObservable());
+
+            return mock.Object;
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/IvyDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/IvyDetectorTests.cs
new file mode 100644
index 000000000..3d3eac1be
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/IvyDetectorTests.cs
@@ -0,0 +1,189 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Common.DependencyGraph;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Detectors.Ivy;
+using Microsoft.ComponentDetection.TestsUtilities;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class IvyDetectorTests
+    {
+        private Mock commandLineMock;
+        private DetectorTestUtility detectorTestUtility;
+        private ScanRequest scanRequest;
+
+        [TestInitialize]
+        public void InitializeTests()
+        {
+            commandLineMock = new Mock();
+            var loggerMock = new Mock();
+
+            var detector = new IvyDetector
+            {
+                CommandLineInvocationService = commandLineMock.Object,
+                Logger = loggerMock.Object,
+            };
+
+            var tempPath = Path.GetTempPath();
+            var detectionPath = Path.Combine(tempPath, Guid.NewGuid().ToString());
+            Directory.CreateDirectory(detectionPath);
+
+            scanRequest = new ScanRequest(new DirectoryInfo(detectionPath), (name, directoryName) => false, loggerMock.Object, null, null, new ComponentRecorder());
+
+            detectorTestUtility = DetectorTestUtilityCreator.Create()
+                                                            .WithScanRequest(scanRequest)
+                                                            .WithDetector(detector);
+        }
+
+        [TestCleanup]
+        public void TestCleanup()
+        {
+            scanRequest.SourceDirectory.Delete(recursive: true);
+        }
+
+        [TestMethod]
+        public async Task IfAntIsNotAvailableThenExitDetectorGracefully()
+        {
+            commandLineMock.Setup(x => x.CanCommandBeLocated(IvyDetector.PrimaryCommand, IvyDetector.AdditionalValidCommands, IvyDetector.AntVersionArgument))
+                .ReturnsAsync(false);
+
+            var (detectorResult, componentRecorder) = await detectorTestUtility.ExecuteDetector();
+
+            Assert.AreEqual(componentRecorder.GetDetectedComponents().Count(), 0);
+            Assert.AreEqual(detectorResult.ResultCode, ProcessingResultCode.Success);
+        }
+
+        [TestMethod]
+        public async Task AntAvailableHappyPath()
+        {
+            // Fake output from the IvyComponentDetectionAntTask
+            var registerUsageContent = "{\"RegisterUsage\": [" +
+                "{ \"gav\": { \"g\": \"d0g\", \"a\": \"d0a\", \"v\": \"0.0.0\"}, \"DevelopmentDependency\": false, \"resolved\": false},\n" +
+                "{ \"gav\": { \"g\": \"d1g\", \"a\": \"d1a\", \"v\": \"1.1.1\"}, \"DevelopmentDependency\": true, \"resolved\": true},\n" +
+                "{ \"gav\": { \"g\": \"d2g\", \"a\": \"d2a\", \"v\": \"2.2.2\"}, \"DevelopmentDependency\": false, \"resolved\": true},\n" +
+                "{ \"gav\": { \"g\": \"d3g\", \"a\": \"d3a\", \"v\": \"3.3.3\"}, \"DevelopmentDependency\": false, \"resolved\": true, \"parent_gav\": { \"g\": \"d2g\", \"a\": \"d2a\", \"v\": \"2.2.2\"}},\n" +
+                "]}";
+
+            IvyHappyPath(content: registerUsageContent);
+
+            var (detectorResult, componentRecorder) = await detectorTestUtility.ExecuteDetector();
+
+            var detectedComponents = componentRecorder.GetDetectedComponents(); // IsDevelopmentDependency = true in componentRecorder but null in detectedComponents... why?
+            Assert.AreEqual(3, detectedComponents.Count());
+            Assert.AreEqual(ProcessingResultCode.Success, detectorResult.ResultCode);
+
+            foreach (var detectedComponent in detectedComponents)
+            {
+                var dm = (MavenComponent)detectedComponent.Component;
+                Assert.AreEqual(dm.ArtifactId.Replace('a', 'g'), dm.GroupId);
+                Assert.AreEqual(dm.GroupId.Replace('g', 'a'), dm.ArtifactId);
+                Assert.AreEqual(string.Format("{0}.{0}.{0}", dm.ArtifactId.Substring(1, 1)), dm.Version);
+                Assert.AreEqual(ComponentType.Maven, dm.Type);
+
+                // "Detector should not populate DetectedComponent.DevelopmentDependency" - see ComponentRecorder.cs.  Hence we get null not true (for d1g:d1a:1.1.1) or false here.
+                Assert.IsNull(detectedComponent.DevelopmentDependency);
+
+                // "Detector should not populate DetectedComponent.DependencyRoots!"
+                Assert.IsNull(detectedComponent.DependencyRoots);
+            }
+        }
+
+        [TestMethod]
+        public async Task IvyDetector_FileObservableIsNotPresent_DetectionShouldNotFail()
+        {
+            commandLineMock.Setup(x => x.CanCommandBeLocated(IvyDetector.PrimaryCommand, IvyDetector.AdditionalValidCommands, IvyDetector.AntVersionArgument))
+                            .ReturnsAsync(true);
+
+            Func action = async () => await detectorTestUtility.ExecuteDetector();
+
+            await action.Should().NotThrowAsync();
+        }
+
+        [TestMethod]
+        public async Task IvyDependencyGraph()
+        {
+            // Fake output from the IvyComponentDetectionAntTask
+            var registerUsageContent = "{\"RegisterUsage\": [" +
+                "{ \"gav\": { \"g\": \"d0g\", \"a\": \"d0a\", \"v\": \"0.0.0\"}, \"DevelopmentDependency\": false, \"resolved\": false},\n" +
+                "{ \"gav\": { \"g\": \"d1g\", \"a\": \"d1a\", \"v\": \"1.1.1\"}, \"DevelopmentDependency\": true, \"resolved\": true},\n" +
+                "{ \"gav\": { \"g\": \"d2g\", \"a\": \"d2a\", \"v\": \"2.2.2\"}, \"DevelopmentDependency\": false, \"resolved\": true},\n" +
+                "{ \"gav\": { \"g\": \"d3g\", \"a\": \"d3a\", \"v\": \"3.3.3\"}, \"DevelopmentDependency\": false, \"resolved\": true, \"parent_gav\": { \"g\": \"d2g\", \"a\": \"d2a\", \"v\": \"2.2.2\"}},\n" +
+                "]}";
+
+            var d1Id = "d1g d1a 1.1.1 - Maven";
+            var d2Id = "d2g d2a 2.2.2 - Maven";
+            var d3Id = "d3g d3a 3.3.3 - Maven";
+
+            IvyHappyPath(content: registerUsageContent);
+
+            var (detectorResult, componentRecorder) = await detectorTestUtility.ExecuteDetector();
+
+            var detectedComponents = componentRecorder.GetDetectedComponents(); // IsDevelopmentDependency = true in componentRecorder but null in detectedComponents... why?
+            Assert.AreEqual(3, detectedComponents.Count());
+            Assert.AreEqual(ProcessingResultCode.Success, detectorResult.ResultCode);
+
+            // There is only one graph
+            var dependencyGraph = componentRecorder.GetDependencyGraphsByLocation().Values.First();
+
+            dependencyGraph.GetDependenciesForComponent(d1Id).Should().HaveCount(0);
+            dependencyGraph.IsComponentExplicitlyReferenced(d1Id).Should().BeTrue();
+            dependencyGraph.IsDevelopmentDependency(d1Id).Should().BeTrue();
+
+            dependencyGraph.GetDependenciesForComponent(d2Id).Should().HaveCount(1);
+            dependencyGraph.GetDependenciesForComponent(d2Id).Should().Contain(d3Id);
+            dependencyGraph.IsComponentExplicitlyReferenced(d2Id).Should().BeTrue();
+            dependencyGraph.IsDevelopmentDependency(d2Id).Should().BeFalse();
+
+            dependencyGraph.GetDependenciesForComponent(d3Id).Should().HaveCount(0);
+            dependencyGraph.IsComponentExplicitlyReferenced(d3Id).Should().BeFalse();
+            dependencyGraph.IsDevelopmentDependency(d3Id).Should().BeFalse();
+        }
+
+        private void IvyHappyPath(string content)
+        {
+            commandLineMock.Setup(x => x.CanCommandBeLocated(IvyDetector.PrimaryCommand, IvyDetector.AdditionalValidCommands, IvyDetector.AntVersionArgument))
+                            .ReturnsAsync(true);
+
+            var expectedIvyXmlLocation = scanRequest.SourceDirectory.FullName;
+
+            File.WriteAllText(Path.Combine(expectedIvyXmlLocation, "ivy.xml"), "(dummy content)");
+            File.WriteAllText(Path.Combine(expectedIvyXmlLocation, "ivysettings.xml"), "(dummy content)");
+            detectorTestUtility
+                .WithFile("ivy.xml", "(dummy content)", fileLocation: Path.Combine(expectedIvyXmlLocation, "ivy.xml"));
+
+            commandLineMock.Setup(
+                x => x.ExecuteCommand(
+                    IvyDetector.PrimaryCommand,
+                    IvyDetector.AdditionalValidCommands,
+                    It.IsAny())).Callback((string cmd, IEnumerable cmd2, string[] parameters) =>
+                {
+                    Assert.AreEqual(parameters[0], "-buildfile");
+                    var workingDir = parameters[1].Replace("build.xml", string.Empty);
+                    Directory.CreateDirectory(Path.Combine(workingDir, "target"));
+                    var jsonFileOutputPath = Path.Combine(workingDir, "target", "RegisterUsage.json");
+                    File.WriteAllText(jsonFileOutputPath, content);
+                    Assert.AreEqual(parameters[2], "resolve-dependencies");
+                }).ReturnsAsync(new CommandLineExecutionResult
+                {
+                    ExitCode = 0,
+                });
+        }
+
+        protected bool ShouldBeEquivalentTo(IEnumerable result, IEnumerable expected)
+        {
+            result.Should().BeEquivalentTo(expected);
+            return true;
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxContainerDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxContainerDetectorTests.cs
new file mode 100644
index 000000000..677ee82a6
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxContainerDetectorTests.cs
@@ -0,0 +1,209 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Common.DependencyGraph;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.BcdeModels;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Detectors.Linux;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class LinuxContainerDetectorTests
+    {
+        private const string NodeLatestImage = "node:latest";
+        private const string NodeLatestDigest = "2a22e4a1a550";
+        private const string BashPackageId = "Ubuntu 20.04 bash 5.0-6ubuntu1 - Linux";
+
+        private static readonly IEnumerable LinuxComponents = new List
+            {
+                new LayerMappedLinuxComponents {
+                    DockerLayer = new DockerLayer { },
+                    LinuxComponents = new List { new LinuxComponent("Ubuntu", "20.04", "bash", "5.0-6ubuntu1") },
+                },
+            };
+
+        private Mock mockDockerService;
+        private Mock mockLogger;
+        private Mock mockSyftLinuxScanner;
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            mockDockerService = new Mock();
+            mockDockerService.Setup(service => service.CanRunLinuxContainersAsync(It.IsAny()))
+                .ReturnsAsync(true);
+            mockDockerService.Setup(service => service.TryPullImageAsync(It.IsAny(), It.IsAny()))
+                .ReturnsAsync(true);
+            mockDockerService.Setup(service => service.InspectImageAsync(It.IsAny(), It.IsAny()))
+                .ReturnsAsync(new ContainerDetails { Id = 1, ImageId = NodeLatestDigest, Layers = Enumerable.Empty() });
+
+            mockLogger = new Mock();
+
+            mockSyftLinuxScanner = new Mock();
+            mockSyftLinuxScanner.Setup(scanner => scanner.ScanLinuxAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny()))
+                .ReturnsAsync(LinuxComponents);
+        }
+
+        [TestMethod]
+        public async Task TestLinuxContainerDetector()
+        {
+            var componentRecorder = new ComponentRecorder();
+
+            var scanRequest = new ScanRequest(new DirectoryInfo(Path.GetTempPath()), (_, __) => false, mockLogger.Object,
+                null, new List { NodeLatestImage }, componentRecorder);
+
+            var linuxContainerDetector = new LinuxContainerDetector
+            {
+                LinuxScanner = mockSyftLinuxScanner.Object,
+                Logger = mockLogger.Object,
+                DockerService = mockDockerService.Object,
+            };
+
+            var scanResult = await linuxContainerDetector.ExecuteDetectorAsync(scanRequest);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents().ToList();
+
+            scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+            detectedComponents.Should().ContainSingle();
+            detectedComponents.First().Component.Id.Should().Be(BashPackageId);
+            scanResult.ContainerDetails.Should().ContainSingle();
+            detectedComponents.All(dc => dc.ContainerDetailIds.Contains(scanResult.ContainerDetails.First().Id)).Should().BeTrue();
+            componentRecorder.GetDetectedComponents().Select(detectedComponent => detectedComponent.Component.Id)
+                .Should().BeEquivalentTo(detectedComponents.Select(detectedComponent => detectedComponent.Component.Id));
+        }
+
+        [TestMethod]
+        public async Task TestLinuxContainerDetector_CantRunLinuxContainers()
+        {
+            var componentRecorder = new ComponentRecorder();
+
+            var scanRequest = new ScanRequest(new DirectoryInfo(Path.GetTempPath()), (_, __) => false, mockLogger.Object, null,
+                new List { NodeLatestImage }, componentRecorder);
+
+            mockDockerService.Setup(service => service.CanRunLinuxContainersAsync(It.IsAny()))
+                .ReturnsAsync(false);
+
+            var linuxContainerDetector = new LinuxContainerDetector
+            {
+                LinuxScanner = mockSyftLinuxScanner.Object,
+                Logger = mockLogger.Object,
+                DockerService = mockDockerService.Object,
+            };
+
+            var scanResult = await linuxContainerDetector.ExecuteDetectorAsync(scanRequest);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents().ToList();
+
+            scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+            detectedComponents.Should().HaveCount(0);
+            scanResult.ContainerDetails.Should().HaveCount(0);
+            mockLogger.Verify(logger => logger.LogInfo(It.IsAny()));
+        }
+
+        [TestMethod]
+        public async Task TestLinuxContainerDetector_TestNull()
+        {
+            var componentRecorder = new ComponentRecorder();
+
+            var scanRequest = new ScanRequest(new DirectoryInfo(Path.GetTempPath()), (_, __) => false, mockLogger.Object, null,
+                null, componentRecorder);
+
+            var linuxContainerDetector = new LinuxContainerDetector
+            {
+                LinuxScanner = mockSyftLinuxScanner.Object,
+                Logger = mockLogger.Object,
+                DockerService = mockDockerService.Object,
+            };
+
+            var scanResult = await linuxContainerDetector.ExecuteDetectorAsync(scanRequest);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+
+            scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+            detectedComponents.Should().HaveCount(0);
+            scanResult.ContainerDetails.Should().HaveCount(0);
+            mockLogger.Verify(logger => logger.LogInfo(It.IsAny()));
+        }
+
+        [TestMethod]
+        public async Task TestLinuxContainerDetector_VerifyLowerCase()
+        {
+            var componentRecorder = new ComponentRecorder();
+
+            var scanRequest = new ScanRequest(new DirectoryInfo(Path.GetTempPath()), (_, __) => false, mockLogger.Object, null,
+                new List { "UPPERCASE" }, componentRecorder);
+
+            var linuxContainerDetector = new LinuxContainerDetector
+            {
+                LinuxScanner = mockSyftLinuxScanner.Object,
+                Logger = mockLogger.Object,
+                DockerService = mockDockerService.Object,
+            };
+
+            var scanResult = await linuxContainerDetector.ExecuteDetectorAsync(scanRequest);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents().ToList();
+
+            scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+            detectedComponents.Should().ContainSingle();
+            detectedComponents.First().Component.Id.Should().Be(BashPackageId);
+            scanResult.ContainerDetails.Should().HaveCount(1);
+            detectedComponents.All(dc => dc.ContainerDetailIds.Contains(scanResult.ContainerDetails.First().Id)).Should().BeTrue();
+        }
+
+        [TestMethod]
+        public async Task TestLinuxContainerDetector_SameImagePassedMultipleTimes()
+        {
+            var componentRecorder = new ComponentRecorder();
+
+            var scanRequest = new ScanRequest(new DirectoryInfo(Path.GetTempPath()), (_, __) => false, mockLogger.Object, null,
+                new List { NodeLatestImage, NodeLatestDigest }, componentRecorder);
+
+            var linuxContainerDetector = new LinuxContainerDetector
+            {
+                LinuxScanner = mockSyftLinuxScanner.Object,
+                Logger = mockLogger.Object,
+                DockerService = mockDockerService.Object,
+            };
+
+            var scanResult = await linuxContainerDetector.ExecuteDetectorAsync(scanRequest);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents().ToList();
+
+            scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+            scanResult.ContainerDetails.Should().HaveCount(1);
+            detectedComponents.Should().HaveCount(1);
+            detectedComponents.First().Component.Id.Should().Be(BashPackageId);
+            detectedComponents.All(dc => dc.ContainerDetailIds.Contains(scanResult.ContainerDetails.First().Id)).Should().BeTrue();
+            mockSyftLinuxScanner.Verify(scanner => scanner.ScanLinuxAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny()), Times.Once);
+        }
+
+        [TestMethod]
+        public async Task TestLinuxContainerDetector_TimeoutParameterSpecified()
+        {
+            var detectorArgs = new Dictionary { { "Linux.ScanningTimeoutSec", "2" } };
+            var scanRequest = new ScanRequest(new DirectoryInfo(Path.GetTempPath()), (_, __) => false, mockLogger.Object,
+                detectorArgs, new List { NodeLatestImage }, new ComponentRecorder());
+
+            var linuxContainerDetector = new LinuxContainerDetector
+            {
+                LinuxScanner = mockSyftLinuxScanner.Object,
+                Logger = mockLogger.Object,
+                DockerService = mockDockerService.Object,
+            };
+
+            Func action = async () => await linuxContainerDetector.ExecuteDetectorAsync(scanRequest);
+            await action.Should().NotThrowAsync();
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxScannerTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxScannerTests.cs
new file mode 100644
index 000000000..835e93e73
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxScannerTests.cs
@@ -0,0 +1,68 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.BcdeModels;
+using Microsoft.ComponentDetection.Detectors.Linux;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class LinuxScannerTests
+    {
+        private const string SyftOutput = @"{
+                ""distro"": {
+                    ""name"":""test-distribution"",
+                    ""version"":""1.0.0""
+                },
+                ""artifacts"": [
+                    {
+                        ""name"":""test"",
+                        ""version"":""1.0.0"",
+                        ""type"":""deb"",
+                        ""locations"": [
+                            {
+                                ""path"": ""/var/lib/dpkg/status"",
+                                ""layerID"": ""sha256:f95fc50d21d981f1efe1f04109c2c3287c271794f5d9e4fdf9888851a174a971""
+                            }
+                        ]
+                    }
+                ]
+            }";
+        
+        private LinuxScanner linuxScanner;
+        private Mock mockDockerService;
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            mockDockerService = new Mock();
+            mockDockerService.Setup(service => service.CanPingDockerAsync(It.IsAny()))
+                .ReturnsAsync(true);
+            mockDockerService.Setup(service => service.TryPullImageAsync(It.IsAny(), It.IsAny()));
+            mockDockerService.Setup(service => service.CreateAndRunContainerAsync(It.IsAny(), It.IsAny>(), It.IsAny()))
+                .ReturnsAsync((SyftOutput, string.Empty));
+
+            linuxScanner = new LinuxScanner { DockerService = mockDockerService.Object };
+        }
+
+        [TestMethod]
+        public async Task TestLinuxScanner()
+        {
+            var result = (await linuxScanner.ScanLinuxAsync("fake_hash", new[] { new DockerLayer { LayerIndex = 0, DiffId = "sha256:f95fc50d21d981f1efe1f04109c2c3287c271794f5d9e4fdf9888851a174a971" } }, 0)).First().LinuxComponents;
+
+            result.Should().HaveCount(1);
+            var package = result.First();
+            package.Name.Should().Be("test");
+            package.Version.Should().Be("1.0.0");
+            package.Release.Should().Be("1.0.0");
+            package.Distribution.Should().Be("test-distribution");
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/MavenCommandServiceTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/MavenCommandServiceTests.cs
new file mode 100644
index 000000000..b0debcd7c
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/MavenCommandServiceTests.cs
@@ -0,0 +1,127 @@
+using FluentAssertions;
+using Microsoft.ComponentDetection.Common;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.Internal;
+using Microsoft.ComponentDetection.Detectors.Maven;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class MavenCommandServiceTests
+    {
+        private Mock commandLineMock;
+        private Mock parserServiceMock;
+        private MavenCommandService mavenCommandService;
+
+        [TestInitialize]
+        public void InitializeTests()
+        {
+            commandLineMock = new Mock();
+            var loggerMock = new Mock();
+
+            parserServiceMock = new Mock();
+
+            mavenCommandService = new MavenCommandService
+            {
+                CommandLineInvocationService = commandLineMock.Object,
+                ParserService = parserServiceMock.Object,
+                Logger = loggerMock.Object,
+            };
+        }
+
+        [TestMethod]
+        public async Task MavenCLIExists_ExpectedArguments_ReturnTrue()
+        {
+            commandLineMock.Setup(x => x.CanCommandBeLocated(
+                MavenCommandService.PrimaryCommand,
+                MavenCommandService.AdditionalValidCommands,
+                MavenCommandService.MvnVersionArgument)).ReturnsAsync(true);
+
+            var result = await mavenCommandService.MavenCLIExists();
+
+            result.Should().BeTrue();
+        }
+
+        [TestMethod]
+        public async Task MavenCLIExists_ExpectedArguments_ReturnFalse()
+        {
+            commandLineMock.Setup(x => x.CanCommandBeLocated(
+                MavenCommandService.PrimaryCommand,
+                MavenCommandService.AdditionalValidCommands,
+                MavenCommandService.MvnVersionArgument)).ReturnsAsync(false);
+
+            var result = await mavenCommandService.MavenCLIExists();
+
+            result.Should().BeFalse();
+        }
+
+        [TestMethod]
+        public async Task GenerateDependenciesFile_Success()
+        {
+            var pomLocation = "Test/location";
+            var processRequest = new ProcessRequest
+            {
+                ComponentStream = new ComponentStream
+                {
+                    Location = pomLocation,
+                },
+            };
+
+            var bcdeMvnFileName = "bcde.mvndeps";
+            var cliParameters = new[] { "dependency:tree", "-B", $"-DoutputFile={bcdeMvnFileName}", "-DoutputType=text", $"-f{pomLocation}" };
+
+            commandLineMock.Setup(x => x.ExecuteCommand(
+                                                        MavenCommandService.PrimaryCommand,
+                                                        MavenCommandService.AdditionalValidCommands,
+                                                        It.Is(y => ShouldBeEquivalentTo(y, cliParameters))))
+                .ReturnsAsync(new CommandLineExecutionResult
+                {
+                    ExitCode = 0,
+                })
+                .Verifiable();
+
+            await mavenCommandService.GenerateDependenciesFile(processRequest);
+
+            Mock.Verify(commandLineMock);
+        }
+
+        [TestMethod]
+        public void ParseDependenciesFile_Success()
+        {
+            const string componentString = "org.apache.maven:maven-compat:jar:3.6.1-SNAPSHOT";
+            string content = $@"com.bcde.test:top-level:jar:1.0.0{Environment.NewLine}\- {componentString}{Environment.NewLine}";
+
+            var pomLocation = "Test/location";
+            var processRequest = new ProcessRequest
+            {
+                ComponentStream = new ComponentStream
+                {
+                    Location = pomLocation,
+                    Stream = new MemoryStream(Encoding.UTF8.GetBytes(content)),
+                },
+            };
+
+            var lines = new[] { "com.bcde.test:top-level:jar:1.0.0", $"\\- {componentString}" };
+            parserServiceMock.Setup(x => x.Parse(lines, It.IsAny())).Verifiable();
+
+            mavenCommandService.ParseDependenciesFile(processRequest);
+
+            Mock.Verify(parserServiceMock);
+        }
+
+        protected bool ShouldBeEquivalentTo(IEnumerable result, IEnumerable expected)
+        {
+            result.Should().BeEquivalentTo(expected);
+            return true;
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/MavenStyleDependencyGraphParserTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/MavenStyleDependencyGraphParserTests.cs
new file mode 100644
index 000000000..3f27ac4c5
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/MavenStyleDependencyGraphParserTests.cs
@@ -0,0 +1,90 @@
+using System.IO;
+using System.Linq;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Common.DependencyGraph;
+using Microsoft.ComponentDetection.Detectors.Maven;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class MavenStyleDependencyGraphParserTests
+    {
+        private readonly string sampleMavenDependencyTreePath = Path.Combine("Mocks", "MvnCliDependencyOutput.txt");
+
+        [TestMethod]
+        public void MavenFormat_ExpectedParse()
+        {
+            var sampleMavenDependencyTree = File.ReadAllLines(sampleMavenDependencyTreePath);
+
+            MavenStyleDependencyGraphParser parser = new MavenStyleDependencyGraphParser();
+            var parsedGraph = parser.Parse(sampleMavenDependencyTree);
+            Assert.AreEqual(parsedGraph.Children.Count, 20);
+            Assert.AreEqual(parsedGraph.Value, "org.apache.maven:maven-compat:jar:3.6.1-SNAPSHOT");
+
+            // Verify a specific interesting path:
+            var mavenCore = parsedGraph.Children.FirstOrDefault(x => x.Value == "org.apache.maven:maven-core:jar:3.6.1-SNAPSHOT:compile");
+            Assert.IsNotNull(mavenCore);
+            Assert.AreEqual(mavenCore.Children.Count, 7);
+
+            var guice = mavenCore.Children.FirstOrDefault(x => x.Value == "com.google.inject:guice:jar:no_aop:4.2.1:compile");
+            Assert.IsNotNull(guice);
+            Assert.AreEqual(guice.Children.Count, 2);
+
+            var guava = guice.Children.FirstOrDefault(x => x.Value == "com.google.guava:guava:jar:25.1-android:compile");
+            Assert.IsNotNull(guava);
+            Assert.AreEqual(guava.Children.Count, 5);
+
+            var animalSnifferAnnotations = guava.Children.FirstOrDefault(x => x.Value == "org.codehaus.mojo:animal-sniffer-annotations:jar:1.14:compile");
+            Assert.IsNotNull(animalSnifferAnnotations);
+            Assert.AreEqual(animalSnifferAnnotations.Children.Count, 0);
+        }
+
+        [TestMethod]
+        public void MavenFormat_WithSingleFileComponentRecorder_ExpectedParse()
+        {
+            var sampleMavenDependencyTree = File.ReadAllLines(sampleMavenDependencyTreePath);
+
+            MavenStyleDependencyGraphParser parser = new MavenStyleDependencyGraphParser();
+
+            var componentRecorder = new ComponentRecorder();
+            var pomfileLocation = "location";
+
+            parser.Parse(sampleMavenDependencyTree, componentRecorder.CreateSingleFileComponentRecorder(pomfileLocation));
+
+            var dependencyGraph = componentRecorder.GetDependencyGraphsByLocation()[pomfileLocation];
+
+            var topLevelComponentTuple = MavenParsingUtilities.GenerateDetectedComponentFromMavenString("org.apache.maven:maven-compat:jar:3.6.1-SNAPSHOT");
+            var topLevelComponent = topLevelComponentTuple.Component;
+            var mavenCoreTuple = MavenParsingUtilities.GenerateDetectedComponentFromMavenString("org.apache.maven:maven-core:jar:3.6.1-SNAPSHOT:compile");
+            var mavenCore = mavenCoreTuple.Component;
+            var guiceTuple = MavenParsingUtilities.GenerateDetectedComponentFromMavenString("com.google.inject:guice:jar:no_aop:4.2.1:compile");
+            var guice = guiceTuple.Component;
+            var guavaTuple = MavenParsingUtilities.GenerateDetectedComponentFromMavenString("com.google.guava:guava:jar:25.1-android:compile");
+            var guava = guavaTuple.Component;
+            var animalSnifferAnnotationsTuple = MavenParsingUtilities.GenerateDetectedComponentFromMavenString("org.codehaus.mojo:animal-sniffer-annotations:jar:1.14:compile");
+            var animalSnifferAnnotations = animalSnifferAnnotationsTuple.Component;
+
+            var topLevelComponentDependencies = dependencyGraph.GetDependenciesForComponent(topLevelComponent.Component.Id);
+            topLevelComponentDependencies.Should().HaveCount(20);
+            topLevelComponentDependencies.Should().Contain(mavenCore.Component.Id);
+            topLevelComponentDependencies.All(componentId => dependencyGraph.IsComponentExplicitlyReferenced(componentId)).Should().BeTrue();
+
+            var mavenCoreDependencies = dependencyGraph.GetDependenciesForComponent(mavenCore.Component.Id);
+            mavenCoreDependencies.Should().HaveCount(7);
+            mavenCoreDependencies.Should().Contain(guice.Component.Id);
+
+            var guiceDependencies = dependencyGraph.GetDependenciesForComponent(guice.Component.Id);
+            guiceDependencies.Should().HaveCount(2);
+            guiceDependencies.Should().Contain(guava.Component.Id);
+
+            var guavaDependencies = dependencyGraph.GetDependenciesForComponent(guava.Component.Id);
+            guavaDependencies.Should().HaveCount(5);
+            guavaDependencies.Should().Contain(animalSnifferAnnotations.Component.Id);
+
+            dependencyGraph.GetDependenciesForComponent(animalSnifferAnnotations.Component.Id).Should().HaveCount(0);
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/MavenTestUtilities.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/MavenTestUtilities.cs
new file mode 100644
index 000000000..e99709043
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/MavenTestUtilities.cs
@@ -0,0 +1,131 @@
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    public static class MavenTestUtilities
+    {
+        public static string GetMalformedPomFile()
+        {
+            string pomFile = @"
+            ";
+
+            return pomFile;
+        }
+
+        public static string GetPomFileNoDependencies()
+        {
+            string pomFile = @"
+            
+                4.0.0
+                
+                    0.0.1
+                
+
+                
+                
+                
+            ";
+
+            return pomFile;
+        }
+
+        public static string GetPomFileWithDependencyToResolveAsProperty(string groupId, string artifactId, string version)
+        {
+            string pomFile = @"
+            
+                4.0.0
+                
+                    {2}
+                
+
+                
+                    
+                        {0}
+                        {1}
+                        ${{myproperty.version}}            
+                    
+                
+                
+            ";
+            var pomFileTemplate = string.Format(pomFile, groupId, artifactId, version);
+            return pomFileTemplate;
+        }
+
+        public static string GetPomFileWithDependencyToResolveAsProjectVar(string groupId, string artifactId, string version)
+        {
+            string pomFile = @"
+            
+                {2}
+                
+                    
+                        {0}
+                        {1}
+                        ${{myproperty.version}}            
+                    
+                
+                
+            ";
+            var pomFileTemplate = string.Format(pomFile, groupId, artifactId, version);
+            return pomFileTemplate;
+        }
+
+        public static string GetPomFileWithDependencyFailToResolve(string groupId, string artifactId, string version)
+        {
+            string pomFile = @"
+            
+                4.0.0
+                
+                    {2}
+                
+
+                
+                    
+                        {0}
+                        {1}
+                        ${{unknown.version}}            
+                    
+                
+                
+            ";
+            var pomFileTemplate = string.Format(pomFile, groupId, artifactId, version);
+
+            return pomFileTemplate;
+        }
+
+        public static string GetPomFileWithDependency(string groupId, string artifactId, string version)
+        {
+            string pomFile = @"
+            
+                4.0.0
+
+                
+                    
+                        {0}
+                        {1}
+                        {2}            
+                    
+                
+                
+            ";
+            var pomFileTemplate = string.Format(pomFile, groupId, artifactId, version);
+            return pomFileTemplate;
+        }
+
+        public static string GetPomFileWithDependencyNoVersion(string groupId, string artifactId)
+        {
+            string pomFile = @"
+            
+                4.0.0
+
+                
+                    
+                        {0}
+                        {1}          
+                    
+                
+                
+            ";
+
+            var pomFileTemplate = string.Format(pomFile, groupId, artifactId);
+            return pomFileTemplate;
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/Microsoft.ComponentDetection.Detectors.Tests.csproj b/test/Microsoft.ComponentDetection.Detectors.Tests/Microsoft.ComponentDetection.Detectors.Tests.csproj
new file mode 100644
index 000000000..364baa999
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/Microsoft.ComponentDetection.Detectors.Tests.csproj
@@ -0,0 +1,38 @@
+
+
+    
+        
+    
+
+    
+      AnyCPU
+    
+
+    
+      AnyCPU
+    
+
+    
+        
+        
+    
+
+    
+        
+            True
+            True
+            TestResources.resx
+        
+        
+            ResXFileCodeGenerator
+            TestResources.Designer.cs
+        
+        
+            Always
+        
+        
+            Always
+        
+    
+
+
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/GradlewDependencyOutput.txt b/test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/GradlewDependencyOutput.txt
new file mode 100644
index 000000000..f0f9b0ef6
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/GradlewDependencyOutput.txt
@@ -0,0 +1,25 @@
+integrationTestCompileClasspath - Compile classpath for source set 'integration test'.
++--- commons-io:commons-io:2.5
++--- org.kohsuke:github-api:1.94
+|    +--- org.apache.commons:commons-lang3:3.7
+|    +--- commons-codec:commons-codec:1.7
+|    +--- com.fasterxml.jackson.core:jackson-databind:2.9.2
+|    |    +--- com.fasterxml.jackson.core:jackson-annotations:2.9.0
+|    |    \--- com.fasterxml.jackson.core:jackson-core:2.9.2
+|    \--- commons-io:commons-io:1.4 -> 2.5
++--- org.zeroturnaround:zt-zip:1.8
+|    \--- org.slf4j:slf4j-api:1.6.6
++--- org.apache.tika:tika-core:1.3
++--- junit:junit:4.11 -> 4.12
+|    \--- org.hamcrest:hamcrest-core:1.3
++--- org.spockframework:spock-core:1.0-groovy-2.4 -> 1.1-groovy-2.4-rc-4
+|    \--- junit:junit:4.12 (*)
++--- com.netflix.nebula:nebula-test:latest.release -> 7.1.0
+|    +--- com.google.guava:guava:19.0
+|    +--- commons-io:commons-io:2.5
+|    +--- org.spockframework:spock-core:1.1-groovy-2.4-rc-4 (*)
+|    +--- cglib:cglib-nodep:3.2.2
+|    \--- org.objenesis:objenesis:2.4
+\--- com.github.stefanbirkner:system-rules:1.16.0
+     \--- junit:junit-dep:[4.9,) -> 4.11
+          \--- junit:junit:4.11 -> 4.12 (*)
\ No newline at end of file
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/MvnCliDependencyOutput.txt b/test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/MvnCliDependencyOutput.txt
new file mode 100644
index 000000000..6860fe6d3
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/MvnCliDependencyOutput.txt
@@ -0,0 +1,44 @@
+org.apache.maven:maven-compat:jar:3.6.1-SNAPSHOT
++- org.apache.maven:maven-model:jar:3.6.1-SNAPSHOT:compile
++- org.apache.maven:maven-model-builder:jar:3.6.1-SNAPSHOT:compile
+|  \- org.apache.maven:maven-builder-support:jar:3.6.1-SNAPSHOT:compile
++- org.apache.maven:maven-settings:jar:3.6.1-SNAPSHOT:compile
++- org.apache.maven:maven-settings-builder:jar:3.6.1-SNAPSHOT:compile
+|  \- org.sonatype.plexus:plexus-sec-dispatcher:jar:1.4:compile
+|     \- org.sonatype.plexus:plexus-cipher:jar:1.7:compile
++- org.apache.maven:maven-artifact:jar:3.6.1-SNAPSHOT:compile
+|  \- org.apache.commons:commons-lang3:jar:3.8.1:compile
++- org.apache.maven:maven-core:jar:3.6.1-SNAPSHOT:compile
+|  +- org.apache.maven:maven-plugin-api:jar:3.6.1-SNAPSHOT:compile
+|  +- org.apache.maven.resolver:maven-resolver-spi:jar:1.3.1:compile
+|  +- org.apache.maven.shared:maven-shared-utils:jar:3.2.1:compile
+|  |  \- commons-io:commons-io:jar:2.5:compile
+|  +- org.eclipse.sisu:org.eclipse.sisu.inject:jar:0.3.3:compile
+|  +- com.google.inject:guice:jar:no_aop:4.2.1:compile
+|  |  +- aopalliance:aopalliance:jar:1.0:compile
+|  |  \- com.google.guava:guava:jar:25.1-android:compile
+|  |     +- com.google.code.findbugs:jsr305:jar:3.0.2:compile
+|  |     +- org.checkerframework:checker-compat-qual:jar:2.0.0:compile
+|  |     +- com.google.errorprone:error_prone_annotations:jar:2.1.3:compile
+|  |     +- com.google.j2objc:j2objc-annotations:jar:1.1:compile
+|  |     \- org.codehaus.mojo:animal-sniffer-annotations:jar:1.14:compile
+|  +- javax.inject:javax.inject:jar:1:compile
+|  \- org.codehaus.plexus:plexus-classworlds:jar:2.5.2:compile
++- org.apache.maven:maven-resolver-provider:jar:3.6.1-SNAPSHOT:compile
+|  \- org.slf4j:slf4j-api:jar:1.7.25:compile
++- org.apache.maven:maven-repository-metadata:jar:3.6.1-SNAPSHOT:compile
++- org.apache.maven.resolver:maven-resolver-api:jar:1.3.1:compile
++- org.apache.maven.resolver:maven-resolver-util:jar:1.3.1:compile
++- org.apache.maven.resolver:maven-resolver-impl:jar:1.3.1:compile
++- org.codehaus.plexus:plexus-utils:jar:3.1.0:compile
++- org.codehaus.plexus:plexus-interpolation:jar:1.25:compile
++- org.eclipse.sisu:org.eclipse.sisu.plexus:jar:0.3.3:compile
+|  \- javax.enterprise:cdi-api:jar:1.0:compile
+|     \- javax.annotation:jsr250-api:jar:1.0:compile
++- org.codehaus.plexus:plexus-component-annotations:jar:1.7.1:compile
++- org.apache.maven.wagon:wagon-provider-api:jar:3.2.0:compile
++- org.apache.maven.wagon:wagon-file:jar:3.2.0:test
++- org.apache.maven.resolver:maven-resolver-connector-basic:jar:1.3.1:test
++- org.apache.maven.resolver:maven-resolver-transport-wagon:jar:1.3.1:test
+\- junit:junit:jar:4.12:test
+   \- org.hamcrest:hamcrest-core:jar:1.3:test
\ No newline at end of file
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/TestResources.Designer.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/TestResources.Designer.cs
new file mode 100644
index 000000000..7083add65
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/TestResources.Designer.cs
@@ -0,0 +1,182 @@
+//------------------------------------------------------------------------------
+// 
+//     This code was generated by a tool.
+//     Runtime Version:4.0.30319.42000
+//
+//     Changes to this file may cause incorrect behavior and will be lost if
+//     the code is regenerated.
+// 
+//------------------------------------------------------------------------------
+
+namespace Microsoft.ComponentDetection.Detectors.Tests.Mocks {
+    using System;
+    
+    
+    /// 
+    ///   A strongly-typed resource class, for looking up localized strings, etc.
+    /// 
+    // This class was auto-generated by the StronglyTypedResourceBuilder
+    // class via a tool like ResGen or Visual Studio.
+    // To add or remove a member, edit your .ResX file then rerun ResGen
+    // with the /str option, or rebuild your VS project.
+    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+    internal class TestResources {
+        
+        private static global::System.Resources.ResourceManager resourceMan;
+        
+        private static global::System.Globalization.CultureInfo resourceCulture;
+        
+        [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+        internal TestResources() {
+        }
+        
+        /// 
+        ///   Returns the cached ResourceManager instance used by this class.
+        /// 
+        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+        internal static global::System.Resources.ResourceManager ResourceManager {
+            get {
+                if (object.ReferenceEquals(resourceMan, null)) {
+                    global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.ComponentDetection.Detectors.Tests.Mocks.TestResources", typeof(TestResources).Assembly);
+                    resourceMan = temp;
+                }
+                return resourceMan;
+            }
+        }
+        
+        /// 
+        ///   Overrides the current thread's CurrentUICulture property for all
+        ///   resource lookups using this strongly typed resource class.
+        /// 
+        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+        internal static global::System.Globalization.CultureInfo Culture {
+            get {
+                return resourceCulture;
+            }
+            set {
+                resourceCulture = value;
+            }
+        }
+        
+        /// 
+        ///   Looks up a localized string similar to integrationTestCompileClasspath - Compile classpath for source set 'integration test'.
+        ///+--- commons-io:commons-io:2.5
+        ///+--- org.kohsuke:github-api:1.94
+        ///|    +--- org.apache.commons:commons-lang3:3.7
+        ///|    +--- commons-codec:commons-codec:1.7
+        ///|    +--- com.fasterxml.jackson.core:jackson-databind:2.9.2
+        ///|    |    +--- com.fasterxml.jackson.core:jackson-annotations:2.9.0
+        ///|    |    \--- com.fasterxml.jackson.core:jackson-core:2.9.2
+        ///|    \--- commons-io:commons-io:1.4 -> 2.5
+        ///+--- org.zeroturnaround:zt-zip: [rest of string was truncated]";.
+        /// 
+        internal static string GradlewDependencyOutput {
+            get {
+                return ResourceManager.GetString("GradlewDependencyOutput", resourceCulture);
+            }
+        }
+        
+        /// 
+        ///   Looks up a localized string similar to org.apache.maven:maven-compat:jar:3.6.1-SNAPSHOT
+        ///+- org.apache.maven:maven-model:jar:3.6.1-SNAPSHOT:compile
+        ///+- org.apache.maven:maven-model-builder:jar:3.6.1-SNAPSHOT:compile
+        ///|  \- org.apache.maven:maven-builder-support:jar:3.6.1-SNAPSHOT:compile
+        ///+- org.apache.maven:maven-settings:jar:3.6.1-SNAPSHOT:compile
+        ///+- org.apache.maven:maven-settings-builder:jar:3.6.1-SNAPSHOT:compile
+        ///|  \- org.sonatype.plexus:plexus-sec-dispatcher:jar:1.4:compile
+        ///|     \- org.sonatype.plexus:plexus-cipher:jar:1.7:compile
+        ///+- [rest of string was truncated]";.
+        /// 
+        internal static string MvnCliDependencyOutput {
+            get {
+                return ResourceManager.GetString("MvnCliDependencyOutput", resourceCulture);
+            }
+        }
+        
+        /// 
+        ///   Looks up a localized string similar to {
+        ///	"version": 3,
+        ///	"targets": {
+        ///		".NETCoreApp,Version=v2.2": {
+        ///			"CommandLineParser/2.8.0": {
+        ///				"type": "package",
+        ///				"compile": {
+        ///					"lib/netstandard2.0/_._": {}
+        ///				},
+        ///				"runtime": {
+        ///					"lib/netstandard2.0/CommandLine.dll": {}
+        ///				}
+        ///			},
+        ///			"coverlet.msbuild/2.5.1": {
+        ///				"type": "package",
+        ///				"build": {
+        ///					"build/netstandard2.0/coverlet.msbuild.props": {},
+        ///					"build/netstandard2.0/coverlet.msbuild.targets": {}
+        ///				}
+        ///			},
+        ///			"DotNet.Glob/2.1.1": {
+        ///				"type": "package [rest of string was truncated]";.
+        /// 
+        internal static string project_assets_2_2 {
+            get {
+                return ResourceManager.GetString("project_assets_2_2", resourceCulture);
+            }
+        }
+        
+        /// 
+        ///   Looks up a localized string similar to {
+        ///	"version": 3,
+        ///	"targets": {
+        ///	  ".NETCoreApp,Version=v2.2": {
+        ///		"coverlet.msbuild/2.5.1": {
+        ///		  "type": "package",
+        ///		  "build": {
+        ///			"build/netstandard2.0/coverlet.msbuild.props": {},
+        ///			"build/netstandard2.0/coverlet.msbuild.targets": {}
+        ///		  }
+        ///		},
+        ///		"DotNet.Glob/2.1.1": {
+        ///		  "type": "package",
+        ///		  "dependencies": {
+        ///			"NETStandard.Library": "1.6.1"
+        ///		  },
+        ///		  "compile": {
+        ///			"lib/netstandard1.1/DotNet.Glob.dll": {}
+        ///		  },
+        ///		  "runtime": {
+        ///			"lib/netstandard1.1/DotNet.Glob.dll": {}
        /// [rest of string was truncated]";.
+        /// 
+        internal static string project_assets_2_2_additional {
+            get {
+                return ResourceManager.GetString("project_assets_2_2_additional", resourceCulture);
+            }
+        }
+        
+        /// 
+        ///   Looks up a localized string similar to {
+        ///    "version": 3,
+        ///    "targets": {
+        ///      ".NETCoreApp,Version=v3.1": {
+        ///        "Microsoft.Extensions.DependencyModel/3.0.0": {
+        ///          "type": "package",
+        ///          "dependencies": {
+        ///            "System.Text.Json": "4.6.0"
+        ///          },
+        ///          "compile": {
+        ///            "lib/netstandard2.0/Microsoft.Extensions.DependencyModel.dll": {}
+        ///          },
+        ///          "runtime": {
+        ///            "lib/netstandard2.0/Microsoft.Extensions.DependencyModel.dll": {}
+        ///          }
+        ///        },
+        ///        "Microsoft. [rest of string was truncated]";.
+        /// 
+        internal static string project_assets_3_1 {
+            get {
+                return ResourceManager.GetString("project_assets_3_1", resourceCulture);
+            }
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/TestResources.resx b/test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/TestResources.resx
new file mode 100644
index 000000000..e8eab69ee
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/TestResources.resx
@@ -0,0 +1,136 @@
+
+
+  
+  
+    
+    
+      
+        
+          
+            
+              
+                
+              
+              
+              
+              
+              
+            
+          
+          
+            
+              
+              
+            
+          
+          
+            
+              
+                
+                
+              
+              
+              
+              
+              
+            
+          
+          
+            
+              
+                
+              
+              
+            
+          
+        
+      
+    
+  
+  
+    text/microsoft-resx
+  
+  
+    2.0
+  
+  
+    System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+  
+  
+    System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+  
+  
+  
+    GradlewDependencyOutput.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8
+  
+  
+    MvnCliDependencyOutput.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8
+  
+  
+    ..\Resources\project_assets_2_2.json;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252
+  
+  
+    ..\Resources\project_assets_2_2_additional.json;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252
+  
+  
+    ..\Resources\project_assets_3_1.json;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252
+  
+
\ No newline at end of file
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/MvnCliDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/MvnCliDetectorTests.cs
new file mode 100644
index 000000000..0481b9679
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/MvnCliDetectorTests.cs
@@ -0,0 +1,207 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Common.DependencyGraph;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Detectors.Maven;
+using Microsoft.ComponentDetection.Detectors.Tests.Utilities;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Microsoft.ComponentDetection.TestsUtilities;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class MvnCliDetectorTests
+    {
+        private IMavenCommandService mavenCommandService;
+        private Mock commandLineMock;
+        private DetectorTestUtility detectorTestUtility;
+        private ScanRequest scanRequest;
+
+        [TestInitialize]
+        public void InitializeTests()
+        {
+            commandLineMock = new Mock();
+            mavenCommandService = new MavenCommandService 
+            {
+                CommandLineInvocationService = commandLineMock.Object,
+                ParserService = new MavenStyleDependencyGraphParserService(),
+            };
+
+            var loggerMock = new Mock();
+
+            var detector = new MvnCliComponentDetector
+            {
+                MavenCommandService = mavenCommandService,
+                Logger = loggerMock.Object,
+            };
+
+            var tempPath = Path.GetTempPath();
+            var detectionPath = Path.Combine(tempPath, Guid.NewGuid().ToString());
+            Directory.CreateDirectory(detectionPath);
+
+            scanRequest = new ScanRequest(new DirectoryInfo(detectionPath), (name, directoryName) => false, loggerMock.Object, null, null, new ComponentRecorder());
+
+            detectorTestUtility = DetectorTestUtilityCreator.Create()
+                                                            .WithScanRequest(scanRequest)
+                                                            .WithDetector(detector);
+        }
+
+        [TestCleanup]
+        public void TestCleanup()
+        {
+            scanRequest.SourceDirectory.Delete();
+        }
+
+        [TestMethod]
+        public async Task IfMavenIsNotAvailableThenExitDetectorGracefully()
+        {
+            commandLineMock.Setup(x => x.CanCommandBeLocated(
+                MavenCommandService.PrimaryCommand,
+                MavenCommandService.AdditionalValidCommands,
+                MavenCommandService.MvnVersionArgument)).ReturnsAsync(false);
+
+            var (detectorResult, componentRecorder) = await detectorTestUtility.ExecuteDetector();
+
+            Assert.AreEqual(componentRecorder.GetDetectedComponents().Count(), 0);
+            Assert.AreEqual(detectorResult.ResultCode, ProcessingResultCode.Success);
+        }
+
+        [TestMethod]
+        public async Task MavenAvailableHappyPath()
+        {
+            const string componentString = "org.apache.maven:maven-compat:jar:3.6.1-SNAPSHOT";
+
+            MvnCliHappyPath(content: componentString);
+
+            var (detectorResult, componentRecorder) = await detectorTestUtility.ExecuteDetector();
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(detectedComponents.Count(), 1);
+            Assert.AreEqual(detectorResult.ResultCode, ProcessingResultCode.Success);
+
+            var mavenComponent = detectedComponents.First().Component as MavenComponent;
+            var splitComponent = componentString.Split(':');
+            Assert.AreEqual(splitComponent[0], mavenComponent.GroupId);
+            Assert.AreEqual(splitComponent[1], mavenComponent.ArtifactId);
+            Assert.AreEqual(splitComponent[3], mavenComponent.Version);
+            Assert.AreEqual(ComponentType.Maven, mavenComponent.Type);
+        }
+
+        [TestMethod]
+        public async Task MavenCli_FileObservableIsNotPresent_DetectionShouldNotFail()
+        {
+            commandLineMock.Setup(x => x.CanCommandBeLocated(
+                MavenCommandService.PrimaryCommand,
+                MavenCommandService.AdditionalValidCommands,
+                MavenCommandService.MvnVersionArgument)).ReturnsAsync(true);
+
+            Func action = async () => await detectorTestUtility.ExecuteDetector();
+
+            await action.Should().NotThrowAsync();
+        }
+
+        [TestMethod]
+        public async Task MavenRoots()
+        {
+            const string componentString = "org.apache.maven:maven-compat:jar:3.6.1-SNAPSHOT";
+            const string childComponentString = "org.apache.maven:maven-compat-child:jar:3.6.1-SNAPSHOT";
+
+            string content = $@"com.bcde.test:top-level:jar:1.0.0{Environment.NewLine}\- {componentString}{Environment.NewLine} \- {childComponentString}";
+
+            MvnCliHappyPath(content);
+
+            var (detectorResult, componentRecorder) = await detectorTestUtility
+                                                   .ExecuteDetector();
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(detectedComponents.Count(), 3);
+            Assert.AreEqual(detectorResult.ResultCode, ProcessingResultCode.Success);
+
+            var splitComponent = componentString.Split(':');
+            var splitChildComponent = childComponentString.Split(':');
+
+            var mavenComponent = detectedComponents.FirstOrDefault(x => (x.Component as MavenComponent).ArtifactId == splitChildComponent[1]);
+            Assert.IsNotNull(mavenComponent);
+
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                mavenComponent.Component.Id,
+                parentComponent => parentComponent.ArtifactId == splitComponent[1]);
+        }
+
+        [TestMethod]
+        public async Task MavenDependencyGraph()
+        {
+            const string explicitReferencedComponent = "org.apache.maven:maven-compat:jar:3.6.1-SNAPSHOT";
+
+            const string intermediateParentComponent = "org.apache.maven:maven-compat-parent:jar:3.6.1-SNAPSHOT";
+
+            const string leafComponentString = "org.apache.maven:maven-compat-child:jar:3.6.1-SNAPSHOT";
+
+            string content = $@"com.bcde.test:top-level:jar:1.0.0
+\- {explicitReferencedComponent}
+    \- {intermediateParentComponent}
+        \-{leafComponentString}";
+
+            const string explicitReferencedComponentId = "org.apache.maven maven-compat 3.6.1-SNAPSHOT - Maven";
+            const string intermediateParentComponentId = "org.apache.maven maven-compat-parent 3.6.1-SNAPSHOT - Maven";
+            const string leafComponentId = "org.apache.maven maven-compat-child 3.6.1-SNAPSHOT - Maven";
+
+            MvnCliHappyPath(content);
+
+            var (detectorResult, componentRecorder) = await detectorTestUtility.ExecuteDetector();
+
+            componentRecorder.GetDetectedComponents().Should().HaveCount(4);
+            detectorResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+
+            // There is only one graph
+            var dependencyGraph = componentRecorder.GetDependencyGraphsByLocation().Values.First();
+
+            dependencyGraph.GetDependenciesForComponent(explicitReferencedComponentId).Should().HaveCount(1);
+            dependencyGraph.GetDependenciesForComponent(explicitReferencedComponentId).Should().Contain(intermediateParentComponentId);
+            dependencyGraph.IsComponentExplicitlyReferenced(explicitReferencedComponentId).Should().BeTrue();
+
+            dependencyGraph.GetDependenciesForComponent(intermediateParentComponentId).Should().HaveCount(1);
+            dependencyGraph.GetDependenciesForComponent(intermediateParentComponentId).Should().Contain(leafComponentId);
+            dependencyGraph.IsComponentExplicitlyReferenced(intermediateParentComponentId).Should().BeFalse();
+
+            dependencyGraph.GetDependenciesForComponent(leafComponentId).Should().HaveCount(0);
+            dependencyGraph.IsComponentExplicitlyReferenced(leafComponentId).Should().BeFalse();
+        }
+
+        private void MvnCliHappyPath(string content)
+        {
+            commandLineMock.Setup(x => x.CanCommandBeLocated(MavenCommandService.PrimaryCommand, MavenCommandService.AdditionalValidCommands, MavenCommandService.MvnVersionArgument)).ReturnsAsync(true);
+
+            var expectedPomLocation = scanRequest.SourceDirectory.FullName;
+
+            var bcdeMvnFileName = "bcde.mvndeps";
+            detectorTestUtility.WithFile("pom.xml", content, fileLocation: expectedPomLocation)
+                                .WithFile("pom.xml", content, searchPatterns: new[] { bcdeMvnFileName }, fileLocation: Path.Combine(expectedPomLocation, "pom.xml"));
+
+            var cliParameters = new[] { "dependency:tree", "-B", $"-DoutputFile={bcdeMvnFileName}", "-DoutputType=text", $"-f{expectedPomLocation}" };
+
+            commandLineMock.Setup(x => x.ExecuteCommand(
+                                                        MavenCommandService.PrimaryCommand,
+                                                        MavenCommandService.AdditionalValidCommands,
+                                                        It.Is(y => ShouldBeEquivalentTo(y, cliParameters))))
+                .ReturnsAsync(new CommandLineExecutionResult
+                {
+                    ExitCode = 0,
+                });
+        }
+
+        protected bool ShouldBeEquivalentTo(IEnumerable result, IEnumerable expected)
+        {
+            result.Should().BeEquivalentTo(expected);
+            return true;
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/NpmDetectorWithRootsTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/NpmDetectorWithRootsTests.cs
new file mode 100644
index 000000000..7035ac2df
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/NpmDetectorWithRootsTests.cs
@@ -0,0 +1,634 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Common.DependencyGraph;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Detectors.Npm;
+using Microsoft.ComponentDetection.Detectors.Tests.Utilities;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Microsoft.ComponentDetection.TestsUtilities;
+
+using static Microsoft.ComponentDetection.Detectors.Tests.Utilities.TestUtilityExtensions;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class NpmDetectorWithRootsTests
+    {
+        private Mock loggerMock;
+        private Mock pathUtilityService;
+        private ComponentRecorder componentRecorder;
+        private DetectorTestUtility detectorTestUtility = DetectorTestUtilityCreator.Create();
+        private string packageLockJsonFileName = "package-lock.json";
+        private string packageJsonFileName = "package.json";
+        private List packageJsonSearchPattern = new List { "package.json" };
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            loggerMock = new Mock();
+            pathUtilityService = new Mock();
+            pathUtilityService.Setup(x => x.GetParentDirectory(It.IsAny())).Returns((string path) => Path.GetDirectoryName(path));
+            componentRecorder = new ComponentRecorder();
+        }
+
+        [TestMethod]
+        public async Task TestNpmDetector_PackageLockReturnsValid()
+        {
+            string componentName0 = Guid.NewGuid().ToString("N");
+            string version0 = NewRandomVersion();
+
+            var (packageLockName, packageLockContents, packageLockPath) = NpmTestUtilities.GetWellFormedPackageLock2(packageLockJsonFileName, componentName0, version0);
+            var (packageJsonName, packageJsonContents, packageJsonPath) = NpmTestUtilities.GetPackageJsonOneRoot(componentName0, version0);
+
+            var detector = new NpmComponentDetectorWithRoots();
+            detector.PathUtilityService = pathUtilityService.Object;
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithDetector(detector)
+                                                    .WithFile(packageLockName, packageLockContents, detector.SearchPatterns, fileLocation: packageLockPath)
+                                                    .WithFile(packageJsonName, packageJsonContents, packageJsonSearchPattern, fileLocation: packageJsonPath)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(4, detectedComponents.Count());
+            foreach (var component in detectedComponents)
+            {
+                componentRecorder.AssertAllExplicitlyReferencedComponents(
+                    component.Component.Id,
+                    parentComponent0 => parentComponent0.Name == componentName0 && parentComponent0.Version == version0);
+                Assert.IsFalse(string.IsNullOrWhiteSpace(((NpmComponent)component.Component).Hash));
+            }
+        }
+
+        [TestMethod]
+        public async Task TestNpmDetector_MismatchedFilesReturnsEmpty()
+        {
+            string componentName0 = Guid.NewGuid().ToString("N");
+            string version0 = NewRandomVersion();
+
+            var (packageLockName, packageLockContents, packageLockPath) = NpmTestUtilities.GetWellFormedPackageLock2(packageLockJsonFileName);
+            var (packageJsonName, packageJsonContents, packageJsonPath) = NpmTestUtilities.GetPackageJsonOneRoot(componentName0, version0);
+
+            var detector = new NpmComponentDetectorWithRoots();
+            detector.PathUtilityService = pathUtilityService.Object;
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithDetector(detector)
+                                                    .WithFile(packageLockName, packageLockContents, detector.SearchPatterns, fileLocation: packageLockPath)
+                                                    .WithFile(packageJsonName, packageJsonContents, packageJsonSearchPattern, fileLocation: packageJsonPath)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(0, componentRecorder.GetDetectedComponents().Count());
+        }
+
+        [TestMethod]
+        public async Task TestNpmDetector_MissingPackageJsonReturnsEmpty()
+        {
+            var (packageLockName, packageLockContents, packageLockPath) = NpmTestUtilities.GetWellFormedPackageLock2(packageLockJsonFileName);
+
+            var detector = new NpmComponentDetectorWithRoots();
+            detector.PathUtilityService = pathUtilityService.Object;
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithDetector(detector)
+                                                    .WithFile(packageLockName, packageLockContents, detector.SearchPatterns, fileLocation: packageLockPath)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(0, componentRecorder.GetDetectedComponents().Count());
+        }
+
+        [TestMethod]
+        public async Task TestNpmDetector_PackageLockMultiRoot()
+        {
+            string componentName0 = Guid.NewGuid().ToString("N");
+            string version0 = NewRandomVersion();
+            string componentName1 = Guid.NewGuid().ToString("N");
+            string componentName2 = Guid.NewGuid().ToString("N");
+            string version2 = NewRandomVersion();
+            string componentName3 = Guid.NewGuid().ToString("N");
+
+            var (packageLockName, packageLockContents, packageLockPath) = NpmTestUtilities.GetWellFormedPackageLock2(packageLockJsonFileName, componentName0, version0, componentName2, version2, packageName1: componentName1, packageName3: componentName3);
+
+            string packagejson = @"{{
+                ""name"": ""test"",
+                ""version"": ""0.0.0"",
+                ""dependencies"": {{
+                    ""{0}"": ""{1}"",
+                    ""{2}"": ""{3}""
+                }}
+            }}";
+
+            var packageJsonTemplate = string.Format(packagejson, componentName0, version0, componentName2, version2);
+
+            var detector = new NpmComponentDetectorWithRoots();
+            detector.PathUtilityService = pathUtilityService.Object;
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithDetector(detector)
+                                                    .WithFile(packageLockName, packageLockContents, detector.SearchPatterns)
+                                                    .WithFile(packageJsonFileName, packageJsonTemplate, packageJsonSearchPattern)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(4, detectedComponents.Count());
+
+            var component0 = detectedComponents.FirstOrDefault(x => x.Component.Id.Contains(componentName0));
+
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                    component0.Component.Id,
+                    parentComponent0 => parentComponent0.Name == componentName0);
+
+            var component1 = detectedComponents.FirstOrDefault(x => x.Component.Id.Contains(componentName1));
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                    component1.Component.Id,
+                    parentComponent0 => parentComponent0.Name == componentName0);
+
+            var component2 = detectedComponents.FirstOrDefault(x => x.Component.Id.Contains(componentName2));
+
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                    component2.Component.Id,
+                    parentComponent0 => parentComponent0.Name == componentName0,
+                    parentComponent2 => parentComponent2.Name == componentName2);
+
+            var component3 = detectedComponents.FirstOrDefault(x => x.Component.Id.Contains(componentName3));
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                    component3.Component.Id,
+                    parentComponent0 => parentComponent0.Name == componentName0,
+                    parentComponent2 => parentComponent2.Name == componentName2);
+        }
+
+        [TestMethod]
+        public async Task TestNpmDetector_VerifyMultiRoot_DependencyGraph()
+        {
+            string componentName0 = Guid.NewGuid().ToString("N");
+            string version0 = NewRandomVersion();
+            string componentName2 = Guid.NewGuid().ToString("N");
+            string version2 = NewRandomVersion();
+
+            var (packageLockName, packageLockContents, packageLockPath) = NpmTestUtilities.GetWellFormedPackageLock2(packageLockJsonFileName, componentName0, version0, componentName2, version2);
+
+            string packagejson = @"{{
+                ""name"": ""test"",
+                ""version"": ""0.0.0"",
+                ""dependencies"": {{
+                    ""{0}"": ""{1}"",
+                    ""{2}"": ""{3}""
+                }}
+            }}";
+
+            var packageJsonTemplate = string.Format(packagejson, componentName0, version0, componentName2, version2);
+
+            var detector = new NpmComponentDetectorWithRoots();
+            detector.PathUtilityService = pathUtilityService.Object;
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithDetector(detector)
+                                                    .WithFile(packageLockName, packageLockContents, detector.SearchPatterns, fileLocation: packageLockPath)
+                                                    .WithFile(packageJsonFileName, packageJsonTemplate, packageJsonSearchPattern)
+                                                    .ExecuteDetector();
+
+            var graphsByLocation = componentRecorder.GetDependencyGraphsByLocation();
+
+            var graph = graphsByLocation[packageLockPath];
+
+            var npmComponent0Id = new NpmComponent(componentName0, version0).Id;
+            var npmComponent2Id = new NpmComponent(componentName2, version2).Id;
+
+            var dependenciesFor0 = graph.GetDependenciesForComponent(npmComponent0Id);
+            Assert.AreEqual(dependenciesFor0.Count(), 2);
+            var dependenciesFor2 = graph.GetDependenciesForComponent(npmComponent2Id);
+            Assert.AreEqual(dependenciesFor2.Count(), 1);
+
+            Assert.IsTrue(dependenciesFor0.Contains(npmComponent2Id));
+        }
+
+        [TestMethod]
+        public async Task TestNpmDetector_EmptyVersionSkipped()
+        {
+            string componentName0 = Guid.NewGuid().ToString("N");
+            string version0 = NewRandomVersion();
+            string componentName2 = Guid.NewGuid().ToString("N");
+            string version2 = NewRandomVersion();
+
+            string packageLockJson = @"{{
+                ""name"": ""test"",
+                ""version"": """",
+                ""dependencies"": {{
+                    ""{0}"": {{
+                        ""version"": ""{1}"",
+                        ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/"",
+                        ""integrity"": ""sha1-EBPRBRBH3TIP4k5JTVxm7K9hR9k="",
+                        ""requires"": {{
+                                ""{2}"": ""{3}""
+                        }}
+                    }},
+                    ""{4}"": {{
+                        ""version"": ""{5}"",
+                        ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/"",
+                        ""integrity"": ""sha1-PRT306DRK/NZUaVL07iuqH7nWPg="",
+                        ""requires"": {{
+                                ""{6}"": ""{7}""
+                        }}
+                    }}
+                }}
+            }}";
+
+            var packageLockTemplate = string.Format(packageLockJson, componentName0, version0, componentName2, version2, componentName2, version2, componentName0, version0);
+
+            string packagejson = @"{{
+                ""name"": ""test"",
+                ""version"": ""0.0.0"",
+                ""dependencies"": {{
+                    ""{0}"": ""{1}"",
+                    ""{2}"": ""{3}""
+                }}
+            }}";
+
+            var packageJsonTemplate = string.Format(packagejson, componentName0, version0, componentName2, version2);
+
+            var detector = new NpmComponentDetectorWithRoots();
+            detector.PathUtilityService = pathUtilityService.Object;
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithDetector(detector)
+                                                    .WithFile(packageLockJsonFileName, packageLockTemplate, detector.SearchPatterns)
+                                                    .WithFile(packageJsonFileName, packageJsonTemplate, packageJsonSearchPattern)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(0, componentRecorder.GetDetectedComponents().Count());
+        }
+
+        [TestMethod]
+        public async Task TestNpmDetector_InvalidNameSkipped()
+        {
+            string componentName0 = Guid.NewGuid().ToString("N");
+            string version0 = NewRandomVersion();
+            string componentName2 = Guid.NewGuid().ToString("N");
+            string version2 = NewRandomVersion();
+
+            string packageLockJson = @"{{
+                ""name"": """",
+                ""version"": ""1.0.0"",
+                ""dependencies"": {{
+                    ""{0}"": {{
+                        ""version"": ""{1}"",
+                        ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/"",
+                        ""integrity"": ""sha1-EBPRBRBH3TIP4k5JTVxm7K9hR9k="",
+                        ""requires"": {{
+                                ""{2}"": ""{3}""
+                        }}
+                    }},
+                    ""{4}"": {{
+                        ""version"": ""{5}"",
+                        ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/"",
+                        ""integrity"": ""sha1-PRT306DRK/NZUaVL07iuqH7nWPg="",
+                        ""requires"": {{
+                                ""{6}"": ""{7}""
+                        }}
+                    }}
+                }}
+            }}";
+
+            var packageLockTemplate = string.Format(packageLockJson, componentName0, version0, componentName2, version2, componentName2, version2, componentName0, version0);
+
+            string packagejson = @"{{
+                ""name"": ""test"",
+                ""version"": ""0.0.0"",
+                ""dependencies"": {{
+                    ""{0}"": ""{1}"",
+                    ""{2}"": ""{3}""
+                }}
+            }}";
+
+            var packageJsonTemplate = string.Format(packagejson, componentName0, version0, componentName2, version2);
+
+            var detector = new NpmComponentDetectorWithRoots();
+            detector.PathUtilityService = pathUtilityService.Object;
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithDetector(detector)
+                                                    .WithFile(packageLockJsonFileName, packageLockTemplate, detector.SearchPatterns)
+                                                    .WithFile(packageJsonFileName, packageJsonTemplate, packageJsonSearchPattern)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(0, componentRecorder.GetDetectedComponents().Count());
+        }
+
+        [TestMethod]
+        public async Task TestNpmDetector_LernaDirectory()
+        {
+            string lockFileLocation = Path.Combine(Path.GetTempPath(), Path.Combine("belowLerna", packageLockJsonFileName));
+            string packageJsonFileLocation = Path.Combine(Path.GetTempPath(), Path.Combine("belowLerna", packageJsonFileName));
+            string lernaFileLocation = Path.Combine(Path.GetTempPath(), "lerna.json");
+
+            string componentName0 = Guid.NewGuid().ToString("N");
+            string version0 = NewRandomVersion();
+            string componentName1 = Guid.NewGuid().ToString("N");
+            string version1 = NewRandomVersion();
+            string componentName2 = Guid.NewGuid().ToString("N");
+            string version2 = NewRandomVersion();
+
+            string packageLockJson = @"{{
+                ""name"": """",
+                ""version"": """",
+                ""dependencies"": {{
+                    ""{0}"": {{
+                        ""version"": ""{1}"",
+                        ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/"",
+                        ""integrity"": ""sha1-EBPRBRBH3TIP4k5JTVxm7K9hR9k="",
+                        ""requires"": {{
+                                ""{2}"": ""{3}""
+                        }}
+                    }},
+                    ""{4}"": {{
+                        ""version"": ""{5}"",
+                        ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/"",
+                        ""integrity"": ""sha1-PRT306DRK/NZUaVL07iuqH7nWPg="",
+                        ""requires"": {{
+                                ""{6}"": ""{7}""
+                        }}
+                    }}
+                }}
+            }}";
+
+            var packageLockTemplate = string.Format(packageLockJson, componentName0, version0, componentName2, version2, componentName2, version2, componentName0, version0);
+
+            string packagejson = @"{{
+                ""name"": ""test"",
+                ""version"": ""0.0.0"",
+                ""dependencies"": {{
+                    ""{0}"": ""{1}"",
+                    ""{2}"": ""{3}"",
+                    ""{4}"": ""{5}""
+                }}
+            }}";
+
+            var packageJsonTemplate = string.Format(packagejson, componentName0, version0, componentName1, version1, componentName2, version2);
+
+            var detector = new NpmComponentDetectorWithRoots();
+            pathUtilityService.Setup(x => x.IsFileBelowAnother(It.IsAny(), It.IsAny())).Returns(true);
+            detector.PathUtilityService = pathUtilityService.Object;
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                        .WithDetector(detector)
+                                        .WithFile("lerna.json", "unused string", detector.SearchPatterns, fileLocation: lernaFileLocation)
+                                        .WithFile(packageLockJsonFileName, packageLockTemplate, detector.SearchPatterns, fileLocation: lockFileLocation)
+                                        .WithFile(packageJsonFileName, packageJsonTemplate, packageJsonSearchPattern, fileLocation: packageJsonFileLocation)
+                                        .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(2, componentRecorder.GetDetectedComponents().Count());
+        }
+
+        [TestMethod]
+        public async Task TestNpmDetector_CircularRequirementsResolve()
+        {
+            string packageJsonComponentPath = Path.Combine(Path.GetTempPath(), packageLockJsonFileName);
+
+            string componentName0 = Guid.NewGuid().ToString("N");
+            string version0 = NewRandomVersion();
+            string componentName2 = Guid.NewGuid().ToString("N");
+            string version2 = NewRandomVersion();
+
+            string packageLockJson = @"{{
+                ""name"": ""test"",
+                ""version"": ""0.0.0"",
+                ""dependencies"": {{
+                    ""{0}"": {{
+                        ""version"": ""{1}"",
+                        ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/"",
+                        ""integrity"": ""sha1-EBPRBRBH3TIP4k5JTVxm7K9hR9k="",
+                        ""requires"": {{
+                                ""{2}"": ""{3}""
+                        }}
+                    }},
+                    ""{4}"": {{
+                        ""version"": ""{5}"",
+                        ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/"",
+                        ""integrity"": ""sha1-PRT306DRK/NZUaVL07iuqH7nWPg="",
+                        ""requires"": {{
+                                ""{6}"": ""{7}""
+                        }}
+                    }}
+                }}
+            }}";
+
+            var packageLockTemplate = string.Format(packageLockJson, componentName0, version0, componentName2, version2, componentName2, version2, componentName0, version0);
+
+            string packagejson = @"{{
+                ""name"": ""test"",
+                ""version"": ""0.0.0"",
+                ""dependencies"": {{
+                    ""{0}"": ""{1}"",
+                    ""{2}"": ""{3}""
+                }}
+            }}";
+
+            var packageJsonTemplate = string.Format(packagejson, componentName0, version0, componentName2, version2);
+
+            var detector = new NpmComponentDetectorWithRoots();
+            detector.PathUtilityService = pathUtilityService.Object;
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithDetector(detector)
+                                                    .WithFile(packageLockJsonFileName, packageLockTemplate, detector.SearchPatterns)
+                                                    .WithFile(packageJsonFileName, packageJsonTemplate, packageJsonSearchPattern)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(2, detectedComponents.Count());
+
+            foreach (var component in detectedComponents)
+            {
+                componentRecorder.AssertAllExplicitlyReferencedComponents(
+                    component.Component.Id,
+                    parentComponent0 => parentComponent0.Name == componentName0,
+                    parentComponent2 => parentComponent2.Name == componentName2);
+            }
+        }
+
+        [TestMethod]
+        public async Task TestNpmDetector_ShrinkwrapLockReturnsValid()
+        {
+            string lockFileName = "npm-shrinkwrap.json";
+            string packageJsonComponentPath = Path.Combine(Path.GetTempPath(), packageJsonFileName);
+
+            string componentName0 = Guid.NewGuid().ToString("N");
+            string version0 = NewRandomVersion();
+
+            var (packageLockName, packageLockContents, packageLockPath) = NpmTestUtilities.GetWellFormedPackageLock2(lockFileName, componentName0, version0);
+            var (packageJsonName, packageJsonContents, packageJsonPath) = NpmTestUtilities.GetPackageJsonOneRoot(componentName0, version0);
+
+            var detector = new NpmComponentDetectorWithRoots();
+            detector.PathUtilityService = pathUtilityService.Object;
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithDetector(detector)
+                                                    .WithFile(packageLockName, packageLockContents, detector.SearchPatterns, fileLocation: packageLockPath)
+                                                    .WithFile(packageJsonFileName, packageJsonContents, packageJsonSearchPattern)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(4, detectedComponents.Count());
+            foreach (var component in detectedComponents)
+            {
+                componentRecorder.AssertAllExplicitlyReferencedComponents(
+                    component.Component.Id,
+                    parentComponent0 => parentComponent0.Name == componentName0 && parentComponent0.Version == version0);
+            }
+        }
+
+        [TestMethod]
+        public async Task TestNpmDetector_IgnoresPackageLocksInSubFolders()
+        {
+            string pathRoot = Path.GetTempPath();
+
+            string packageLockUnderNodeModules = Path.Combine(pathRoot, Path.Combine("node_modules", packageLockJsonFileName));
+            string packageJsonUnderNodeModules = Path.Combine(pathRoot, Path.Combine("node_modules", packageJsonFileName));
+
+            string componentName0 = Guid.NewGuid().ToString("N");
+            string version0 = NewRandomVersion();
+            string componentName2 = Guid.NewGuid().ToString("N");
+            string version2 = NewRandomVersion();
+
+            var (packageLockName, packageLockContents, packageLockPath) = NpmTestUtilities.GetWellFormedPackageLock2(packageLockJsonFileName, componentName0, version0);
+            var (packageLockName2, packageLockContents2, packageLockPath2) = NpmTestUtilities.GetWellFormedPackageLock2(packageLockJsonFileName, componentName2, version2, packageName0: "test2");
+
+            string packagejson = @"{{
+                ""name"": ""{2}"",
+                ""version"": ""0.0.0"",
+                ""dependencies"": {{
+                    ""{0}"": ""{1}""
+                }}
+            }}";
+
+            var packageJsonTemplate = string.Format(packagejson, componentName0, version0, "test");
+
+            var packageJsonTemplate2 = string.Format(packagejson, componentName2, version2, "test2");
+
+            var detector = new NpmComponentDetectorWithRoots();
+            detector.PathUtilityService = pathUtilityService.Object;
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithDetector(detector)
+                                                    /* Top level */
+                                                    .WithFile(packageLockName, packageLockContents, detector.SearchPatterns, fileLocation: packageLockPath)
+                                                    .WithFile(packageJsonFileName, packageJsonTemplate, packageJsonSearchPattern)
+                                                    /* Under node_modules */
+                                                    .WithFile(packageLockName2, packageLockContents2, detector.SearchPatterns, fileLocation: packageLockUnderNodeModules)
+                                                    .WithFile(packageJsonFileName, packageJsonTemplate2, packageJsonSearchPattern, fileLocation: packageJsonUnderNodeModules)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(4, detectedComponents.Count());
+            foreach (var component in detectedComponents)
+            {
+                componentRecorder.AssertAllExplicitlyReferencedComponents(
+                    component.Component.Id,
+                    parentComponent0 => parentComponent0.Name == componentName0);
+            }
+        }
+
+        [TestMethod]
+        public async Task TestNpmDetector_DependencyGraphIsCreated()
+        {
+            string packageJsonComponentPath = Path.Combine(Path.GetTempPath(), packageLockJsonFileName);
+
+            var componentA = (Name: "componentA", Version: "1.0.0");
+            var componentB = (Name: "componentB", Version: "1.0.0");
+            var componentC = (Name: "componentC", Version: "1.0.0");
+
+            string packageLockJson = @"{{
+                ""name"": ""test"",
+                ""version"": ""0.0.0"",
+                ""dependencies"": {{
+                    ""{0}"": {{
+                        ""version"": ""{1}"",
+                        ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/"",
+                        ""integrity"": ""sha1-EBPRBRBH3TIP4k5JTVxm7K9hR9k="",
+                        ""requires"": {{
+                                ""{2}"": ""{3}""
+                        }}
+                    }},
+                    ""{4}"": {{
+                        ""version"": ""{5}"",
+                        ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/"",
+                        ""integrity"": ""sha1-PRT306DRK/NZUaVL07iuqH7nWPg="",
+                        ""dependencies"": {{
+                            ""{6}"": {{
+                                ""version"": ""{7}"",
+                                ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/"",
+                                ""integrity"": ""sha1-PRT306DRK/NZUaVL07iuqH7nWPg=""
+                            }}
+                        }}
+                    }}
+                }}
+            }}";
+
+            var packageLockTemplate = string.Format(
+                packageLockJson,
+                componentA.Name, componentA.Version,
+                componentB.Name, componentB.Version,
+                componentB.Name, componentB.Version,
+                componentC.Name, componentC.Version);
+
+            string packagejson = @"{{
+                ""name"": ""test"",
+                ""version"": ""0.0.0"",
+                ""dependencies"": {{
+                    ""{0}"": ""{1}"",
+                    ""{2}"": ""{3}""
+                }}
+            }}";
+
+            var packageJsonTemplate = string.Format(packagejson, componentA.Name, componentA.Version, componentB.Name, componentB.Version);
+
+            var detector = new NpmComponentDetectorWithRoots();
+            detector.PathUtilityService = pathUtilityService.Object;
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithDetector(detector)
+                                                    .WithFile(packageLockJsonFileName, packageLockTemplate, detector.SearchPatterns)
+                                                    .WithFile(packageJsonFileName, packageJsonTemplate, packageJsonSearchPattern)
+                                                    .ExecuteDetector();
+
+            scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            detectedComponents.Should().HaveCount(3);
+
+            var componentAId = detectedComponents.First(c => ((NpmComponent)c.Component).Name.Equals(componentA.Name)).Component.Id;
+            var componentBId = detectedComponents.First(c => ((NpmComponent)c.Component).Name.Equals(componentB.Name)).Component.Id;
+            var componentCId = detectedComponents.First(c => ((NpmComponent)c.Component).Name.Equals(componentC.Name)).Component.Id;
+
+            var dependencyGraph = componentRecorder.GetDependencyGraphsByLocation().Values.First();
+
+            dependencyGraph.GetDependenciesForComponent(componentAId).Should().HaveCount(1);
+            dependencyGraph.GetDependenciesForComponent(componentAId).Should().Contain(componentBId);
+            dependencyGraph.GetDependenciesForComponent(componentBId).Should().HaveCount(1);
+            dependencyGraph.GetDependenciesForComponent(componentBId).Should().Contain(componentCId);
+            dependencyGraph.GetDependenciesForComponent(componentCId).Should().HaveCount(0);
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/NpmTestUtilities.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/NpmTestUtilities.cs
new file mode 100644
index 000000000..41ab83fa8
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/NpmTestUtilities.cs
@@ -0,0 +1,143 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reactive.Linq;
+using Microsoft.ComponentDetection.Common.DependencyGraph;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.Internal;
+using Moq;
+using Microsoft.ComponentDetection.TestsUtilities;
+
+using static Microsoft.ComponentDetection.Detectors.Tests.Utilities.TestUtilityExtensions;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    public static class NpmTestUtilities
+    {
+        public static string GetPackageJsonNoDependencies()
+        {
+            string packagejson = @"{{
+                ""name"": ""test"",
+                ""version"": ""0.0.0""
+            }}";
+
+            return packagejson;
+        }
+
+        public static IComponentStream GetPackageJsonOneRootComponentStream(string componentName0, string version0)
+        {
+            string packagejson = @"{{
+                ""name"": ""test"",
+                ""version"": ""0.0.0"",
+                ""dependencies"": {{
+                    ""{0}"": ""{1}""
+                }}
+            }}";
+
+            var packageJsonTemplate = string.Format(packagejson, componentName0, version0);
+
+            return GetMockedPackageJsonStream(packageJsonTemplate);
+        }
+
+        public static IComponentStream GetMockedPackageJsonStream(string content)
+        {
+            var packageJsonMock = new Mock();
+            packageJsonMock.SetupGet(x => x.Stream).Returns(content.ToStream());
+            packageJsonMock.SetupGet(x => x.Pattern).Returns("package.json");
+            packageJsonMock.SetupGet(x => x.Location).Returns(Path.Combine(Path.GetTempPath(), "package.json"));
+
+            return packageJsonMock.Object;
+        }
+
+        public static Mock GetMockDirectoryWalker(IEnumerable packageLockStreams, IEnumerable packageJsonStreams, string directoryName, IEnumerable lernaJsonStreams = null, IEnumerable patterns = null, IEnumerable lernaPatterns = null, IComponentRecorder componentRecorder = null)
+        {
+            var mock = new Mock();
+            var components = new List();
+            if (componentRecorder == null)
+            {
+                componentRecorder = new ComponentRecorder();
+            }
+
+            if (lernaJsonStreams != null && lernaPatterns != null)
+            {
+                components.AddRange(lernaJsonStreams);
+            }
+
+            components.AddRange(packageLockStreams);
+            components.AddRange(packageJsonStreams);
+
+            mock.Setup(x => x.GetFilteredComponentStreamObservable(It.IsAny(), It.IsAny>(), It.IsAny())).Returns(() => components
+            .Select(cs => new ProcessRequest
+            {
+                ComponentStream = cs,
+                SingleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder(cs.Location),
+            }).ToObservable());
+
+            return mock;
+        }
+
+        public static (string, string, string) GetPackageJsonOneRoot(string componentName0, string version0)
+        {
+            string packagejson = @"{{
+                ""name"": ""test"",
+                ""version"": ""0.0.0"",
+                ""dependencies"": {{
+                    ""{0}"": ""{1}""
+                }}
+            }}";
+
+            var packageJsonTemplate = string.Format(packagejson, componentName0, version0);
+
+            return ("package.json", packageJsonTemplate, Path.Combine(Path.GetTempPath(), "package.json"));
+        }
+
+        public static (string, string, string) GetWellFormedPackageLock2(string lockFileName, string rootName0 = null, string rootVersion0 = null, string rootName2 = null, string rootVersion2 = null, string packageName0 = "test", string packageName1 = null, string packageName3 = null)
+        {
+            string packageLockJson = @"{{
+                ""name"": ""{10}"",
+                ""version"": ""0.0.0"",
+                ""dependencies"": {{
+                    ""{0}"": {{
+                        ""version"": ""{1}"",
+                        ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/"",
+                        ""integrity"": ""sha1-EBPRBRBH3TIP4k5JTVxm7K9hR9k="",
+                        ""dependencies"": {{
+                                ""{2}"": {{
+                                    ""version"": ""{3}"",
+                                    ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/"",
+                                    ""integrity"": ""sha1-PRT306DRK/NZUaVL07iuqH7nWPg=""
+                                }}
+                        }},
+                        ""requires"": {{
+                                ""{4}"": ""{5}""
+                        }}
+                    }},
+                    ""{6}"": {{
+                        ""version"": ""{7}"",
+                        ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/"",
+                        ""integrity"": ""sha1-PRT306DRK/NZUaVL07iuqH7nWPg="",
+                        ""dependencies"": {{
+                                ""{8}"": {{
+                                    ""version"": ""{9}"",
+                                    ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/"",
+                                    ""integrity"": ""sha1-PRT306DRK/NZUaVL07iuqH7nWPg=""
+                                }}
+                        }}
+                    }}
+                }}
+            }}";
+            string componentName0 = rootName0 ?? Guid.NewGuid().ToString("N");
+            string version0 = rootVersion0 ?? NewRandomVersion();
+            string componentName1 = packageName1 ?? Guid.NewGuid().ToString("N");
+            string version1 = NewRandomVersion();
+            string componentName2 = rootName2 ?? Guid.NewGuid().ToString("N");
+            string version2 = rootVersion2 ?? NewRandomVersion();
+            string componentName3 = packageName3 ?? Guid.NewGuid().ToString("N");
+            string version3 = NewRandomVersion();
+            var packageLockTemplate = string.Format(packageLockJson, componentName0, version0, componentName1, version1, componentName2, version2, componentName2, version2, componentName3, version3, packageName0);
+
+            return (lockFileName, packageLockTemplate, Path.Combine(Path.GetTempPath(), lockFileName));
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/NpmUtilitiesTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/NpmUtilitiesTests.cs
new file mode 100644
index 000000000..e2640c9d6
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/NpmUtilitiesTests.cs
@@ -0,0 +1,270 @@
+using System.Linq;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Common.DependencyGraph;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Detectors.Npm;
+using Microsoft.ComponentDetection.Detectors.Tests.Utilities;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Newtonsoft.Json.Linq;
+using NuGet.Versioning;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class NpmUtilitiesTests
+    {
+        private Mock loggerMock;
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            loggerMock = new Mock();
+        }
+
+        [TestMethod]
+        public void TestGetTypedComponent()
+        {
+            string json = @"{
+                ""async"": {
+                    ""version"": ""2.3.0"",
+                    ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/async/-/async-2.3.0.tgz"",
+                    ""integrity"": ""sha1-EBPRBRBH3TIP4k5JTVxm7K9hR9k=""
+                },
+            }";
+
+            JObject j = JObject.Parse(json);
+
+            var componentFromJProperty = NpmComponentUtilities.GetTypedComponent(j.Children().Single(), "registry.npmjs.org", loggerMock.Object);
+
+            Assert.IsNotNull(componentFromJProperty);
+            Assert.AreEqual(componentFromJProperty.Type, ComponentType.Npm);
+
+            NpmComponent npmComponent = (NpmComponent)componentFromJProperty;
+            Assert.AreEqual(npmComponent.Name, "async");
+            Assert.AreEqual(npmComponent.Version, "2.3.0");
+        }
+
+        [TestMethod]
+        public void TestGetTypedComponent_FailsOnMalformed()
+        {
+            string json = @"{
+                ""async"": {
+                    ""version"": ""NOTAVERSION"",
+                    ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/async/-/async-2.3.0.tgz"",
+                    ""integrity"": ""sha1-EBPRBRBH3TIP4k5JTVxm7K9hR9k=""
+                },
+            }";
+
+            JObject j = JObject.Parse(json);
+
+            var componentFromJProperty = NpmComponentUtilities.GetTypedComponent(j.Children().Single(), "registry.npmjs.org", loggerMock.Object);
+
+            Assert.IsNull(componentFromJProperty);
+        }
+
+        [TestMethod]
+        public void TestGetTypedComponent_FailsOnInvalidPackageName()
+        {
+            string jsonInvalidCharacter = @"{
+                ""async<"": {
+                    ""version"": ""1.0.0"",
+                    ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/async/-/async-2.3.0.tgz"",
+                    ""integrity"": ""sha1-EBPRBRBH3TIP4k5JTVxm7K9hR9k=""
+                },
+            }";
+
+            JObject j = JObject.Parse(jsonInvalidCharacter);
+            var componentFromJProperty = NpmComponentUtilities.GetTypedComponent(j.Children().Single(), "registry.npmjs.org", loggerMock.Object);
+            Assert.IsNull(componentFromJProperty);
+
+            string jsonUrlName = @"{
+                ""http://thisis/my/packagename"": {
+                    ""version"": ""1.0.0"",
+                    ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/async/-/async-2.3.0.tgz"",
+                    ""integrity"": ""sha1-EBPRBRBH3TIP4k5JTVxm7K9hR9k=""
+                },
+            }";
+
+            j = JObject.Parse(jsonUrlName);
+            componentFromJProperty = NpmComponentUtilities.GetTypedComponent(j.Children().Single(), "registry.npmjs.org", loggerMock.Object);
+            Assert.IsNull(componentFromJProperty);
+
+            string jsonInvalidInitialCharacter1 = @"{
+                ""_async"": {
+                    ""version"": ""1.0.0"",
+                    ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/async/-/async-2.3.0.tgz"",
+                    ""integrity"": ""sha1-EBPRBRBH3TIP4k5JTVxm7K9hR9k=""
+                },
+            }";
+
+            j = JObject.Parse(jsonInvalidInitialCharacter1);
+            componentFromJProperty = NpmComponentUtilities.GetTypedComponent(j.Children().Single(), "registry.npmjs.org", loggerMock.Object);
+            Assert.IsNull(componentFromJProperty);
+
+            string jsonInvalidInitialCharacter2 = @"{
+                "".async"": {
+                    ""version"": ""1.0.0"",
+                    ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/async/-/async-2.3.0.tgz"",
+                    ""integrity"": ""sha1-EBPRBRBH3TIP4k5JTVxm7K9hR9k=""
+                },
+            }";
+
+            j = JObject.Parse(jsonInvalidInitialCharacter2);
+            componentFromJProperty = NpmComponentUtilities.GetTypedComponent(j.Children().Single(), "registry.npmjs.org", loggerMock.Object);
+            Assert.IsNull(componentFromJProperty);
+
+            var longPackageName = new string('a', 214);
+            string jsonLongName = $@"{{
+                ""{longPackageName}"": {{
+                    ""version"": ""1.0.0"",
+                    ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/async/-/async-2.3.0.tgz"",
+                    ""integrity"": ""sha1-EBPRBRBH3TIP4k5JTVxm7K9hR9k=""
+                }},
+            }}";
+
+            j = JObject.Parse(jsonLongName);
+            componentFromJProperty = NpmComponentUtilities.GetTypedComponent(j.Children().Single(), "registry.npmjs.org", loggerMock.Object);
+            Assert.IsNull(componentFromJProperty);
+        }
+
+        [TestMethod]
+        public void TestTryParseNpmVersion()
+        {
+            var parsed = NpmComponentUtilities.TryParseNpmVersion("registry.npmjs.org", "archiver", "https://registry.npmjs.org/archiver-2.1.1.tgz", out SemanticVersion parsedVersion);
+            Assert.IsTrue(parsed);
+            Assert.AreEqual(parsedVersion.ToString(), "2.1.1");
+
+            parsed = NpmComponentUtilities.TryParseNpmVersion("registry.npmjs.org", "archiver", "notavalidurl", out parsedVersion);
+            Assert.IsFalse(parsed);
+        }
+
+        [TestMethod]
+        public void TestTraverseAndGetRequirementsAndDependencies()
+        {
+            string json = @"{
+                ""archiver"": {
+                    ""version"": ""2.3.0"",
+                    ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/async/-/async-2.3.0.tgz"",
+                    ""integrity"": ""sha1-EBPRBRBH3TIP4k5JTVxm7K9hR9k="",
+                    ""dependencies"": {
+                            ""archiver-utils"": {
+                                ""version"": ""1.3.0"",
+                                ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/archiver-utils/-/archiver-utils-1.3.0.tgz"",
+                                ""integrity"": ""sha1-PRT306DRK/NZUaVL07iuqH7nWPg=""
+                            }
+                    }
+                },
+            }";
+
+            var jsonChildren = JObject.Parse(json).Children();
+            var currentDependency = jsonChildren.Single();
+            var dependencyLookup = jsonChildren.ToDictionary(dependency => dependency.Name);
+
+            var typedComponent = NpmComponentUtilities.GetTypedComponent(currentDependency, "registry.npmjs.org", loggerMock.Object);
+            ComponentRecorder componentRecorder = new ComponentRecorder();
+
+            var singleFileComponentRecorder1 = componentRecorder.CreateSingleFileComponentRecorder("/this/is/a/test/path/");
+            var singleFileComponentRecorder2 = componentRecorder.CreateSingleFileComponentRecorder("/this/is/a/different/path/");
+
+            NpmComponentUtilities.TraverseAndRecordComponents(currentDependency, singleFileComponentRecorder1, typedComponent, typedComponent);
+            NpmComponentUtilities.TraverseAndRecordComponents(currentDependency, singleFileComponentRecorder2, typedComponent, typedComponent);
+
+            Assert.AreEqual(componentRecorder.GetDetectedComponents().Count(), 1);
+            Assert.IsNotNull(componentRecorder.GetComponent(typedComponent.Id));
+
+            var graph1 = componentRecorder.GetDependencyGraphsByLocation()["/this/is/a/test/path/"];
+            var graph2 = componentRecorder.GetDependencyGraphsByLocation()["/this/is/a/different/path/"];
+
+            Assert.IsTrue(graph1.GetExplicitReferencedDependencyIds(typedComponent.Id).Contains(typedComponent.Id));
+            Assert.IsTrue(graph2.GetExplicitReferencedDependencyIds(typedComponent.Id).Contains(typedComponent.Id));
+            Assert.IsFalse(componentRecorder.GetEffectiveDevDependencyValue(typedComponent.Id).GetValueOrDefault(true));
+
+            string json1 = @"{
+                ""test"": {
+                    ""version"": ""2.0.0"",
+                    ""resolved"": ""https://mseng.pkgs.visualstudio.com/_packaging/VsoMicrosoftExternals/npm/registry/async/-/async-2.3.0.tgz"",
+                    ""integrity"": ""sha1-EBPRBRBH3TIP4k5JTVxm7K9hR9k="",
+                    ""dev"": ""true""
+                },
+            }";
+
+            var jsonChildren1 = JObject.Parse(json1).Children();
+            var currentDependency1 = jsonChildren1.Single();
+            var dependencyLookup1 = jsonChildren1.ToDictionary(dependency => dependency.Name);
+
+            var typedComponent1 = NpmComponentUtilities.GetTypedComponent(currentDependency1, "registry.npmjs.org", loggerMock.Object);
+
+            NpmComponentUtilities.TraverseAndRecordComponents(currentDependency1, singleFileComponentRecorder2, typedComponent1, typedComponent1);
+
+            Assert.AreEqual(componentRecorder.GetDetectedComponents().Count(), 2);
+
+            Assert.IsTrue(graph2.GetExplicitReferencedDependencyIds(typedComponent1.Id).Contains(typedComponent1.Id));
+            Assert.IsTrue(componentRecorder.GetEffectiveDevDependencyValue(typedComponent1.Id).GetValueOrDefault(false));
+
+            NpmComponentUtilities.TraverseAndRecordComponents(currentDependency1, singleFileComponentRecorder2, typedComponent, typedComponent1, parentComponentId: typedComponent1.Id);
+
+            Assert.AreEqual(componentRecorder.GetDetectedComponents().Count(), 2);
+            var explicitlyReferencedDependencyIds = graph2.GetExplicitReferencedDependencyIds(typedComponent.Id);
+            Assert.IsTrue(explicitlyReferencedDependencyIds.Contains(typedComponent.Id));
+            Assert.IsTrue(explicitlyReferencedDependencyIds.Contains(typedComponent1.Id));
+            Assert.AreEqual(2, explicitlyReferencedDependencyIds.Count);
+        }
+
+        [TestMethod]
+        public void AddOrUpdateDetectedComponent_NewComponent_ComponentAdded()
+        {
+            var expectedDetectedComponent = new DetectedComponent(new NpmComponent("test", "1.0.0"));
+            var expectedDetectedDevComponent = new DetectedComponent(new NpmComponent("test2", "1.0.0"));
+
+            ComponentRecorder componentRecorder = new ComponentRecorder();
+
+            var addedComponent1 = NpmComponentUtilities.AddOrUpdateDetectedComponent(
+                componentRecorder.CreateSingleFileComponentRecorder("path1"),
+                expectedDetectedComponent.Component, isDevDependency: false);
+
+            var addedComponent2 = NpmComponentUtilities.AddOrUpdateDetectedComponent(
+                componentRecorder.CreateSingleFileComponentRecorder("path2"),
+                expectedDetectedDevComponent.Component, isDevDependency: true);
+
+            addedComponent1.Should().BeEquivalentTo(expectedDetectedComponent, options => options.Excluding(obj => obj.DependencyRoots));
+            addedComponent2.Should().BeEquivalentTo(expectedDetectedDevComponent, options => options.Excluding(obj => obj.DependencyRoots));
+
+            componentRecorder.GetDetectedComponents().Count().Should().Be(2);
+
+            var nonDevComponent = componentRecorder.GetComponent(expectedDetectedComponent.Component.Id);
+            nonDevComponent.Should().BeEquivalentTo(expectedDetectedComponent.Component);
+            componentRecorder.GetEffectiveDevDependencyValue(nonDevComponent.Id).Should().Be(false);
+            componentRecorder.ForOneComponent(nonDevComponent.Id, grouping => grouping.AllFileLocations.Should().BeEquivalentTo("path1"));
+
+            var devComponent = componentRecorder.GetComponent(expectedDetectedDevComponent.Component.Id);
+            devComponent.Should().BeEquivalentTo(expectedDetectedDevComponent.Component);
+            componentRecorder.GetEffectiveDevDependencyValue(devComponent.Id).Should().Be(true);
+            componentRecorder.ForOneComponent(devComponent.Id, grouping => grouping.AllFileLocations.Should().BeEquivalentTo("path2"));
+        }
+
+        [TestMethod]
+
+        public void AddOrUpdateDetectedComponent_ComponentExistAsDevDependencyNewUpdateIsNoDevDependency_DevDependencyIsUpdatedToFalse()
+        {
+            var detectedComponent = new DetectedComponent(new NpmComponent("name", "1.0"))
+            {
+                DevelopmentDependency = true,
+            };
+
+            var componentRecorder = new ComponentRecorder();
+            var singleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder("path");
+            singleFileComponentRecorder.RegisterUsage(detectedComponent);
+
+            var updatedDetectedComponent = NpmComponentUtilities.AddOrUpdateDetectedComponent(
+                componentRecorder.CreateSingleFileComponentRecorder("path"), detectedComponent.Component,
+                isDevDependency: false);
+
+            componentRecorder.GetEffectiveDevDependencyValue(detectedComponent.Component.Id).Should().BeFalse();
+            componentRecorder.GetEffectiveDevDependencyValue(updatedDetectedComponent.Component.Id).Should().BeFalse();
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/NuGetComponentDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/NuGetComponentDetectorTests.cs
new file mode 100644
index 000000000..206bf6026
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/NuGetComponentDetectorTests.cs
@@ -0,0 +1,191 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Common.DependencyGraph;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.Internal;
+using Microsoft.ComponentDetection.Detectors.NuGet;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Microsoft.ComponentDetection.TestsUtilities;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class NuGetComponentDetectorTests
+    {
+        private Mock loggerMock;
+        private DetectorTestUtility detectorTestUtility;
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            loggerMock = new Mock();
+            detectorTestUtility = DetectorTestUtilityCreator.Create();
+        }
+
+        [TestMethod]
+        public async Task TestNuGetDetectorWithNoFiles_ReturnsSuccessfully()
+        {
+            var (scanResult, componentRecorder) = await detectorTestUtility.ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(0, componentRecorder.GetDetectedComponents().Count());
+        }
+
+        [TestMethod]
+        public async Task TestNugetDetector_ReturnsValidNuspecComponent()
+        {
+            var nuspec = NugetTestUtilities.GetRandomValidNuSpecComponent();
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("*.nuspec", nuspec)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(1, componentRecorder.GetDetectedComponents().Count());
+        }
+
+        [TestMethod]
+        public async Task TestNugetDetector_ReturnsValidNupkgComponent()
+        {
+            var nupkg = await NugetTestUtilities.ZipNupkgComponent("test.nupkg", NugetTestUtilities.GetRandomValidNuPkgComponent());
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("test.nupkg", nupkg)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(1, componentRecorder.GetDetectedComponents().Count());
+        }
+
+        [TestMethod]
+        public async Task TestNugetDetector_ReturnsValidMixedComponent()
+        {
+            var nuspec = NugetTestUtilities.GetRandomValidNuSpecComponent();
+            var nupkg = await NugetTestUtilities.ZipNupkgComponent("test.nupkg", NugetTestUtilities.GetRandomValidNuPkgComponent());
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("test.nuspec", nuspec)
+                                                    .WithFile("test.nupkg", nupkg)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(2, componentRecorder.GetDetectedComponents().Count());
+        }
+
+        [TestMethod]
+        public async Task TestNugetDetector_HandlesMalformedComponentsInComponentList()
+        {
+            var validNupkg = await NugetTestUtilities.ZipNupkgComponent("test.nupkg", NugetTestUtilities.GetRandomValidNuPkgComponent());
+            var malformedNupkg = await NugetTestUtilities.ZipNupkgComponent("malformed.nupkg", NugetTestUtilities.GetRandomMalformedNuPkgComponent());
+            var nuspec = NugetTestUtilities.GetRandomValidNuSpecComponent();
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithLogger(loggerMock)
+                                                    .WithFile("test.nuspec", nuspec)
+                                                    .WithFile("test.nupkg", validNupkg)
+                                                    .WithFile("malformed.nupkg", malformedNupkg)
+                                                    .ExecuteDetector();
+
+            loggerMock.Verify(x => x.LogFailedReadingFile(Path.Join(Path.GetTempPath(), "malformed.nupkg"), It.IsAny()));
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(2, componentRecorder.GetDetectedComponents().Count());
+        }
+
+        [TestMethod]
+        public async Task TestNugetDetector_AdditionalDirectories()
+        {
+            var component1 = NugetTestUtilities.GetRandomValidNuSpecComponentStream();
+            var streamsDetectedInNormalPass = new List { component1 };
+
+            var additionalDirectory = CreateTemporaryDirectory();
+            var nugetConfigComponent = NugetTestUtilities.GetValidNuGetConfig(additionalDirectory);
+            var streamsDetectedInAdditionalDirectoryPass = new List { nugetConfigComponent };
+
+            var componentRecorder = new ComponentRecorder();
+            var detector = new NuGetComponentDetector();
+            var sourceDirectoryPath = CreateTemporaryDirectory();
+
+            detector.Logger = loggerMock.Object;
+
+            // Use strict mock evaluation because we're doing some "fun" stuff with this mock.
+            var componentStreamEnumerableFactoryMock = new Mock(MockBehavior.Strict);
+            var directoryWalkerMock = new Mock(MockBehavior.Strict);
+
+            directoryWalkerMock.Setup(x => x.Initialize(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>()));
+
+            // First setup is for the invocation of stream enumerable factory used to find NuGet.Configs -- a special case the detector supports to locate repos located outside the source dir
+            //  We return a nuget config that targets a different temp folder that is NOT in a subtree of the sourcedirectory.
+            componentStreamEnumerableFactoryMock.Setup(
+                x => x.GetComponentStreams(
+                    Match.Create(info => info.FullName.Contains(sourceDirectoryPath)),
+                    Match.Create>(stuff => stuff.Contains(NuGetComponentDetector.NugetConfigFileName)),
+                    It.IsAny(),
+                    It.IsAny()))
+                .Returns(streamsDetectedInAdditionalDirectoryPass);
+
+            // Normal detection setup here -- we have it returning empty.
+            componentStreamEnumerableFactoryMock.Setup(
+                x => x.GetComponentStreams(
+                    Match.Create(info => info.FullName.Contains(sourceDirectoryPath)),
+                    Match.Create>(stuff => detector.SearchPatterns.Intersect(stuff).Count() == detector.SearchPatterns.Count),
+                    It.IsAny(),
+                    It.IsAny()))
+                .Returns(Enumerable.Empty());
+
+            // This is matching the additional directory that is ONLY sourced in the nuget.config. If this works, we would see the component in our results.
+            componentStreamEnumerableFactoryMock.Setup(
+                x => x.GetComponentStreams(
+                    Match.Create(info => info.FullName.Contains(additionalDirectory)),
+                    Match.Create>(stuff => detector.SearchPatterns.Intersect(stuff).Count() == detector.SearchPatterns.Count),
+                    It.IsAny(),
+                    It.IsAny()))
+                .Returns(streamsDetectedInNormalPass);
+
+            // Normal detection setup here -- we have it returning empty.
+            directoryWalkerMock.Setup(
+                x => x.GetFilteredComponentStreamObservable(
+                    Match.Create(info => info.FullName.Contains(sourceDirectoryPath)),
+                    It.IsAny>(),
+                    It.IsAny()))
+                .Returns(() => streamsDetectedInAdditionalDirectoryPass.Select(cs => new ProcessRequest { ComponentStream = cs, SingleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder(cs.Location) }).ToObservable());
+
+            // This is matching the additional directory that is ONLY sourced in the nuget.config. If this works, we would see the component in our results.
+            directoryWalkerMock.Setup(
+                x => x.GetFilteredComponentStreamObservable(
+                    Match.Create(info => info.FullName.Contains(additionalDirectory)),
+                    It.IsAny>(),
+                    It.IsAny()))
+                .Returns(() => streamsDetectedInNormalPass.Select(cs => new ProcessRequest { ComponentStream = cs, SingleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder(cs.Location) }).ToObservable());
+
+            detector.ComponentStreamEnumerableFactory = componentStreamEnumerableFactoryMock.Object;
+            detector.Scanner = directoryWalkerMock.Object;
+
+            var scanResult = await detector.ExecuteDetectorAsync(new ScanRequest(new DirectoryInfo(sourceDirectoryPath), (name, directoryName) => false, null, new Dictionary(), null, componentRecorder));
+
+            directoryWalkerMock.VerifyAll();
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(1, componentRecorder.GetDetectedComponents().Count());
+        }
+
+        private string CreateTemporaryDirectory()
+        {
+            string path;
+            do
+            {
+                path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+            }
+            while (Directory.Exists(path) || File.Exists(path));
+
+            Directory.CreateDirectory(path);
+            return path;
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/NuGetNuspecUtilitiesTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/NuGetNuspecUtilitiesTests.cs
new file mode 100644
index 000000000..3cdb9cead
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/NuGetNuspecUtilitiesTests.cs
@@ -0,0 +1,100 @@
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Detectors.NuGet;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class NuGetNuspecUtilitiesTests
+    {
+        [TestMethod]
+        public async Task GetNuspecBytes_FailsOnEmptyStream()
+        {
+            using var stream = new MemoryStream();
+
+            async Task ShouldThrow() => await NuGetNuspecUtilities.GetNuspecBytesAsync(stream);
+
+            await Assert.ThrowsExceptionAsync(ShouldThrow);
+
+            // The position should always be reset to 0
+            Assert.AreEqual(0, stream.Position);
+        }
+
+        [TestMethod]
+        public async Task GetNuspecBytes_FailsOnTooSmallStream()
+        {
+            using var stream = new MemoryStream();
+
+            for (int i = 0; i < NuGetNuspecUtilities.MinimumLengthForZipArchive - 1; i++)
+            {
+                stream.WriteByte(0);
+            }
+
+            stream.Seek(0, SeekOrigin.Begin);
+
+            async Task ShouldThrow() => await NuGetNuspecUtilities.GetNuspecBytesAsync(stream);
+
+            await Assert.ThrowsExceptionAsync(ShouldThrow);
+
+            // The position should always be reset to 0
+            Assert.AreEqual(0, stream.Position);
+        }
+
+        [TestMethod]
+        public async Task GetNuspecBytes_FailsIfNuspecNotPresent()
+        {
+            using var stream = new MemoryStream();
+
+            using (ZipArchive archive = new ZipArchive(stream, ZipArchiveMode.Create, true))
+            {
+                archive.CreateEntry("test.txt");
+            }
+
+            stream.Seek(0, SeekOrigin.Begin);
+
+            async Task ShouldThrow() => await NuGetNuspecUtilities.GetNuspecBytesAsync(stream);
+
+            // No Nuspec File is in the archive
+            await Assert.ThrowsExceptionAsync(ShouldThrow);
+
+            // The position should always be reset to 0
+            Assert.AreEqual(0, stream.Position);
+        }
+
+        [TestMethod]
+        public async Task GetNuspecBytes_ReadsNuspecBytes()
+        {
+            byte[] randomBytes = { 0xDE, 0xAD, 0xC0, 0xDE };
+
+            using var stream = new MemoryStream();
+
+            using (ZipArchive archive = new ZipArchive(stream, ZipArchiveMode.Create, true))
+            {
+                var entry = archive.CreateEntry("test.nuspec");
+
+                using var entryStream = entry.Open();
+
+                await entryStream.WriteAsync(randomBytes, 0, randomBytes.Length);
+            }
+
+            stream.Seek(0, SeekOrigin.Begin);
+
+            var bytes = await NuGetNuspecUtilities.GetNuspecBytesAsync(stream);
+
+            Assert.AreEqual(randomBytes.Length, bytes.Length);
+
+            for (int i = 0; i < randomBytes.Length; i++)
+            {
+                Assert.AreEqual(randomBytes[i], bytes[i]);
+            }
+
+            // The position should always be reset to 0
+            Assert.AreEqual(0, stream.Position);
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/NuGetProjectModelProjectCentricComponentDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/NuGetProjectModelProjectCentricComponentDetectorTests.cs
new file mode 100644
index 000000000..5ea7e2e81
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/NuGetProjectModelProjectCentricComponentDetectorTests.cs
@@ -0,0 +1,328 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Detectors.NuGet;
+using Microsoft.ComponentDetection.Detectors.Tests.Mocks;
+using Microsoft.ComponentDetection.Detectors.Tests.Utilities;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Microsoft.ComponentDetection.TestsUtilities;
+using Newtonsoft.Json;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class NuGetProjectModelProjectCentricComponentDetectorTests
+    {
+        private DetectorTestUtility detectorTestUtility;
+        private readonly string projectAssetsJsonFileName = "project.assets.json";
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            var detector = new NuGetProjectModelProjectCentricComponentDetector();
+            
+            var loggerMock = new Mock();
+            loggerMock.Setup(x => x.LogWarning(It.IsAny())).Callback(message => Console.WriteLine(message));
+            loggerMock.Setup(x => x.LogFailedReadingFile(It.IsAny(), It.IsAny())).Callback((message, exception) =>
+            {
+                Console.WriteLine(message);
+                Console.WriteLine(exception.ToString());
+            });
+            detector.Logger = loggerMock.Object;
+
+            var fileUtilityServiceMock = new Mock();
+            fileUtilityServiceMock.Setup(x => x.Exists(It.IsAny()))
+                .Returns(true);
+            detector.FileUtilityService = fileUtilityServiceMock.Object;
+
+            detectorTestUtility = DetectorTestUtilityCreator.Create()
+                                                            .WithDetector(detector);
+        }
+
+        [TestMethod]
+        public async Task ScanDirectoryAsync_Base_2_2_Verification()
+        {
+            string osAgnostic = Convert22SampleToOSAgnostic(TestResources.project_assets_2_2);
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile(projectAssetsJsonFileName, osAgnostic)
+                                                    .ExecuteDetector();
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+
+            // Number of unique nodes in ProjectAssetsJson
+            Console.WriteLine(string.Join(",", detectedComponents.Select(x => x.Component.Id)));
+            Assert.AreEqual(3, detectedComponents.Count());
+            Assert.IsNotNull(detectedComponents.Select(x => x.Component).Cast().FirstOrDefault(x => x.Name.Contains("coverlet.msbuild")));
+
+            Assert.IsTrue(detectedComponents.All(x =>
+                            componentRecorder.IsDependencyOfExplicitlyReferencedComponents(
+                                    x.Component.Id,
+                                    y => y.Id == x.Component.Id)));
+
+            componentRecorder.ForAllComponents(grouping => Assert.IsTrue(grouping.AllFileLocations.Any(location => location.Contains("Loader.csproj"))));
+        }
+
+        [TestMethod]
+        public async Task ScanDirectoryAsync_Base_2_2_additional_Verification()
+        {
+            string osAgnostic = Convert22SampleToOSAgnostic(TestResources.project_assets_2_2_additional);
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile(projectAssetsJsonFileName, osAgnostic)
+                                                    .ExecuteDetector();
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+
+            // Number of unique nodes in ProjectAssetsJson
+            Console.WriteLine(string.Join(",", detectedComponents.Select(x => x.Component.Id)));
+            Assert.AreEqual(26, detectedComponents.Count());
+            Assert.IsNotNull(detectedComponents.Select(x => x.Component).Cast().FirstOrDefault(x => x.Name.Contains("Polly")));
+            Assert.AreEqual(5, detectedComponents.Select(x => x.Component).Cast().Count(x => x.Name.Contains("System.Composition")));
+
+            var nugetVersioning = detectedComponents.FirstOrDefault(x => (x.Component as NuGetComponent).Name.Contains("NuGet.DependencyResolver.Core"));
+            Assert.IsNotNull(nugetVersioning);
+
+            Assert.IsTrue(componentRecorder.IsDependencyOfExplicitlyReferencedComponents(
+                            nugetVersioning.Component.Id,
+                            x => x.Name.Contains("NuGet.ProjectModel")));
+
+            componentRecorder.ForAllComponents(grouping => Assert.IsTrue(grouping.AllFileLocations.Any(location => location.Contains("Detectors.csproj"))));
+        }
+
+        [TestMethod]
+        public async Task ScanDirectoryAsync_ExcludedFrameworkComponent_2_2_Verification()
+        {
+            string osAgnostic = Convert22SampleToOSAgnostic(TestResources.project_assets_2_2);
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile(projectAssetsJsonFileName, osAgnostic)
+                                                    .ExecuteDetector();
+
+            var ommittedComponentInformationJson = scanResult.AdditionalTelemetryDetails[NuGetProjectModelProjectCentricComponentDetector.OmittedFrameworkComponentsTelemetryKey];
+            var omittedComponentsWithCount = JsonConvert.DeserializeObject>(ommittedComponentInformationJson);
+
+            Assert.IsTrue(omittedComponentsWithCount.Keys.Count() > 5, "Ommitted framework assemblies are missing. There should be more than ten, but this is a gut check to make sure we have data.");
+            Assert.AreEqual(omittedComponentsWithCount["Microsoft.NETCore.App"], 4, "There should be four cases of the NETCore.App library being omitted in the test data.");
+        }
+
+        [TestMethod]
+        public async Task ScanDirectoryAsync_DependencyGraph_2_2_additional_Verification()
+        {
+            string osAgnostic = Convert22SampleToOSAgnostic(TestResources.project_assets_2_2_additional);
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile(projectAssetsJsonFileName, osAgnostic)
+                                                    .ExecuteDetector();
+            var graphsByLocation = componentRecorder.GetDependencyGraphsByLocation();
+            var graph = graphsByLocation.Values.First();
+
+            var expectedDependencyIdsForCompositionTypedParts = new[]
+            {
+                "NuGet.DependencyResolver.Core 5.6.0 - NuGet",
+            };
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            var componentDetectionCommon = detectedComponents.First(x => x.Component.Id.Contains("NuGet.ProjectModel"));
+            var dependencies = graph.GetDependenciesForComponent(componentDetectionCommon.Component.Id);
+            foreach (var expectedId in expectedDependencyIdsForCompositionTypedParts)
+            {
+                Assert.IsTrue(dependencies.Contains(expectedId));
+            }
+
+            Assert.AreEqual(dependencies.Count(), expectedDependencyIdsForCompositionTypedParts.Count());
+
+            Assert.AreEqual(graph.GetComponents().Count(), detectedComponents.Count());
+
+            // Top level dependencies look like this:
+            // (we expect all non-proj and non-framework to show up as explicit refs, so those will be absent from the check)
+            // 
+            // "DotNet.Glob >= 2.1.1",
+            // "Microsoft.NETCore.App >= 2.2.8",
+            // "Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common >= 1.0.0",
+            // "Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts >= 1.0.0",
+            // "MinVer >= 2.5.0",
+            // "Nett >= 0.10.0",
+            // "Newtonsoft.Json >= 12.0.3",
+            // "NuGet.ProjectModel >= 5.6.0",
+            // "NuGet.Versioning >= 5.6.0",
+            // "Polly >= 7.0.3",
+            // "SemanticVersioning >= 1.2.0",
+            // "StyleCop.Analyzers >= 1.0.2",
+            // "System.Composition.AttributedModel >= 1.4.0",
+            // "System.Composition.Convention >= 1.4.0",
+            // "System.Composition.Hosting >= 1.4.0",
+            // "System.Composition.Runtime >= 1.4.0",
+            // "System.Composition.TypedParts >= 1.4.0",
+            // "System.Reactive >= 4.1.2",
+            // "System.Threading.Tasks.Dataflow >= 4.9.0",
+            // "coverlet.msbuild >= 2.5.1",
+            // "yamldotnet >= 5.3.0"
+            var expectedExplicitRefs = new[]
+            {
+                "DotNet.Glob",
+                "MinVer",
+                "Nett",
+                "Newtonsoft.Json",
+                "NuGet.ProjectModel",
+                "NuGet.Versioning",
+                "Polly",
+                "SemanticVersioning",
+                "StyleCop.Analyzers",
+                "System.Composition.AttributedModel",
+                "System.Composition.Convention",
+                "System.Composition.Hosting",
+                "System.Composition.Runtime",
+                "System.Composition.TypedParts",
+                "System.Reactive",
+                "coverlet.msbuild",
+                "YamlDotNet",
+            };
+
+            foreach (var componentId in graph.GetComponents())
+            {
+                var component = detectedComponents.First(x => x.Component.Id == componentId);
+                bool expectedExplicitRefValue = expectedExplicitRefs.Contains(((NuGetComponent)component.Component).Name);
+                Assert.AreEqual(expectedExplicitRefValue, graph.IsComponentExplicitlyReferenced(componentId));
+            }
+        }
+
+        [TestMethod]
+        public async Task ScanDirectoryAsync_Base_3_1_Verification()
+        {
+            string osAgnostic = Convert31SampleToOSAgnostic(TestResources.project_assets_3_1);
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile(projectAssetsJsonFileName, osAgnostic)
+                                                    .ExecuteDetector();
+
+            // Number of unique nodes in ProjectAssetsJson
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(2, detectedComponents.Count());
+            Assert.IsNotNull(detectedComponents.Select(x => x.Component).Cast().FirstOrDefault(x => x.Name.Contains("Microsoft.Extensions.DependencyModel")));
+
+            var systemTextJson = detectedComponents.FirstOrDefault(x => (x.Component as NuGetComponent).Name.Contains("System.Text.Json"));
+
+            Assert.IsTrue(componentRecorder.IsDependencyOfExplicitlyReferencedComponents(
+                            systemTextJson.Component.Id,
+                            x => x.Name.Contains("Microsoft.Extensions.DependencyModel")));
+
+            componentRecorder.ForAllComponents(grouping => Assert.IsTrue(grouping.AllFileLocations.Any(location => location.Contains("ExtCore.WebApplication.csproj"))));
+        }
+
+        [TestMethod]
+        public async Task ScanDirectoryAsync_ExcludedFrameworkComponent_3_1_Verification()
+        {
+            string osAgnostic = Convert31SampleToOSAgnostic(TestResources.project_assets_3_1);
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile(projectAssetsJsonFileName, osAgnostic)
+                                                    .ExecuteDetector();
+
+            var ommittedComponentInformationJson = scanResult.AdditionalTelemetryDetails[NuGetProjectModelProjectCentricComponentDetector.OmittedFrameworkComponentsTelemetryKey];
+            var omittedComponentsWithCount = JsonConvert.DeserializeObject>(ommittedComponentInformationJson);
+
+            // With 3.X, we don't expect there to be a lot of these, but there are still netstandard libraries present which can bring things into the graph
+            Assert.AreEqual(omittedComponentsWithCount.Keys.Count(), 4, "Ommitted framework assemblies are missing. There should be more than ten, but this is a gut check to make sure we have data.");
+            Assert.AreEqual(omittedComponentsWithCount["System.Reflection"], 1, "There should be one case of the System.Reflection library being omitted in the test data.");
+        }
+
+        [TestMethod]
+        public async Task ScanDirectoryAsync_DependencyGraph_3_1_Verification()
+        {
+            string osAgnostic = Convert31SampleToOSAgnostic(TestResources.project_assets_3_1);
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile(projectAssetsJsonFileName, osAgnostic)
+                                                    .ExecuteDetector();
+
+            var graphsByLocation = componentRecorder.GetDependencyGraphsByLocation();
+            var graph = graphsByLocation.Values.First();
+
+            var expectedDependencyIdsForExtensionsDependencyModel = new[]
+            {
+                "System.Text.Json 4.6.0 - NuGet",
+            };
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            
+            var componentDetectionCommon = detectedComponents.First(x => x.Component.Id.Contains("Microsoft.Extensions.DependencyModel"));
+            var dependencies = graph.GetDependenciesForComponent(componentDetectionCommon.Component.Id);
+            foreach (var expectedId in expectedDependencyIdsForExtensionsDependencyModel)
+            {
+                Assert.IsTrue(dependencies.Contains(expectedId));
+            }
+
+            Assert.AreEqual(graph.GetComponents().Count(), detectedComponents.Count());
+
+            // Top level dependencies look like this:
+            // (we expect all non-proj and non-framework to show up as explicit refs, so those will be absent from the check)
+            // 
+            // "ExtCore.Infrastructure >= 5.1.0",
+            // "Microsoft.Extensions.DependencyModel >= 3.0.0",
+            // "System.Runtime.Loader >= 4.3.0"
+            var expectedExplicitRefs = new[]
+            {
+                "Microsoft.Extensions.DependencyModel",
+            };
+
+            foreach (var componentId in graph.GetComponents())
+            {
+                var component = detectedComponents.First(x => x.Component.Id == componentId);
+                bool expectedExplicitRefValue = expectedExplicitRefs.Contains(((NuGetComponent)component.Component).Name);
+                Assert.AreEqual(expectedExplicitRefValue, graph.IsComponentExplicitlyReferenced(componentId));
+            }
+        }
+
+        [TestMethod]
+        public async Task ScanDirectory_NoPackageSpec()
+        {
+            string osAgnostic =
+@"{
+  ""version"": 3,
+  ""targets"": {
+    "".NETCoreApp,Version=v3.0"": {}
+  },
+ ""packageFolders"": {}
+}";
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile(projectAssetsJsonFileName, osAgnostic)
+                                                    .ExecuteDetector();
+
+            scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+            
+            var dependencyGraphs = componentRecorder.GetDependencyGraphsByLocation();
+
+            dependencyGraphs.Count().Should().Be(0);
+        }
+
+        private string Convert22SampleToOSAgnostic(string project_assets)
+        {
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                return project_assets;
+            }
+
+            project_assets = project_assets.Replace("D:\\\\Source\\\\componentdetection-bcde\\\\src\\\\Common\\\\", "/d/source/componentdetection-bcde/src/Common/");
+            project_assets = project_assets.Replace("D:\\\\Source\\\\componentdetection-bcde\\\\src\\\\Contracts\\\\", "/d/source/componentdetection-bcde/src/Contracts/");
+            project_assets = project_assets.Replace("D:\\\\Source\\\\componentdetection-bcde\\\\src\\\\Detectors\\\\", "/d/source/componentdetection-bcde/src/Detectors/");
+            project_assets = project_assets.Replace("D:\\\\Source\\\\componentdetection-bcde\\\\src\\\\Orchestrator\\\\", "/d/source/componentdetection-bcde/src/Orchestrator/");
+            project_assets = project_assets.Replace("D:\\\\Source\\\\componentdetection-bcde\\\\src\\\\Loader\\\\", "/d/source/componentdetection-bcde/src/Loader/");
+
+            return project_assets;
+        }
+
+        private string Convert31SampleToOSAgnostic(string project_assets)
+        {
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                return project_assets;
+            }
+
+            project_assets = project_assets.Replace("D:\\\\Source\\\\ExtCore\\\\src\\\\ExtCore.WebApplication\\\\", "/d/Source/ExtCore/src/ExtCore.WebApplication/");
+            project_assets = project_assets.Replace("D:\\\\Source\\\\ExtCore\\\\src\\\\ExtCore.Infrastructure\\\\", "/d/Source/ExtCore/src/ExtCore.Infrastructure/");
+            return project_assets;
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/NugetTestUtilities.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/NugetTestUtilities.cs
new file mode 100644
index 000000000..df387cb0e
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/NugetTestUtilities.cs
@@ -0,0 +1,170 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Contracts;
+using Moq;
+using Microsoft.ComponentDetection.TestsUtilities;
+
+using static Microsoft.ComponentDetection.Detectors.Tests.Utilities.TestUtilityExtensions;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    public static class NugetTestUtilities
+    {
+        public static string GetRandomValidNuSpecComponent()
+        {
+            string componentName = Guid.NewGuid().ToString("N");
+            string componentSpecFileName = $"{componentName}.nuspec";
+            string componentSpecPath = Path.Combine(Path.GetTempPath(), componentSpecFileName);
+            var template = GetTemplatedNuspec(componentName, NewRandomVersion());
+
+            return template;
+        }
+
+        public static IComponentStream GetRandomValidNuSpecComponentStream()
+        {
+            string componentName = Guid.NewGuid().ToString("N");
+            string componentSpecFileName = $"{componentName}.nuspec";
+            string componentSpecPath = Path.Combine(Path.GetTempPath(), componentSpecFileName);
+            var template = GetTemplatedNuspec(componentName, NewRandomVersion());
+
+            var mock = new Mock();
+            mock.SetupGet(x => x.Stream).Returns(template.ToStream());
+            mock.SetupGet(x => x.Pattern).Returns("*.nuspec");
+            mock.SetupGet(x => x.Location).Returns(componentSpecPath);
+
+            return mock.Object;
+        }
+
+        public static IComponentStream GetValidNuGetConfig(string repositoryPath)
+        {
+            var template = GetTemplatedNuGetConfig(repositoryPath);
+
+            var mock = new Mock();
+            mock.SetupGet(x => x.Stream).Returns(template.ToStream());
+            mock.Setup(x => x.Location).Returns(Path.Combine(repositoryPath, "nuget.config"));
+            mock.Setup(x => x.Pattern).Returns("nuget.config");
+
+            return mock.Object;
+        }
+
+        public static string GetRandomValidNuPkgComponent()
+        {
+            string componentName = Guid.NewGuid().ToString("N");
+            var template = GetTemplatedNuspec(componentName, NewRandomVersion());
+            return template;
+        }
+
+        public static async Task ZipNupkgComponent(string filename, string content)
+        {
+            var stream = new MemoryStream();
+
+            using (ZipArchive archive = new ZipArchive(stream, ZipArchiveMode.Create, true))
+            {
+                var entry = archive.CreateEntry($"{filename}.nuspec");
+
+                using var entryStream = entry.Open();
+
+                var templateBytes = Encoding.UTF8.GetBytes(content);
+                await entryStream.WriteAsync(templateBytes, 0, templateBytes.Length);
+            }
+
+            stream.Seek(0, SeekOrigin.Begin);
+            return stream;
+        }
+
+        public static string GetRandomMalformedNuPkgComponent()
+        {
+            string componentName = Guid.NewGuid().ToString("N");
+            var template = GetTemplatedNuspec(componentName, NewRandomVersion());
+            template = template.Replace("", "");
+            return template;
+        }
+
+        private static string GetTemplatedNuspec(string id, string version)
+        {
+            string nuspec = @"
+                            
+                                
+                                    
+                                    {0}
+                                    {1}
+                                    
+                                    
+
+                                    
+                                    
+                                
+                                
+                            ";
+
+            return string.Format(nuspec, id, version);
+        }
+
+        private static string GetTemplatedNuGetConfig(string repositoryPath)
+        {
+            string nugetConfig =
+                @"
+                
+                    
+                        
+                    
+                ";
+            return string.Format(nugetConfig, repositoryPath);
+        }
+
+        private static string GetTemplatedProjectAsset(IDictionary>> packages)
+        {
+            string individualPackageJson =
+                @"""{packageName}"": {
+                      ""type"": ""package"",
+                      ""dependencies"": {
+                          {dependencyList}
+                      }
+                  }";
+
+            var packageJsons = new List();
+            foreach (var package in packages)
+            {
+                var packageName = package.Key;
+                var dependencyList = string.Join(",", package.Value.Select(d => string.Format(@"""{0}"":""{1}""", d.Key, d.Value)));
+                packageJsons.Add(individualPackageJson.Replace("{packageName}", packageName).Replace("{dependencyList}", dependencyList));
+            }
+
+            var allPackageJson = string.Join(",", packageJsons);
+            string pojectAssetsJson =
+                @"{
+                      ""version"": 2,
+                      ""targets"": {
+                            "".NETCoreApp,Version=v2.1"": {
+                                {allPackageJson}
+                            },
+                            "".NETFramework,Version=v4.6.1"": {
+                                {allPackageJson}
+                            }
+                      },
+                      ""project"": {
+                        ""version"": ""1.0.0"",
+                        ""restore"": {
+                          ""projectPath"": ""myproject.csproj""
+                        },
+                        ""frameworks"": {
+                          ""netcoreapp2.0"": {
+                            ""dependencies"": {
+                              ""Microsoft.NETCore.App"": {
+                                ""target"": ""Package"",
+                                ""version"": ""[2.0.0, )""
+                              }
+                            }
+                          }
+                        }
+                      }
+                }";
+            return pojectAssetsJson.Replace("{allPackageJson}", allPackageJson);
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/PipComponentDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/PipComponentDetectorTests.cs
new file mode 100644
index 000000000..aa76ae8cd
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/PipComponentDetectorTests.cs
@@ -0,0 +1,321 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Detectors.Pip;
+using Microsoft.ComponentDetection.Detectors.Tests.Utilities;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Microsoft.ComponentDetection.TestsUtilities;
+using Newtonsoft.Json;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    public class PipComponentDetectorTests
+    {
+        private Mock pythonCommandService;
+        private Mock pythonResolver;
+        private Mock loggerMock;
+
+        private DetectorTestUtility detectorTestUtility;
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            pythonCommandService = new Mock();
+            pythonResolver = new Mock();
+            loggerMock = new Mock();
+
+            var detector = new PipComponentDetector
+            {
+                PythonCommandService = pythonCommandService.Object,
+                PythonResolver = pythonResolver.Object,
+                Logger = loggerMock.Object,
+            };
+
+            detectorTestUtility = DetectorTestUtilityCreator.Create()
+                                                            .WithDetector(detector);
+        }
+
+        [TestMethod]
+        public async Task TestPipDetector_PythonNotInstalled()
+        {
+            pythonCommandService.Setup(x => x.PythonExists(It.IsAny())).ReturnsAsync(false);
+
+            loggerMock.Setup(x => x.LogInfo(It.Is(l => l.Contains("No python found"))));
+
+            var (result, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("setup.py", string.Empty)
+                                                    .WithLogger(loggerMock)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, result.ResultCode);
+            loggerMock.VerifyAll();
+        }
+
+        [TestMethod]
+        public async Task TestPipDetector_PythonInstalledNoFiles()
+        {
+            var (result, componentRecorder) = await detectorTestUtility.ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, result.ResultCode);
+        }
+
+        [TestMethod]
+        public async Task TestPipDetector_SetupPyAndRequirementsTxt()
+        {
+            pythonCommandService.Setup(x => x.PythonExists(It.IsAny())).ReturnsAsync(true);
+
+            var baseSetupPyDependencies = ToGitTuple(new List { "a==1.0", "b>=2.0,!=2.1", "c!=1.1" });
+            var baseRequirementsTextDependencies = ToGitTuple(new List { "d~=1.0", "e<=2.0", "f===1.1" });
+            baseRequirementsTextDependencies.Add((null, new GitComponent(new Uri("https://github.com/example/example"), "deadbee")));
+
+            pythonCommandService.Setup(x => x.ParseFile(Path.Join(Path.GetTempPath(), "setup.py"), null)).ReturnsAsync(baseSetupPyDependencies);
+            pythonCommandService.Setup(x => x.ParseFile(Path.Join(Path.GetTempPath(), "requirements.txt"), null)).ReturnsAsync(baseRequirementsTextDependencies);
+
+            var setupPyRoots = new List
+            {
+                new PipGraphNode(new PipComponent("a", "1.0")),
+                new PipGraphNode(new PipComponent("b", "2.3")),
+                new PipGraphNode(new PipComponent("c", "1.0.1")),
+            };
+
+            setupPyRoots[1].Children.Add(new PipGraphNode(new PipComponent("z", "1.2.3")));
+
+            var requirementsTxtRoots = new List
+            {
+                new PipGraphNode(new PipComponent("d", "1.0")),
+                new PipGraphNode(new PipComponent("e", "1.9")),
+                new PipGraphNode(new PipComponent("f", "1.1")),
+            };
+
+            pythonResolver.Setup(x => x.ResolveRoots(It.Is>(p => p.Any(d => d.Name == "b")))).ReturnsAsync(setupPyRoots);
+            pythonResolver.Setup(x => x.ResolveRoots(It.Is>(p => p.Any(d => d.Name == "d")))).ReturnsAsync(requirementsTxtRoots);
+
+            var (result, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("setup.py", string.Empty)
+                                                    .WithFile("requirements.txt", string.Empty)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, result.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(8, detectedComponents.Count());
+
+            var pipComponents = detectedComponents.Where(detectedComponent => detectedComponent.Component.Id.Contains("pip")).ToList();
+            Assert.AreEqual("1.2.3", ((PipComponent)pipComponents.Single(x => ((PipComponent)x.Component).Name == "z").Component).Version);
+
+            foreach (var item in setupPyRoots)
+            {
+                var reference = (PipComponent)item.Value;
+
+                Assert.AreEqual(reference.Version, ((PipComponent)pipComponents.Single(x => ((PipComponent)x.Component).Name == reference.Name).Component).Version);
+            }
+
+            var gitComponents = detectedComponents.Where(detectedComponent => detectedComponent.Component.Type == ComponentType.Git);
+            gitComponents.Count().Should().Be(1);
+            var gitComponent = (GitComponent)gitComponents.Single().Component;
+
+            gitComponent.RepositoryUrl.Should().Be("https://github.com/example/example");
+            gitComponent.CommitHash.Should().Be("deadbee");
+        }
+
+        [TestMethod]
+        public async Task TestPipDetector_ComponentsDedupedAcrossFiles()
+        {
+            pythonCommandService.Setup(x => x.PythonExists(It.IsAny())).ReturnsAsync(true);
+
+            var baseRequirementsTextDependencies = ToGitTuple(new List { "d~=1.0", "e<=2.0", "f===1.1", "h==1.3" });
+            var baseRequirementsTextDependencies2 = ToGitTuple(new List { "D~=1.0", "E<=2.0", "F===1.1", "g==2" });
+            pythonCommandService.Setup(x => x.ParseFile(Path.Join(Path.GetTempPath(), "requirements.txt"), null)).ReturnsAsync(baseRequirementsTextDependencies);
+            pythonCommandService.Setup(x => x.ParseFile(Path.Join(Path.GetTempPath(), "TEST", "requirements.txt"), null)).ReturnsAsync(baseRequirementsTextDependencies2);
+
+            var requirementsTxtRoots = new List
+            {
+                new PipGraphNode(new PipComponent("d", "1.0")),
+                new PipGraphNode(new PipComponent("e", "1.9")),
+                new PipGraphNode(new PipComponent("f", "1.1")),
+                new PipGraphNode(new PipComponent("h", "1.3")),
+            };
+            var requirementsTxtRoots2 = new List
+            {
+                new PipGraphNode(new PipComponent("D", "1.0")),
+                new PipGraphNode(new PipComponent("E", "1.9")),
+                new PipGraphNode(new PipComponent("F", "1.1")),
+                new PipGraphNode(new PipComponent("g", "1.2")),
+            };
+
+            pythonResolver.Setup(x => x.ResolveRoots(It.Is>(p => p.Any(d => d.Name == "h")))).ReturnsAsync(requirementsTxtRoots);
+            pythonResolver.Setup(x => x.ResolveRoots(It.Is>(p => p.Any(d => d.Name == "g")))).ReturnsAsync(requirementsTxtRoots2);
+
+            var (result, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("requirements.txt", string.Empty)
+                                                    .WithFile("requirements.txt", string.Empty, fileLocation: Path.Join(Path.GetTempPath(), "TEST", "requirements.txt"))
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, result.ResultCode);
+            Assert.AreEqual(5, componentRecorder.GetDetectedComponents().Count());
+        }
+
+        [TestMethod]
+        public async Task TestPipDetector_ComponentRecorder()
+        {
+            pythonCommandService.Setup(x => x.PythonExists(It.IsAny())).ReturnsAsync(true);
+
+            const string file1 = "c:\\repo\\setup.py";
+            const string file2 = "c:\\repo\\lib\\requirements.txt";
+
+            var baseReqs = ToGitTuple(new List { "a~=1.0", "b<=2.0", });
+            var altReqs = ToGitTuple(new List { "c~=1.0", "d<=2.0", "e===1.1" });
+            pythonCommandService.Setup(x => x.ParseFile(file1, null)).ReturnsAsync(baseReqs);
+            pythonCommandService.Setup(x => x.ParseFile(file2, null)).ReturnsAsync(altReqs);
+
+            var rootA = new PipGraphNode(new PipComponent("a", "1.0"));
+            var rootB = new PipGraphNode(new PipComponent("b", "2.1"));
+            var rootC = new PipGraphNode(new PipComponent("c", "1.0"));
+            var rootD = new PipGraphNode(new PipComponent("d", "1.9"));
+            var rootE = new PipGraphNode(new PipComponent("e", "1.1"));
+
+            var red = new PipGraphNode(new PipComponent("red", "0.2"));
+            var green = new PipGraphNode(new PipComponent("green", "1.3"));
+            var blue = new PipGraphNode(new PipComponent("blue", "0.4"));
+
+            var cat = new PipGraphNode(new PipComponent("cat", "1.8"));
+            var lion = new PipGraphNode(new PipComponent("lion", "3.8"));
+            var dog = new PipGraphNode(new PipComponent("dog", "2.1"));
+
+            rootA.Children.Add(red);
+            rootB.Children.Add(green);
+            rootC.Children.AddRange(new[] { red, blue, });
+            rootD.Children.Add(cat);
+            green.Children.Add(cat);
+            cat.Children.Add(lion);
+            blue.Children.Add(cat);
+            blue.Children.Add(dog);
+
+            pythonResolver.Setup(x =>
+                x.ResolveRoots(It.Is>(p => p.Any(d => d.Name == "a"))))
+                .ReturnsAsync(new List { rootA, rootB, });
+
+            pythonResolver.Setup(x =>
+                x.ResolveRoots(It.Is>(p => p.Any(d => d.Name == "c"))))
+                .ReturnsAsync(new List { rootC, rootD, rootE, });
+
+            var (result, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("setup.py", string.Empty, fileLocation: file1)
+                                                    .WithFile("setup.py", string.Empty, fileLocation: file2)
+                                                    .ExecuteDetector();
+
+            var discoveredComponents = componentRecorder.GetDetectedComponents();
+
+            Assert.AreEqual(ProcessingResultCode.Success, result.ResultCode);
+            Assert.AreEqual(11, discoveredComponents.Count());
+
+            var rootIds = new[]
+            {
+                "a 1.0 - pip",
+                "b 2.1 - pip",
+                "c 1.0 - pip",
+                "d 1.9 - pip",
+                "e 1.1 - pip",
+            };
+
+            foreach (var rootId in rootIds)
+            {
+                componentRecorder.AssertAllExplicitlyReferencedComponents(
+                    rootId,
+                    x => x.Id == rootId);
+            }
+
+            CheckChild(componentRecorder, "red 0.2 - pip", new[] { "a 1.0 - pip", "c 1.0 - pip", });
+            CheckChild(componentRecorder, "green 1.3 - pip", new[] { "b 2.1 - pip", });
+            CheckChild(componentRecorder, "blue 0.4 - pip", new[] { "c 1.0 - pip", });
+            CheckChild(componentRecorder, "cat 1.8 - pip", new[] { "b 2.1 - pip", "c 1.0 - pip", "d 1.9 - pip", });
+            CheckChild(componentRecorder, "lion 3.8 - pip", new[] { "b 2.1 - pip", "c 1.0 - pip", "d 1.9 - pip", });
+            CheckChild(componentRecorder, "dog 2.1 - pip", new[] { "c 1.0 - pip", });
+
+            var graphsByLocations = componentRecorder.GetDependencyGraphsByLocation();
+            Assert.AreEqual(2, graphsByLocations.Count);
+
+            var graph1ComponentsWithDeps = new Dictionary
+            {
+                { "a 1.0 - pip", new[] { "red 0.2 - pip" } },
+                { "b 2.1 - pip", new[] { "green 1.3 - pip" } },
+                { "red 0.2 - pip", Array.Empty() },
+                { "green 1.3 - pip", new[] { "cat 1.8 - pip" } },
+                { "cat 1.8 - pip", new[] { "lion 3.8 - pip" } },
+                { "lion 3.8 - pip", Array.Empty() },
+            };
+
+            var graph1 = graphsByLocations[file1];
+            Assert.IsTrue(graph1ComponentsWithDeps.Keys.Take(2).All(graph1.IsComponentExplicitlyReferenced));
+            Assert.IsTrue(graph1ComponentsWithDeps.Keys.Skip(2).All(a => !graph1.IsComponentExplicitlyReferenced(a)));
+            CheckGraphStructure(graph1, graph1ComponentsWithDeps);
+
+            var graph2ComponentsWithDeps = new Dictionary
+            {
+                { "c 1.0 - pip", new[] { "red 0.2 - pip", "blue 0.4 - pip" } },
+                { "d 1.9 - pip", new[] { "cat 1.8 - pip" } },
+                { "e 1.1 - pip", Array.Empty() },
+                { "red 0.2 - pip", Array.Empty() },
+                { "blue 0.4 - pip", new[] { "cat 1.8 - pip", "dog 2.1 - pip" } },
+                { "cat 1.8 - pip", new[] { "lion 3.8 - pip" } },
+                { "dog 2.1 - pip", Array.Empty() },
+                { "lion 3.8 - pip", Array.Empty() },
+            };
+
+            var graph2 = graphsByLocations[file2];
+            Assert.IsTrue(graph2ComponentsWithDeps.Keys.Take(3).All(graph2.IsComponentExplicitlyReferenced));
+            Assert.IsTrue(graph2ComponentsWithDeps.Keys.Skip(3).All(a => !graph2.IsComponentExplicitlyReferenced(a)));
+            CheckGraphStructure(graph2, graph2ComponentsWithDeps);
+        }
+
+        private void CheckGraphStructure(IDependencyGraph graph, Dictionary graphComponentsWithDeps)
+        {
+            var graphComponents = graph.GetComponents().ToArray();
+            Assert.AreEqual(
+                graphComponentsWithDeps.Keys.Count,
+                graphComponents.Length,
+                $"Expected {graphComponentsWithDeps.Keys.Count} component to be recorded but got {graphComponents.Length} instead!");
+
+            foreach (var componentId in graphComponentsWithDeps.Keys)
+            {
+                Assert.IsTrue(
+                    graphComponents.Contains(componentId),
+                    $"Component `{componentId}` not recorded!");
+
+                var recordedDeps = graph.GetDependenciesForComponent(componentId).ToArray();
+                var expectedDeps = graphComponentsWithDeps[componentId];
+
+                Assert.AreEqual(
+                    expectedDeps.Length,
+                    recordedDeps.Length,
+                    $"Count missmatch of expected dependencies ({JsonConvert.SerializeObject(expectedDeps)}) and recorded dependencies ({JsonConvert.SerializeObject(recordedDeps)}) for `{componentId}`!");
+
+                foreach (var expectedDep in expectedDeps)
+                {
+                    Assert.IsTrue(
+                        recordedDeps.Contains(expectedDep),
+                        $"Expected `{expectedDep}` in the list of dependencies for `{componentId}` but only recorded: {JsonConvert.SerializeObject(recordedDeps)}");
+                }
+            }
+        }
+
+        private void CheckChild(IComponentRecorder recorder, string childId, string[] parentIds)
+        {
+            recorder.AssertAllExplicitlyReferencedComponents(
+                childId,
+                parentIds.Select(parentId => new Func(x => x.Id == parentId)).ToArray());
+        }
+
+        private List<(string, GitComponent)> ToGitTuple(IList components)
+        {
+            return components.Select(dep => (dep, null)).ToList();
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/PipDependencySpecifierTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/PipDependencySpecifierTests.cs
new file mode 100644
index 000000000..ec05f80a2
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/PipDependencySpecifierTests.cs
@@ -0,0 +1,34 @@
+using System.Collections.Generic;
+using Microsoft.ComponentDetection.Detectors.Pip;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    public class PipDependencySpecifierTests
+    {
+        [TestMethod]
+        public void TestPipDependencySpecifierConstruction()
+        {
+            List<(string, PipDependencySpecification)> specs = new List<(string, PipDependencySpecification)>
+            {
+                ("TestPackage==1.0", new PipDependencySpecification { Name = "TestPackage", DependencySpecifiers = new List { "==1.0" } }),
+                ("TestPackage>=1.0,!=1.1", new PipDependencySpecification { Name = "TestPackage", DependencySpecifiers = new List { ">=1.0", "!=1.1" } }),
+                ("OtherPackage!=1.2,>=1.0,<=1.9,~=1.4", new PipDependencySpecification { Name = "OtherPackage", DependencySpecifiers = new List { "!=1.2", ">=1.0", "<=1.9", "~=1.4" } }),
+            };
+
+            foreach (var spec in specs)
+            {
+                var (specString, referenceDependencySpecification) = spec;
+                var dependencySpecifier = new PipDependencySpecification(specString);
+
+                Assert.AreEqual(referenceDependencySpecification.Name, dependencySpecifier.Name);
+
+                for (int i = 0; i < referenceDependencySpecification.DependencySpecifiers.Count; i++)
+                {
+                    Assert.AreEqual(referenceDependencySpecification.DependencySpecifiers[i], dependencySpecifier.DependencySpecifiers[i]);
+                }
+            }
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/PipResolverTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/PipResolverTests.cs
new file mode 100644
index 000000000..3f4a4a77b
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/PipResolverTests.cs
@@ -0,0 +1,241 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Detectors.Pip;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class PipResolverTests
+    {
+        private Mock loggerMock;
+        private Mock pyPiClient;
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            loggerMock = new Mock();
+            pyPiClient = new Mock();
+        }
+
+        [TestMethod]
+        public async Task TestPipResolverSimpleGraph()
+        {
+            var a = new PipDependencySpecification("a==1.0");
+            var b = new PipDependencySpecification("b==1.0");
+            var c = new PipDependencySpecification("c==1.0");
+
+            var versions = new List { "1.0" };
+
+            var aReleases = CreateReleasesDictionary(versions);
+            var bReleases = CreateReleasesDictionary(versions);
+            var cReleases = CreateReleasesDictionary(versions);
+
+            pyPiClient.Setup(x => x.GetReleases(a)).ReturnsAsync(aReleases);
+            pyPiClient.Setup(x => x.GetReleases(b)).ReturnsAsync(bReleases);
+            pyPiClient.Setup(x => x.GetReleases(c)).ReturnsAsync(cReleases);
+
+            pyPiClient.Setup(x => x.FetchPackageDependencies("a", "1.0", aReleases["1.0"].First())).ReturnsAsync(new List { b });
+            pyPiClient.Setup(x => x.FetchPackageDependencies("b", "1.0", bReleases["1.0"].First())).ReturnsAsync(new List { c });
+            pyPiClient.Setup(x => x.FetchPackageDependencies("c", "1.0", cReleases["1.0"].First())).ReturnsAsync(new List { });
+
+            var dependencies = new List { a };
+
+            var resolver = new PythonResolver() { PypiClient = pyPiClient.Object, Logger = loggerMock.Object, };
+
+            var resolveResult = await resolver.ResolveRoots(dependencies);
+
+            Assert.IsNotNull(resolveResult);
+
+            var expectedA = new PipGraphNode(new PipComponent("a", "1.0"));
+            var expectedB = new PipGraphNode(new PipComponent("b", "1.0"));
+            var expectedC = new PipGraphNode(new PipComponent("c", "1.0"));
+
+            expectedA.Children.Add(expectedB);
+            expectedB.Parents.Add(expectedA);
+            expectedB.Children.Add(expectedC);
+            expectedC.Parents.Add(expectedB);
+
+            Assert.IsTrue(CompareGraphs(resolveResult.First(), expectedA));
+        }
+
+        [TestMethod]
+        public async Task TestPipResolverNonExistantRoot()
+        {
+            var a = new PipDependencySpecification("a==1.0");
+            var b = new PipDependencySpecification("b==1.0");
+            var c = new PipDependencySpecification("c==1.0");
+            var doesNotExist = new PipDependencySpecification("dne==1.0");
+
+            var versions = new List { "1.0" };
+
+            var aReleases = CreateReleasesDictionary(versions);
+            var bReleases = CreateReleasesDictionary(versions);
+            var cReleases = CreateReleasesDictionary(versions);
+
+            pyPiClient.Setup(x => x.GetReleases(a)).ReturnsAsync(aReleases);
+            pyPiClient.Setup(x => x.GetReleases(b)).ReturnsAsync(bReleases);
+            pyPiClient.Setup(x => x.GetReleases(c)).ReturnsAsync(cReleases);
+            pyPiClient.Setup(x => x.GetReleases(doesNotExist)).ReturnsAsync(CreateReleasesDictionary(new List()));
+
+            pyPiClient.Setup(x => x.FetchPackageDependencies("a", "1.0", aReleases["1.0"].First())).ReturnsAsync(new List { b });
+            pyPiClient.Setup(x => x.FetchPackageDependencies("b", "1.0", bReleases["1.0"].First())).ReturnsAsync(new List { c });
+            pyPiClient.Setup(x => x.FetchPackageDependencies("c", "1.0", cReleases["1.0"].First())).ReturnsAsync(new List { });
+
+            var dependencies = new List { a, doesNotExist };
+
+            var resolver = new PythonResolver() { PypiClient = pyPiClient.Object, Logger = loggerMock.Object, };
+
+            var resolveResult = await resolver.ResolveRoots(dependencies);
+
+            Assert.IsNotNull(resolveResult);
+
+            var expectedA = new PipGraphNode(new PipComponent("a", "1.0"));
+            var expectedB = new PipGraphNode(new PipComponent("b", "1.0"));
+            var expectedC = new PipGraphNode(new PipComponent("c", "1.0"));
+
+            expectedA.Children.Add(expectedB);
+            expectedB.Parents.Add(expectedA);
+            expectedB.Children.Add(expectedC);
+            expectedC.Parents.Add(expectedB);
+
+            Assert.IsTrue(CompareGraphs(resolveResult.First(), expectedA));
+        }
+
+        [TestMethod]
+        public async Task TestPipResolverNonExistantLeaf()
+        {
+            var a = new PipDependencySpecification("a==1.0");
+            var b = new PipDependencySpecification("b==1.0");
+            var c = new PipDependencySpecification("c==1.0");
+
+            var versions = new List { "1.0" };
+
+            var aReleases = CreateReleasesDictionary(versions);
+            var bReleases = CreateReleasesDictionary(versions);
+            var cReleases = CreateReleasesDictionary(versions);
+
+            pyPiClient.Setup(x => x.GetReleases(a)).ReturnsAsync(aReleases);
+            pyPiClient.Setup(x => x.GetReleases(b)).ReturnsAsync(bReleases);
+            pyPiClient.Setup(x => x.GetReleases(c)).ReturnsAsync(CreateReleasesDictionary(new List()));
+
+            pyPiClient.Setup(x => x.FetchPackageDependencies("a", "1.0", aReleases["1.0"].First())).ReturnsAsync(new List { b });
+            pyPiClient.Setup(x => x.FetchPackageDependencies("b", "1.0", bReleases["1.0"].First())).ReturnsAsync(new List { c });
+
+            var dependencies = new List { a };
+
+            var resolver = new PythonResolver() { PypiClient = pyPiClient.Object, Logger = loggerMock.Object, };
+
+            var resolveResult = await resolver.ResolveRoots(dependencies);
+
+            Assert.IsNotNull(resolveResult);
+
+            var expectedA = new PipGraphNode(new PipComponent("a", "1.0"));
+            var expectedB = new PipGraphNode(new PipComponent("b", "1.0"));
+
+            expectedA.Children.Add(expectedB);
+            expectedB.Parents.Add(expectedA);
+
+            Assert.IsTrue(CompareGraphs(resolveResult.First(), expectedA));
+            pyPiClient.Verify(x => x.FetchPackageDependencies(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2));
+        }
+
+        [TestMethod]
+        public async Task TestPipResolverBacktrack()
+        {
+            var a = new PipDependencySpecification("a==1.0");
+            var b = new PipDependencySpecification("b==1.0");
+            var c = new PipDependencySpecification("c<=1.1");
+            var cAlt = new PipDependencySpecification("c==1.0");
+
+            var versions = new List { "1.0" };
+
+            var otherVersions = new List { "1.0", "1.1" };
+
+            var aReleases = CreateReleasesDictionary(versions);
+            var bReleases = CreateReleasesDictionary(versions);
+            var cReleases = CreateReleasesDictionary(otherVersions);
+
+            pyPiClient.Setup(x => x.GetReleases(a)).ReturnsAsync(aReleases);
+            pyPiClient.Setup(x => x.GetReleases(b)).ReturnsAsync(bReleases);
+            pyPiClient.Setup(x => x.GetReleases(c)).ReturnsAsync(cReleases);
+
+            pyPiClient.Setup(x => x.FetchPackageDependencies("a", "1.0", aReleases["1.0"].First())).ReturnsAsync(new List { b, c });
+            pyPiClient.Setup(x => x.FetchPackageDependencies("b", "1.0", bReleases["1.0"].First())).ReturnsAsync(new List { cAlt });
+            pyPiClient.Setup(x => x.FetchPackageDependencies("c", "1.1", cReleases["1.1"].First())).ReturnsAsync(new List { });
+            pyPiClient.Setup(x => x.FetchPackageDependencies("c", "1.0", cReleases["1.0"].First())).ReturnsAsync(new List { });
+
+            var dependencies = new List { a };
+
+            var resolver = new PythonResolver() { PypiClient = pyPiClient.Object, Logger = loggerMock.Object, };
+
+            var resolveResult = await resolver.ResolveRoots(dependencies);
+
+            Assert.IsNotNull(resolveResult);
+
+            var expectedA = new PipGraphNode(new PipComponent("a", "1.0"));
+            var expectedB = new PipGraphNode(new PipComponent("b", "1.0"));
+            var expectedC = new PipGraphNode(new PipComponent("c", "1.0"));
+
+            expectedA.Children.Add(expectedB);
+            expectedA.Children.Add(expectedC);
+            expectedB.Parents.Add(expectedA);
+            expectedB.Children.Add(expectedC);
+            expectedC.Parents.Add(expectedA);
+            expectedC.Parents.Add(expectedB);
+
+            Assert.IsTrue(CompareGraphs(resolveResult.First(), expectedA));
+            pyPiClient.Verify(x => x.FetchPackageDependencies(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(4));
+        }
+
+        private bool CompareGraphs(PipGraphNode a, PipGraphNode b)
+        {
+            var componentA = (PipComponent)a.Value;
+            var componentB = (PipComponent)b.Value;
+
+            if (!string.Equals(componentA.Name, componentB.Name, StringComparison.OrdinalIgnoreCase) ||
+                !string.Equals(componentA.Version, componentB.Version, StringComparison.OrdinalIgnoreCase))
+            {
+                return false;
+            }
+
+            if (a.Children.Count != b.Children.Count)
+            {
+                return false;
+            }
+
+            bool valid = true;
+
+            for (int i = 0; i < a.Children.Count; i++)
+            {
+                valid = CompareGraphs(a.Children[i], b.Children[i]);
+            }
+
+            return valid;
+        }
+
+        private SortedDictionary> CreateReleasesDictionary(IList versions)
+        {
+            var toReturn = new SortedDictionary>(new PythonVersionComparer());
+
+            foreach (var version in versions)
+            {
+                toReturn.Add(version, new List { CreatePythonProjectRelease() });
+            }
+
+            return toReturn;
+        }
+
+        private PythonProjectRelease CreatePythonProjectRelease()
+        {
+            return new PythonProjectRelease { PackageType = "bdist_wheel", PythonVersion = "3.5.2", Size = 1000, Url = new Uri($"https://{Guid.NewGuid()}") };
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs
new file mode 100644
index 000000000..ed0096c47
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs
@@ -0,0 +1,354 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Common.DependencyGraph;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Detectors.Pnpm;
+using Microsoft.ComponentDetection.Detectors.Tests.Utilities;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Microsoft.ComponentDetection.TestsUtilities;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class PnpmDetectorTests
+    {
+        private DetectorTestUtility detectorTestUtility;
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            var componentRecorder = new ComponentRecorder(enableManualTrackingOfExplicitReferences: false);
+            detectorTestUtility = DetectorTestUtilityCreator.Create()
+                                    .WithScanRequest(new ScanRequest(new DirectoryInfo(Path.GetTempPath()), null, null, new Dictionary(), null, componentRecorder));
+        }
+
+        [TestMethod]
+        public async Task TestPnpmDetector_SingleFileLocatesExpectedInput()
+        {
+            var yamlFile = @"
+dependencies:
+  'query-string': 4.3.4,
+  '@babel/helper-compilation-targets': 7.10.4_@babel+core@7.10.5
+
+packages:
+  /query-string-🙌/4.3.4:
+    dependencies:
+      object-assign: 4.1.1
+      strict-uri-encode: 1.1.0
+      test: 1.0.0
+    dev: true
+    engines:
+      node: '>=0.10.0'
+    resolution:
+      integrity: sha1-u7aTucqRXCMlFbIosaArYJBD2+s=
+  /object-assign/4.1.1:
+    dev: true
+    engines:
+      node: '>=0.10.0'
+    resolution:
+      integrity: sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
+  /strict-uri-encode/1.1.0:
+    engines:
+      node: '>=0.10.0'
+    resolution:
+      integrity: sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=
+  /test/1.0.0:
+    dev: true
+    engines:
+      node: '>=0.10.0'
+    resolution:
+      integrity: sha1-A5siXfHVgrH1TmWt3UNS4Y+qBxM=
+  /@babel/helper-compilation-targets/7.10.4_@babel+core@7.10.5:
+    dev: false
+registry: 'https://test/registry'
+shrinkwrapMinorVersion: 7
+shrinkwrapVersion: 3";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("shrinkwrap1.yaml", yamlFile)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(5, detectedComponents.Count());
+
+            var queryString = detectedComponents.Single(component => ((NpmComponent)component.Component).Name.Contains("query-string"));
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                queryString.Component.Id,
+                parentComponent => parentComponent.Name == "query-string-🙌");
+
+            Assert.AreEqual("4.3.4", ((NpmComponent)queryString.Component).Version);
+            Assert.IsTrue(componentRecorder.GetEffectiveDevDependencyValue(queryString.Component.Id).GetValueOrDefault(false));
+
+            var objectAssign = detectedComponents.Single(component => ((NpmComponent)component.Component).Name.Contains("object-assign"));
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                objectAssign.Component.Id,
+                parentComponent => parentComponent.Name == "query-string-🙌" && parentComponent.Version == "4.3.4");
+            Assert.AreEqual("4.1.1", ((NpmComponent)objectAssign.Component).Version);
+            Assert.IsTrue(componentRecorder.GetEffectiveDevDependencyValue(objectAssign.Component.Id).GetValueOrDefault(false));
+
+            var strictUriEncode = detectedComponents.Single(component => ((NpmComponent)component.Component).Name.Contains("strict-uri-encode"));
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                strictUriEncode.Component.Id,
+                parentComponent => parentComponent.Name == "query-string-🙌" && parentComponent.Version == "4.3.4");
+            Assert.AreEqual("1.1.0", ((NpmComponent)strictUriEncode.Component).Version);
+            Assert.IsFalse(componentRecorder.GetEffectiveDevDependencyValue(strictUriEncode.Component.Id).GetValueOrDefault(true));
+
+            var babelHelperCompilation = detectedComponents.Single(component => ((NpmComponent)component.Component).Name.Contains("helper-compilation-targets"));
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                babelHelperCompilation.Component.Id,
+                parentComponent => parentComponent.Name == "@babel/helper-compilation-targets" && parentComponent.Version == "7.10.4");
+            Assert.IsFalse(componentRecorder.GetEffectiveDevDependencyValue(babelHelperCompilation.Component.Id).GetValueOrDefault(true));
+
+            var test = detectedComponents.Single(component => ((NpmComponent)component.Component).Name.Contains("test"));
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                test.Component.Id,
+                parentComponent => parentComponent.Name == "query-string-🙌" && parentComponent.Version == "4.3.4");
+            Assert.IsTrue(componentRecorder.GetEffectiveDevDependencyValue(test.Component.Id).GetValueOrDefault(false));
+
+            componentRecorder.ForAllComponents(grouping =>
+            {
+                Assert.IsTrue(grouping.AllFileLocations.First().Contains("shrinkwrap1.yaml"));
+            });
+
+            foreach (var component in detectedComponents)
+            {
+                Assert.AreEqual(component.Component.Type, ComponentType.Npm);
+            }
+        }
+
+        [TestMethod]
+        public async Task TestPnpmDetector_SameComponentMergesRootsAndLocationsAcrossMultipleFiles()
+        {
+            var yamlFile1 = @"
+dependencies:
+  'query-string': 4.3.4
+packages:
+  /query-string/4.3.4:
+    dependencies:
+      strict-uri-encode: 1.1.0
+    dev: false
+    engines:
+      node: '>=0.10.0'
+    resolution:
+      integrity: sha1-u7aTucqRXCMlFbIosaArYJBD2+s=
+  /strict-uri-encode/1.1.0:
+    dev: false
+    engines:
+      node: '>=0.10.0'
+    resolution:
+      integrity: sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=
+registry: 'https://test/registry'
+shrinkwrapMinorVersion: 7
+shrinkwrapVersion: 3";
+
+            var yamlFile2 = @"
+dependencies:
+  'some-other-root': 1.2.3
+packages:
+  /some-other-root/1.2.3:
+    dependencies:
+      strict-uri-encode: 1.1.0
+    dev: false
+    engines:
+      node: '>=0.10.0'
+    resolution:
+      integrity: sha1-u7aTucqRXCMlFbIosaArYJBD2+s=
+  /strict-uri-encode/1.1.0:
+    dev: false
+    engines:
+      node: '>=0.10.0'
+    resolution:
+      integrity: sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=
+registry: 'https://test/registry'
+shrinkwrapMinorVersion: 7
+shrinkwrapVersion: 3";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("shrinkwrap1.yaml", yamlFile1)
+                                                    .WithFile("shrinkwrap2.yaml", yamlFile2)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(3, detectedComponents.Count());
+            var strictUriEncodeComponent = detectedComponents.Select(x => new { Component = x.Component as NpmComponent, DetectedComponent = x }).FirstOrDefault(x => x.Component.Name.Contains("strict-uri-encode"));
+
+            Assert.IsNotNull(strictUriEncodeComponent);
+
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                strictUriEncodeComponent.Component.Id,
+                parentComponent => parentComponent.Name == "some-other-root", parentComponent => parentComponent.Name == "query-string");
+
+            componentRecorder.ForOneComponent(strictUriEncodeComponent.Component.Id, grouping => Assert.AreEqual(2, grouping.AllFileLocations.Count()));
+        }
+
+        [TestMethod]
+        public async Task TestPnpmDetector_SpecialDependencyVersionStringDoesntBlowUsUp()
+        {
+            var yamlFile1 = @"
+dependencies:
+  'query-string': 4.3.4
+packages:
+  /query-string/4.3.4:
+    dependencies:
+      '@ms/items-view': /@ms/items-view/0.128.9/react-dom@15.6.2+react@15.6.2
+    dev: false
+    engines:
+      node: '>=0.10.0'
+    resolution:
+      integrity: sha1-u7aTucqRXCMlFbIosaArYJBD2+s=
+  /@ms/items-view/0.128.9/react-dom@15.6.2+react@15.6.2:
+    dev: false
+    engines:
+      node: '>=0.10.0'
+    resolution:
+      integrity: sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=
+registry: 'https://test/registry'
+shrinkwrapMinorVersion: 7
+shrinkwrapVersion: 3";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("shrinkwrap1.yaml", yamlFile1)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(2, detectedComponents.Count());
+            var msItemsViewComponent = detectedComponents.Select(x => new { Component = x.Component as NpmComponent, DetectedComponent = x }).FirstOrDefault(x => x.Component.Name.Contains("@ms/items-view"));
+
+            Assert.IsNotNull(msItemsViewComponent);
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                msItemsViewComponent.Component.Id,
+                parentComponent => parentComponent.Name == "query-string");
+        }
+
+        [TestMethod]
+        public async Task TestPnpmDetector_DetectorRecognizeDevDependenciesValues()
+        {
+            var yamlFile1 = @"
+                dependencies:
+                  'query-string': 4.3.4,
+                  'strict-uri-encode': 1.1.0
+                packages:
+                  /query-string/4.3.4:
+                    dev: false
+                  /strict-uri-encode/1.1.0:
+                    dev: true";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("shrinkwrap1.yaml", yamlFile1)
+                                                    .ExecuteDetector();
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            var noDevDependencyComponent = detectedComponents.Select(x => new { Component = x.Component as NpmComponent, DetectedComponent = x }).FirstOrDefault(x => x.Component.Name.Contains("query-string"));
+            var devDependencyComponent = detectedComponents.Select(x => new { Component = x.Component as NpmComponent, DetectedComponent = x }).FirstOrDefault(x => x.Component.Name.Contains("strict-uri-encode"));
+
+            componentRecorder.GetEffectiveDevDependencyValue(noDevDependencyComponent.Component.Id).Should().BeFalse();
+            componentRecorder.GetEffectiveDevDependencyValue(devDependencyComponent.Component.Id).Should().BeTrue();
+        }
+
+        [TestMethod]
+        public async Task TestPnpmDetector_DetectorRecognizeDevDependenciesValues_InWeirdCases()
+        {
+            var yamlFile1 = @"
+                dependencies:
+                  'query-string': 4.3.4,
+                  'strict-uri-encode': 1.1.0
+                packages:
+                  /query-string/4.3.4:
+                    dependencies:
+                      solo-non-dev-dep: 0.1.2
+                      shared-non-dev-dep: 0.1.2
+                    dev: false
+                  /strict-uri-encode/1.1.0:
+                    dependencies:
+                      solo-dev-dep: 0.1.2
+                      shared-non-dev-dep: 0.1.2
+                    dev: true";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("shrinkwrap1.yaml", yamlFile1)
+                                                    .ExecuteDetector();
+
+            componentRecorder.GetEffectiveDevDependencyValue("solo-non-dev-dep 0.1.2 - Npm").Value.Should().BeFalse();
+            componentRecorder.GetEffectiveDevDependencyValue("solo-dev-dep 0.1.2 - Npm").Value.Should().BeTrue();
+            componentRecorder.GetEffectiveDevDependencyValue("shared-non-dev-dep 0.1.2 - Npm").Value.Should().BeFalse();
+        }
+
+        [TestMethod]
+        public async Task TestPnpmDetector_HandlesMalformedYaml()
+        {
+            // This is a clearly malformed Yaml. We expect parsing it to "succeed" but find no components
+            var yamlFile1 = @"dependencies";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("shrinkwrap1.yaml", yamlFile1)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(0, componentRecorder.GetDetectedComponents().Count());
+        }
+
+        [TestMethod]
+        public async Task TestPnpmDetector_DependencyGraphIsCreated()
+        {
+            var yamlFile = @"
+dependencies:
+  'query-string': 4.3.4,
+
+packages:
+  /query-string/4.3.4:
+    dependencies:
+      object-assign: 4.1.1
+      test: 1.0.0
+    dev: false
+  /object-assign/4.1.1:
+    dependencies:
+      strict-uri-encode: 1.1.0
+    dev: false
+  /strict-uri-encode/1.1.0:
+    dev: false
+  /test/1.0.0:
+    dev: true";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("shrinkwrap1.yaml", yamlFile)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(4, componentRecorder.GetDetectedComponents().Count());
+
+            var queryStringComponentId = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPath("/query-string/4.3.4").Component.Id;
+            var objectAssignComponentId = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPath("/object-assign/4.1.1").Component.Id;
+            var strictUriComponentId = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPath("/strict-uri-encode/1.1.0").Component.Id;
+            var testComponentId = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPath("/test/1.0.0").Component.Id;
+
+            var dependencyGraph = componentRecorder.GetDependencyGraphsByLocation().Values.First();
+
+            var queryStringDependencies = dependencyGraph.GetDependenciesForComponent(queryStringComponentId);
+            Assert.AreEqual(2, queryStringDependencies.Count());
+            Assert.IsTrue(queryStringDependencies.Contains(objectAssignComponentId));
+            Assert.IsTrue(queryStringDependencies.Contains(testComponentId));
+
+            var objectAssignDependencies = dependencyGraph.GetDependenciesForComponent(objectAssignComponentId);
+            Assert.AreEqual(1, objectAssignDependencies.Count());
+            Assert.IsTrue(objectAssignDependencies.Contains(strictUriComponentId));
+
+            var stringUriDependencies = dependencyGraph.GetDependenciesForComponent(strictUriComponentId);
+            Assert.AreEqual(0, stringUriDependencies.Count());
+
+            var testDependencies = dependencyGraph.GetDependenciesForComponent(testComponentId);
+            Assert.AreEqual(0, testDependencies.Count());
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmParsingUtilitiesTest.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmParsingUtilitiesTest.cs
new file mode 100644
index 000000000..4260ba4cb
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmParsingUtilitiesTest.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Detectors.Pnpm;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Microsoft.ComponentDetection.TestsUtilities;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    public class PnpmParsingUtilitiesTest
+    {
+        [TestMethod]
+        public async Task DeserializePnpmYamlFile()
+        {
+            var yamlFile = @"
+dependencies:
+  'query-string': 4.3.4
+packages:
+  /query-string/4.3.4:
+    dependencies:
+      '@ms/items-view': /@ms/items-view/0.128.9/react-dom@15.6.2+react@15.6.2
+    dev: false
+    engines:
+      node: '>=0.10.0'
+    resolution:
+      integrity: sha1-u7aTucqRXCMlFbIosaArYJBD2+s=
+  /@ms/items-view/0.128.9/react-dom@15.6.2+react@15.6.2:
+    dev: true
+    engines:
+      node: '>=0.10.0'
+    resolution:
+      integrity: sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=
+registry: 'https://test/registry'
+shrinkwrapMinorVersion: 7
+shrinkwrapVersion: 3";
+
+            var parsedYaml = await PnpmParsingUtilities.DeserializePnpmYamlFile(CreateComponentStreamForShrinkwrap(yamlFile));
+
+            parsedYaml.packages.Should().HaveCount(2);
+            parsedYaml.packages.Should().ContainKey("/query-string/4.3.4");
+            parsedYaml.packages.Should().ContainKey("/@ms/items-view/0.128.9/react-dom@15.6.2+react@15.6.2");
+
+            var queryStringPackage = parsedYaml.packages["/query-string/4.3.4"];
+            queryStringPackage.dependencies.Should().HaveCount(1);
+            queryStringPackage.dependencies.Should().ContainKey("@ms/items-view");
+            queryStringPackage.dependencies["@ms/items-view"].Should().BeEquivalentTo("/@ms/items-view/0.128.9/react-dom@15.6.2+react@15.6.2");
+            queryStringPackage.dev.Should().BeEquivalentTo("false");
+
+            var itemViewPackage = parsedYaml.packages["/@ms/items-view/0.128.9/react-dom@15.6.2+react@15.6.2"];
+            itemViewPackage.dependencies.Should().BeNull();
+            itemViewPackage.dev.Should().BeEquivalentTo("true");
+        }
+
+        [TestMethod]
+        public void CreateDetectedComponentFromPnpmPath()
+        {
+            var detectedComponent1 = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPath("/@ms/items-view/0.128.9/react-dom@15.6.2+react@15.6.2");
+            detectedComponent1.Should().NotBeNull();
+            detectedComponent1.Component.Should().NotBeNull();
+            ((NpmComponent)detectedComponent1.Component).Name.Should().BeEquivalentTo("@ms/items-view");
+            ((NpmComponent)detectedComponent1.Component).Version.Should().BeEquivalentTo("0.128.9");
+
+            var detectedComponent2 = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPath("/@babel/helper-compilation-targets/7.10.4_@babel+core@7.10.5");
+            detectedComponent2.Should().NotBeNull();
+            detectedComponent2.Component.Should().NotBeNull();
+            ((NpmComponent)detectedComponent2.Component).Name.Should().BeEquivalentTo("@babel/helper-compilation-targets");
+            ((NpmComponent)detectedComponent2.Component).Version.Should().BeEquivalentTo("7.10.4");
+
+            var detectedComponent3 = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPath("/query-string/4.3.4");
+            detectedComponent3.Should().NotBeNull();
+            detectedComponent3.Component.Should().NotBeNull();
+            ((NpmComponent)detectedComponent3.Component).Name.Should().BeEquivalentTo("query-string");
+            ((NpmComponent)detectedComponent3.Component).Version.Should().BeEquivalentTo("4.3.4");
+        }
+
+        [TestMethod]
+        public void IsPnpmPackageDevDependency()
+        {
+            var pnpmPackage = new Package();
+
+            pnpmPackage.dev = "true";
+            PnpmParsingUtilities.IsPnpmPackageDevDependency(pnpmPackage).Should().BeTrue();
+
+            pnpmPackage.dev = "TRUE";
+            PnpmParsingUtilities.IsPnpmPackageDevDependency(pnpmPackage).Should().BeTrue();
+
+            pnpmPackage.dev = "false";
+            PnpmParsingUtilities.IsPnpmPackageDevDependency(pnpmPackage).Should().BeFalse();
+
+            pnpmPackage.dev = "FALSE";
+            PnpmParsingUtilities.IsPnpmPackageDevDependency(pnpmPackage).Should().BeFalse();
+
+            pnpmPackage.dev = string.Empty;
+            PnpmParsingUtilities.IsPnpmPackageDevDependency(pnpmPackage).Should().BeFalse();
+
+            pnpmPackage.dev = null;
+            PnpmParsingUtilities.IsPnpmPackageDevDependency(pnpmPackage).Should().BeFalse();
+
+            Action action = () => PnpmParsingUtilities.IsPnpmPackageDevDependency(null);
+            action.Should().Throw();
+        }
+
+        private IComponentStream CreateComponentStreamForShrinkwrap(string content)
+        {
+            var packageLockMock = new Mock();
+            packageLockMock.SetupGet(x => x.Stream).Returns(content.ToStream());
+            packageLockMock.SetupGet(x => x.Pattern).Returns("test");
+            packageLockMock.SetupGet(x => x.Location).Returns("test");
+
+            return packageLockMock.Object;
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/PodDetectorTest.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/PodDetectorTest.cs
new file mode 100644
index 000000000..166b82354
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/PodDetectorTest.cs
@@ -0,0 +1,597 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Detectors.CocoaPods;
+using Microsoft.ComponentDetection.Detectors.Tests.Utilities;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Microsoft.ComponentDetection.TestsUtilities;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class PodDetectorTest
+    {
+        private DetectorTestUtility detectorTestUtility;
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            detectorTestUtility = DetectorTestUtilityCreator.Create();
+        }
+
+        [TestMethod]
+        public async Task TestPodDetector_EmptyPodfileLock()
+        {
+            var podfileLockContent = @"PODFILE CHECKSUM: b3f970aecf9d240064c3b1737d975c9cb179c851
+
+COCOAPODS: 1.4.0.beta.1";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("Podfile.lock", podfileLockContent)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(0, componentRecorder.GetDetectedComponents().Count());
+        }
+
+        [TestMethod]
+        public async Task TestPodDetector_DetectorRecognizePodComponents()
+        {
+            var podfileLockContent = @"PODS:
+  - AzureCore (0.5.0):
+    - KeychainAccess (~> 3.2)
+    - Willow (~> 5.2)
+  - AzureData (0.5.0):
+    - AzureCore (= 0.5.0)
+  - AzureMobile (0.5.0):
+    - AzureData (= 0.5.0)
+  - KeychainAccess (3.2.1)
+  - Willow (5.2.1)
+
+DEPENDENCIES:
+  - AzureMobile (~> 0.5.0)
+
+SPEC CHECKSUMS:
+  AzureCore: 9f6c42e03d59a13b508bff356a85cd9438b654fb
+  AzureData: f423992bd28e1006e3c358d3e3ce60d71f8ba090
+  AzureMobile: 4fd580aa2f73f4a8ac463971b4a5483afd586f2a
+  KeychainAccess: d5470352939ced6d6f7fb51cb2e67aae51fc294f
+  Willow: a6310f9aedcb6f4de8c35b94fd3416a660ae9280
+
+COCOAPODS: 0.39.0";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("Podfile.lock", podfileLockContent)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(5, detectedComponents.Count());
+
+            AssertPodComponentNameAndVersion(detectedComponents, "AzureCore", "0.5.0");
+            AssertPodComponentNameAndVersion(detectedComponents, "AzureData", "0.5.0");
+            AssertPodComponentNameAndVersion(detectedComponents, "AzureMobile", "0.5.0");
+            AssertPodComponentNameAndVersion(detectedComponents, "KeychainAccess", "3.2.1");
+            AssertPodComponentNameAndVersion(detectedComponents, "Willow", "5.2.1");
+        }
+
+        [TestMethod]
+        public async Task TestPodDetector_DetectorRecognizeSubspecsAsSinglePodComponent()
+        {
+            var podfileLockContent = @"PODS:
+  - MSAL/app-lib (1.0.7)
+  - MSAL/extension (1.0.7)
+  - MSGraphClientSDK (1.0.0):
+    - MSGraphClientSDK/Authentication (= 1.0.0)
+    - MSGraphClientSDK/Common (= 1.0.0)
+  - MSGraphClientSDK/Authentication (1.0.0)
+  - MSGraphClientSDK/Common (1.0.0):
+    - MSGraphClientSDK/Authentication
+
+DEPENDENCIES:
+  - MSAL
+  - MSGraphClientSDK
+
+SPEC CHECKSUMS:
+  MSAL: e4c1cbcf59e04073b427ce9fbfc0346b54abb62e
+  MSGraphClientSDK: ffc07a58a838e0702c7bf2a856367035d4a335d7
+
+PODFILE CHECKSUM: accace11c2720ac62a63c1b7629cc202a7e108b8
+
+COCOAPODS: 1.8.4";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("Podfile.lock", podfileLockContent)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(2, detectedComponents.Count());
+
+            AssertPodComponentNameAndVersion(detectedComponents, "MSAL", "1.0.7");
+            AssertPodComponentNameAndVersion(detectedComponents, "MSGraphClientSDK", "1.0.0");
+        }
+
+        [TestMethod]
+        public async Task TestPodDetector_DetectorRecognizeGitComponents()
+        {
+            var podfileLockContent = @"PODS:
+  - MSGraphClientSDK (1.0.0):
+    - MSGraphClientSDK/Authentication (= 1.0.0)
+    - MSGraphClientSDK/Common (= 1.0.0)
+  - MSGraphClientSDK/Authentication (1.0.0)
+  - MSGraphClientSDK/Common (1.0.0):
+    - MSGraphClientSDK/Authentication
+
+DEPENDENCIES:
+  - MSGraphClientSDK (from `https://github.com/microsoftgraph/msgraph-sdk-objc.git`, branch `main`)
+
+EXTERNAL SOURCES:
+  MSGraphClientSDK:
+    :branch: main
+    :git: https://github.com/microsoftgraph/msgraph-sdk-objc.git
+
+CHECKOUT OPTIONS:
+  MSGraphClientSDK:
+    :commit: da7223e3c455fe558de361c611df36c6dcc4229d
+    :git: https://github.com/microsoftgraph/msgraph-sdk-objc.git
+
+SPEC CHECKSUMS:
+  MSGraphClientSDK: ffc07a58a838e0702c7bf2a856367035d4a335d7
+
+PODFILE CHECKSUM: accace11c2720ac62a63c1b7629cc202a7e108b8
+
+COCOAPODS: 1.8.4";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("Podfile.lock", podfileLockContent)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(1, detectedComponents.Count());
+
+            AssertGitComponentHashAndUrl(detectedComponents, "da7223e3c455fe558de361c611df36c6dcc4229d", "https://github.com/microsoftgraph/msgraph-sdk-objc.git");
+        }
+
+        [TestMethod]
+        public async Task TestPodDetector_DetectorRecognizeGitComponentsWithTagsAsPodComponents()
+        {
+            var podfileLockContent = @"PODS:
+  - MSGraphClientSDK (1.0.0):
+    - MSGraphClientSDK/Authentication (= 1.0.0)
+    - MSGraphClientSDK/Common (= 1.0.0)
+  - MSGraphClientSDK/Authentication (1.0.0)
+  - MSGraphClientSDK/Common (1.0.0):
+    - MSGraphClientSDK/Authentication
+
+DEPENDENCIES:
+  - MSGraphClientSDK (from `https://github.com/microsoftgraph/msgraph-sdk-objc.git`, tag `1.0.0`)
+
+EXTERNAL SOURCES:
+  MSGraphClientSDK:
+    :branch: main
+    :git: https://github.com/microsoftgraph/msgraph-sdk-objc.git
+
+CHECKOUT OPTIONS:
+  MSGraphClientSDK:
+    :git: https://github.com/microsoftgraph/msgraph-sdk-objc.git
+    :tag: 1.0.0
+
+SPEC CHECKSUMS:
+  MSGraphClientSDK: ffc07a58a838e0702c7bf2a856367035d4a335d7
+
+PODFILE CHECKSUM: accace11c2720ac62a63c1b7629cc202a7e108b8
+
+COCOAPODS: 1.8.4";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("Podfile.lock", podfileLockContent)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(1, detectedComponents.Count());
+
+            AssertPodComponentNameAndVersion(detectedComponents, "MSGraphClientSDK", "1.0.0");
+        }
+
+        [TestMethod]
+        public async Task TestPodDetector_DetectorRecognizePodComponentsFromExternalPodspecs()
+        {
+            var podfileLockContent = @"PODS:
+  - CocoaLumberjack (3.6.0):
+    - CocoaLumberjack/Core (= 3.6.0)
+  - CocoaLumberjack/Core (3.6.0)
+  - SVGKit (2.1.0):
+    - CocoaLumberjack (~> 3.0)
+
+EXTERNAL SOURCES:
+  SVGKit:
+    :podspec: ""https://example.com/SVGKit.podspec""
+
+DEPENDENCIES:
+  - SVGKit (from `https://example.com/SVGKit.podspec`)
+
+SPEC CHECKSUMS:
+  CocoaLumberjack: 78b0c238666f4f58db069738ec176f4519557516
+  SVGKit: 8a2fc74258bdb2abb54d3b65f3dd68b0277a9c4d
+
+PODFILE CHECKSUM: accace11c2720ac62a63c1b7629cc202a7e108b8
+
+COCOAPODS: 1.8.4";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("Podfile.lock", podfileLockContent)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(2, detectedComponents.Count());
+
+            AssertPodComponentNameAndVersion(detectedComponents, "CocoaLumberjack", "3.6.0");
+            AssertPodComponentNameAndVersion(detectedComponents, "SVGKit", "2.1.0");
+        }
+
+        [TestMethod]
+        public async Task TestPodDetector_DetectorRecognizePodComponentsFromLocalPath()
+        {
+            var podfileLockContent = @"PODS:
+  - Keys (1.0.1)
+
+EXTERNAL SOURCES:
+  Keys:
+    :path: Pods/CocoaPodsKeys
+
+DEPENDENCIES:
+  - Keys (from `Pods/CocoaPodsKeys`)
+
+SPEC CHECKSUMS:
+  Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9
+
+PODFILE CHECKSUM: accace11c2720ac62a63c1b7629cc202a7e108b8
+
+COCOAPODS: 1.8.4";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("Podfile.lock", podfileLockContent)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(1, detectedComponents.Count());
+
+            AssertPodComponentNameAndVersion(detectedComponents, "Keys", "1.0.1");
+        }
+
+        [TestMethod]
+        public async Task TestPodDetector_MultiplePodfileLocks()
+        {
+            var podfileLockContent = @"PODS:
+  - AzureCore (0.5.0):
+    - KeychainAccess (~> 3.2)
+    - Willow (~> 5.2)
+  - AzureData (0.5.0):
+    - AzureCore (= 0.5.0)
+  - AzureMobile (0.5.0):
+    - AzureData (= 0.5.0)
+  - KeychainAccess (3.2.1)
+  - Willow (5.2.1)
+
+DEPENDENCIES:
+  - AzureMobile (= 0.5.0)
+
+SPEC CHECKSUMS:
+  AzureCore: 9f6c42e03d59a13b508bff356a85cd9438b654fb
+  AzureData: f423992bd28e1006e3c358d3e3ce60d71f8ba090
+  AzureMobile: 4fd580aa2f73f4a8ac463971b4a5483afd586f2a
+  KeychainAccess: d5470352939ced6d6f7fb51cb2e67aae51fc294f
+  Willow: a6310f9aedcb6f4de8c35b94fd3416a660ae9280
+
+COCOAPODS: 0.39.0";
+
+            var podfileLockContent2 = @"PODS:
+  - AzureCore (0.5.1):
+    - KeychainAccess (~> 3.2)
+    - Willow (~> 5.2)
+  - CocoaLumberjack (3.6.0):
+    - CocoaLumberjack/Core (= 3.6.0)
+  - CocoaLumberjack/Core (3.6.0)
+  - KeychainAccess (3.2.1)
+  - SVGKit (2.1.0):
+    - CocoaLumberjack (~> 3.0)
+  - Willow (5.2.1)
+
+DEPENDENCIES:
+  - SVGKit (~> 2.0)
+  - AzureCore (= 0.5.1)
+
+SPEC CHECKSUMS:
+  AzureCore: 9f6c42e03d59a13b508bff356a85cd9438b654fb
+  CocoaLumberjack: 78b0c238666f4f58db069738ec176f4519557516
+  KeychainAccess: d5470352939ced6d6f7fb51cb2e67aae51fc294f
+  SVGKit: 8a2fc74258bdb2abb54d3b65f3dd68b0277a9c4d
+  Willow: a6310f9aedcb6f4de8c35b94fd3416a660ae9280
+
+PODFILE CHECKSUM: accace11c2720ac62a63c1b7629cc202a7e108b8
+
+COCOAPODS: 1.8.4";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("Podfile.lock", podfileLockContent)
+                                                    .WithFile("Podfile.lock", podfileLockContent2)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(8, detectedComponents.Count());
+
+            AssertPodComponentNameAndVersion(detectedComponents, "AzureCore", "0.5.0");
+            AssertPodComponentNameAndVersion(detectedComponents, "AzureCore", "0.5.1");
+            AssertPodComponentNameAndVersion(detectedComponents, "AzureData", "0.5.0");
+            AssertPodComponentNameAndVersion(detectedComponents, "AzureMobile", "0.5.0");
+            AssertPodComponentNameAndVersion(detectedComponents, "CocoaLumberjack", "3.6.0");
+            AssertPodComponentNameAndVersion(detectedComponents, "KeychainAccess", "3.2.1");
+            AssertPodComponentNameAndVersion(detectedComponents, "SVGKit", "2.1.0");
+            AssertPodComponentNameAndVersion(detectedComponents, "Willow", "5.2.1");
+        }
+
+        [TestMethod]
+        public async Task TestPodDetector_DetectorSupportsDependencyRoots()
+        {
+            var podfileLockContent = @"PODS:
+  - AzureCore (0.5.0):
+    - KeychainAccess (~> 3.2)
+    - Willow (~> 5.2)
+  - AzureData (0.5.0):
+    - AzureCore (= 0.5.0)
+  - AzureMobile (0.5.0):
+    - AzureData (= 0.5.0)
+  - KeychainAccess (3.2.1)
+  - Willow (5.2.1)
+
+DEPENDENCIES:
+  - AzureData (= 0.5.0)
+  - AzureMobile (= 0.5.0)
+
+SPEC CHECKSUMS:
+  AzureCore: 9f6c42e03d59a13b508bff356a85cd9438b654fb
+  AzureData: f423992bd28e1006e3c358d3e3ce60d71f8ba090
+  AzureMobile: 4fd580aa2f73f4a8ac463971b4a5483afd586f2a
+  KeychainAccess: d5470352939ced6d6f7fb51cb2e67aae51fc294f
+  Willow: a6310f9aedcb6f4de8c35b94fd3416a660ae9280
+
+COCOAPODS: 0.39.0";
+
+            var podfileLockContent2 = @"PODS:
+  - AzureCore (0.5.1):
+    - KeychainAccess (~> 3.2)
+    - Willow (~> 5.2)
+  - CocoaLumberjack (3.6.0):
+    - CocoaLumberjack/Core (= 3.6.0)
+  - CocoaLumberjack/Core (3.6.0)
+  - KeychainAccess (3.2.1)
+  - SVGKit (2.1.0):
+    - CocoaLumberjack (~> 3.0)
+  - Willow (5.2.1)
+
+DEPENDENCIES:
+  - SVGKit (from `https://github.com/SVGKit/SVGKit.git`, branch `2.x`)
+  - AzureCore (= 0.5.1)
+
+EXTERNAL SOURCES:
+  SVGKit:
+    :branch: 2.x
+    :git: https://github.com/SVGKit/SVGKit.git
+
+CHECKOUT OPTIONS:
+  SVGKit:
+    :commit: 0d4db53890c664fb8605666e6fbccd14912ff821
+    :git: https://github.com/SVGKit/SVGKit.git
+
+SPEC CHECKSUMS:
+  AzureCore: 9f6c42e03d59a13b508bff356a85cd9438b654fc
+  CocoaLumberjack: 78b0c238666f4f58db069738ec176f4519557516
+  KeychainAccess: d5470352939ced6d6f7fb51cb2e67aae51fc294f
+  SVGKit: 8a2fc74258bdb2abb54d3b65f3dd68b0277a9c4d
+  Willow: a6310f9aedcb6f4de8c35b94fd3416a660ae9280
+
+PODFILE CHECKSUM: accace11c2720ac62a63c1b7629cc202a7e108b8
+
+COCOAPODS: 1.8.4";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("Podfile.lock", podfileLockContent)
+                                                    .WithFile("Podfile.lock", podfileLockContent2)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(8, detectedComponents.Count());
+
+            AssertPodComponentNameAndVersion(detectedComponents, "AzureCore", "0.5.0");
+            AssertPodComponentNameAndVersion(detectedComponents, "AzureCore", "0.5.1");
+            AssertPodComponentNameAndVersion(detectedComponents, "AzureData", "0.5.0");
+            AssertPodComponentNameAndVersion(detectedComponents, "AzureMobile", "0.5.0");
+            AssertPodComponentNameAndVersion(detectedComponents, "CocoaLumberjack", "3.6.0");
+            AssertPodComponentNameAndVersion(detectedComponents, "KeychainAccess", "3.2.1");
+            AssertGitComponentHashAndUrl(detectedComponents, "0d4db53890c664fb8605666e6fbccd14912ff821", "https://github.com/SVGKit/SVGKit.git");
+            AssertPodComponentNameAndVersion(detectedComponents, "Willow", "5.2.1");
+
+            AssertPodComponentHasPodComponentDependencyRoot(componentRecorder, component: (name: "AzureCore", version: "0.5.1"), root: (name: "AzureCore", version: "0.5.1"));
+            AssertPodComponentHasPodComponentDependencyRoot(componentRecorder, component: (name: "AzureData", version: "0.5.0"), root: (name: "AzureData", version: "0.5.0"));
+            AssertPodComponentHasPodComponentDependencyRoot(componentRecorder, component: (name: "AzureMobile", version: "0.5.0"), root: (name: "AzureMobile", version: "0.5.0"));
+            AssertGitComponentHasGitComponentDependencyRoot(componentRecorder, component: (commit: "0d4db53890c664fb8605666e6fbccd14912ff821", repo: "https://github.com/SVGKit/SVGKit.git"), root: (commit: "0d4db53890c664fb8605666e6fbccd14912ff821", repo: "https://github.com/SVGKit/SVGKit.git"));
+
+            AssertPodComponentHasPodComponentDependencyRoot(componentRecorder, component: (name: "AzureCore", version: "0.5.0"), root: (name: "AzureData", version: "0.5.0"));
+            AssertPodComponentHasPodComponentDependencyRoot(componentRecorder, component: (name: "AzureData", version: "0.5.0"), root: (name: "AzureMobile", version: "0.5.0"));
+            AssertPodComponentHasGitComponentDependencyRoot(componentRecorder, component: (name: "CocoaLumberjack", version: "3.6.0"), root: (commit: "0d4db53890c664fb8605666e6fbccd14912ff821", repo: "https://github.com/SVGKit/SVGKit.git"));
+            AssertPodComponentHasPodComponentDependencyRoot(componentRecorder, component: (name: "KeychainAccess", version: "3.2.1"), root: (name: "AzureCore", version: "0.5.1"));
+            AssertPodComponentHasPodComponentDependencyRoot(componentRecorder, component: (name: "KeychainAccess", version: "3.2.1"), root: (name: "AzureData", version: "0.5.0"));
+            AssertPodComponentHasPodComponentDependencyRoot(componentRecorder, component: (name: "Willow", version: "5.2.1"), root: (name: "AzureCore", version: "0.5.1"));
+            AssertPodComponentHasPodComponentDependencyRoot(componentRecorder, component: (name: "Willow", version: "5.2.1"), root: (name: "AzureData", version: "0.5.0"));
+        }
+
+        [TestMethod]
+        public async Task TestPodDetector_DetectorHandlesMainSpecRepoDifferences()
+        {
+            var podfileLockContent = @"PODS:
+  - AzureCore (0.5.0)
+
+SPEC REPOS:
+  https://github.com/cocoapods/specs.git:
+    - AzureCore
+
+DEPENDENCIES:
+  - AzureCore (= 0.5.0)
+
+SPEC CHECKSUMS:
+  AzureCore: 9f6c42e03d59a13b508bff356a85cd9438b654fb
+
+COCOAPODS: 1.7.3";
+
+            var podfileLockContent2 = @"PODS:
+  - AzureCore (0.5.0)
+
+SPEC REPOS:
+  https://github.com/CocoaPods/Specs.git:
+    - AzureCore
+
+DEPENDENCIES:
+  - AzureCore (= 0.5.0)
+
+SPEC CHECKSUMS:
+  AzureCore: 9f6c42e03d59a13b508bff356a85cd9438b654fb
+
+COCOAPODS: 1.8.4";
+
+            var podfileLockContent3 = @"PODS:
+  - AzureCore (0.5.0)
+
+SPEC REPOS:
+  trunk:
+    - AzureCore
+
+DEPENDENCIES:
+  - AzureCore (= 0.5.0)
+
+SPEC CHECKSUMS:
+  AzureCore: 9f6c42e03d59a13b508bff356a85cd9438b654fb
+
+COCOAPODS: 1.8.4";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("Podfile.lock", podfileLockContent)
+                                                    .WithFile("Podfile.lock", podfileLockContent2)
+                                                    .WithFile("Podfile.lock", podfileLockContent3)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(1, detectedComponents.Count());
+
+            AssertPodComponentNameAndVersion(detectedComponents, "AzureCore", "0.5.0");
+        }
+
+        [TestMethod]
+        public async Task TestPodDetector_DetectorRecognizeComponentsSpecRepo()
+        {
+            var podfileLockContent = @"PODS:
+  - AzureCore (0.5.0)
+
+SPEC REPOS:
+  trunk:
+    - AzureCore
+
+DEPENDENCIES:
+  - AzureCore (= 0.5.0)
+
+SPEC CHECKSUMS:
+  AzureCore: 9f6c42e03d59a13b508bff356a85cd9438b654fb
+
+COCOAPODS: 1.8.4";
+
+            var podfileLockContent2 = @"PODS:
+  - AzureCore (0.5.0)
+
+SPEC REPOS:
+  https://msblox.visualstudio.com/DefaultCollection/_git/CocoaPods:
+    - AzureCore
+
+DEPENDENCIES:
+  - AzureCore (= 0.5.0)
+
+SPEC CHECKSUMS:
+  AzureCore: 9f6c42e03d59a13b508bff356a85cd9438b654fb
+
+COCOAPODS: 1.8.4";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("Podfile.lock", podfileLockContent)
+                                                    .WithFile("Podfile.lock", podfileLockContent2, fileLocation: Path.Join(Path.GetTempPath(), "sub-folder", "Podfile.lock"))
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(1, detectedComponents.Count());
+
+            var firstComponent = detectedComponents.First();
+            componentRecorder.ForOneComponent(firstComponent.Component.Id, grouping => Assert.AreEqual(2, Enumerable.Count(grouping.AllFileLocations)));
+        }
+
+        private void AssertPodComponentNameAndVersion(IEnumerable detectedComponents, string name, string version)
+        {
+            Assert.IsNotNull(
+                detectedComponents.SingleOrDefault(component =>
+                component.Component is PodComponent &&
+                (component.Component as PodComponent).Name.Equals(name) &&
+                (component.Component as PodComponent).Version.Equals(version)), $"Component with name {name} and version {version} was not found");
+        }
+
+        private void AssertGitComponentHashAndUrl(IEnumerable detectedComponents, string commitHash, string repositoryUrl)
+        {
+            Assert.IsNotNull(
+                detectedComponents.SingleOrDefault(component =>
+                component.Component is GitComponent &&
+                (component.Component as GitComponent).CommitHash.Equals(commitHash) &&
+                (component.Component as GitComponent).RepositoryUrl.Equals(repositoryUrl)), $"Component with commit hash {commitHash} and repository url {repositoryUrl} was not found");
+        }
+
+        private void AssertPodComponentHasPodComponentDependencyRoot(IComponentRecorder recorder, (string name, string version) component, (string name, string version) root)
+        {
+            Assert.IsTrue(recorder.IsDependencyOfExplicitlyReferencedComponents(
+                new PodComponent(component.name, component.version).Id,
+                x => x.Id == new PodComponent(root.name, root.version).Id));
+        }
+
+        private void AssertPodComponentHasGitComponentDependencyRoot(IComponentRecorder recorder, (string name, string version) component, (string commit, string repo) root)
+        {
+            Assert.IsTrue(recorder.IsDependencyOfExplicitlyReferencedComponents(
+                new PodComponent(component.name, component.version).Id,
+                x => x.Id == new GitComponent(new Uri(root.repo), root.commit).Id));
+        }
+
+        private void AssertGitComponentHasGitComponentDependencyRoot(IComponentRecorder recorder, (string commit, string repo) component, (string commit, string repo) root)
+        {
+            Assert.IsTrue(recorder.IsDependencyOfExplicitlyReferencedComponents(
+                new GitComponent(new Uri(component.repo), component.commit).Id,
+                x => x.Id == new GitComponent(new Uri(root.repo), root.commit).Id));
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/PyPiClientTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/PyPiClientTests.cs
new file mode 100644
index 000000000..ae18825c2
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/PyPiClientTests.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Detectors.Pip;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Moq.Protected;
+using Newtonsoft.Json;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    public class PyPiClientTests
+    {
+        private PyPiClient pypiClient;
+
+        [TestInitialize]
+        public void Initialize()
+        {
+            pypiClient = new PyPiClient()
+            {
+                Logger = new Mock().Object,
+            };
+        }
+
+        [TestMethod]
+        public async Task GetReleases_InvalidSpecVersion_NotThrow()
+        {
+            var pythonSpecs = new PipDependencySpecification { DependencySpecifiers = new List { "==1.0.0", "==1.0.0notvalid" } };
+
+            var pythonProject = new PythonProject
+            {
+                Releases = new Dictionary>
+                {
+                    { "1.0.0", new List { new PythonProjectRelease() } },
+                },
+            };
+
+            PyPiClient.HttpClient = new HttpClient(MockHttpMessageHandler(JsonConvert.SerializeObject(pythonProject)));
+
+            Func action = async () => await pypiClient.GetReleases(pythonSpecs);
+
+            await action.Should().NotThrowAsync();
+        }
+
+        private HttpMessageHandler MockHttpMessageHandler(string content)
+        {
+            var handlerMock = new Mock();
+            handlerMock.Protected()
+                .Setup>(
+                    "SendAsync",
+                    ItExpr.IsAny(),
+                    ItExpr.IsAny())
+               .ReturnsAsync(new HttpResponseMessage()
+               {
+                   StatusCode = HttpStatusCode.OK,
+                   Content = new StringContent(content),
+               });
+
+            return handlerMock.Object;
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/PythonCommandServiceTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/PythonCommandServiceTests.cs
new file mode 100644
index 000000000..ec41cc726
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/PythonCommandServiceTests.cs
@@ -0,0 +1,381 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Detectors.Pip;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class PythonCommandServiceTests
+    {
+        private Mock commandLineInvokationService;
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            commandLineInvokationService = new Mock();
+        }
+
+        [TestMethod]
+        public async Task PythonCommandService_ReturnsTrueWhenPythonExists()
+        {
+            commandLineInvokationService.Setup(x => x.CanCommandBeLocated("python", It.IsAny>(), "--version")).ReturnsAsync(true);
+
+            PythonCommandService service = new PythonCommandService { CommandLineInvocationService = commandLineInvokationService.Object };
+
+            Assert.IsTrue(await service.PythonExists());
+        }
+
+        [TestMethod]
+        public async Task PythonCommandService_ReturnsFalseWhenPythonExists()
+        {
+            commandLineInvokationService.Setup(x => x.CanCommandBeLocated("python", It.IsAny>(), "--version")).ReturnsAsync(false);
+
+            PythonCommandService service = new PythonCommandService { CommandLineInvocationService = commandLineInvokationService.Object };
+
+            Assert.IsFalse(await service.PythonExists());
+        }
+
+        [TestMethod]
+        public async Task PythonCommandService_ReturnsTrueWhenPythonExistsForAPath()
+        {
+            commandLineInvokationService.Setup(x => x.CanCommandBeLocated("test", It.IsAny>(), "--version")).ReturnsAsync(true);
+
+            PythonCommandService service = new PythonCommandService { CommandLineInvocationService = commandLineInvokationService.Object };
+
+            Assert.IsTrue(await service.PythonExists("test"));
+        }
+
+        [TestMethod]
+        public async Task PythonCommandService_ReturnsFalseWhenPythonExistsForAPath()
+        {
+            commandLineInvokationService.Setup(x => x.CanCommandBeLocated("test", It.IsAny>(), "--version")).ReturnsAsync(false);
+
+            PythonCommandService service = new PythonCommandService { CommandLineInvocationService = commandLineInvokationService.Object };
+
+            Assert.IsFalse(await service.PythonExists("test"));
+        }
+
+        [TestMethod]
+        public async Task PythonCommandService_ParsesEmptySetupPyOutputCorrectly()
+        {
+            var fakePath = @"c:\the\fake\path.py";
+            var fakePathAsPassedToPython = fakePath.Replace("\\", "/");
+
+            commandLineInvokationService.Setup(x => x.CanCommandBeLocated("python", It.IsAny>(), "--version")).ReturnsAsync(true);
+            commandLineInvokationService.Setup(x => x.ExecuteCommand("python", It.IsAny>(), It.Is(c => c.Contains(fakePathAsPassedToPython))))
+                                        .ReturnsAsync(new CommandLineExecutionResult { ExitCode = 0, StdOut = "[]", StdErr = string.Empty });
+
+            PythonCommandService service = new PythonCommandService { CommandLineInvocationService = commandLineInvokationService.Object };
+
+            var result = await service.ParseFile(fakePath);
+
+            Assert.AreEqual(0, result.Count);
+        }
+
+        [TestMethod]
+        public async Task PythonCommandService_ParsesRegularSetupPyOutputCorrectly()
+        {
+            var fakePath = @"c:\the\fake\path.py";
+            var fakePathAsPassedToPython = fakePath.Replace("\\", "/");
+
+            commandLineInvokationService.Setup(x => x.CanCommandBeLocated("python", It.IsAny>(), "--version")).ReturnsAsync(true);
+            commandLineInvokationService.Setup(x => x.ExecuteCommand("python", It.IsAny>(), It.Is(c => c.Contains(fakePathAsPassedToPython))))
+                                        .ReturnsAsync(new CommandLineExecutionResult { ExitCode = 0, StdOut = "['knack==0.4.1', 'setuptools>=1.0,!=1.1', 'vsts-cli-common==0.1.3', 'vsts-cli-admin==0.1.3', 'vsts-cli-build==0.1.3', 'vsts-cli-code==0.1.3', 'vsts-cli-team==0.1.3', 'vsts-cli-package==0.1.3', 'vsts-cli-work==0.1.3']", StdErr = string.Empty });
+
+            PythonCommandService service = new PythonCommandService { CommandLineInvocationService = commandLineInvokationService.Object };
+
+            var result = await service.ParseFile(fakePath);
+            var expected = new string[] { "knack==0.4.1", "setuptools>=1.0,!=1.1", "vsts-cli-common==0.1.3", "vsts-cli-admin==0.1.3", "vsts-cli-build==0.1.3", "vsts-cli-code==0.1.3", "vsts-cli-team==0.1.3", "vsts-cli-package==0.1.3", "vsts-cli-work==0.1.3" }.Select(dep => (dep, null)).ToArray();
+
+            Assert.AreEqual(9, result.Count);
+
+            for (int i = 0; i < 9; i++)
+            {
+                Assert.AreEqual(expected[i], result[i]);
+            }
+        }
+
+        [TestMethod]
+        public async Task PythonCommandService_ParsesRequirementsTxtCorrectly()
+        {
+            var testPath = Path.Join(Directory.GetCurrentDirectory(), string.Join(Guid.NewGuid().ToString(), ".txt"));
+
+            commandLineInvokationService.Setup(x => x.CanCommandBeLocated("python", It.IsAny>(), "--version")).ReturnsAsync(true);
+            PythonCommandService service = new PythonCommandService { CommandLineInvocationService = commandLineInvokationService.Object };
+
+            try
+            {
+                using (StreamWriter writer = File.CreateText(testPath))
+                {
+                    writer.WriteLine("knack==0.4.1");
+                    writer.WriteLine("vsts-cli-common==0.1.3    \\      ");
+                    writer.WriteLine("    --hash=sha256:856476331f3e26598017290fd65bebe81c960e806776f324093a46b76fb2d1c0");
+                    writer.Flush();
+                }
+
+                var result = await service.ParseFile(testPath);
+                var expected = new string[] { "knack==0.4.1", "vsts-cli-common==0.1.3" }.Select(dep => (dep, null)).ToArray();
+
+                Assert.AreEqual(expected.Length, result.Count);
+
+                for (int i = 0; i < expected.Length; i++)
+                {
+                    Assert.AreEqual(expected[i], result[i]);
+                }
+            }
+            finally
+            {
+                if (File.Exists(testPath))
+                {
+                    File.Delete(testPath);
+                }
+            }
+        }
+
+        [TestMethod]
+        public async Task ParseFile_RequirementTxtHasComment_CommentAreIgnored()
+        {
+            var testPath = Path.Join(Directory.GetCurrentDirectory(), string.Join(Guid.NewGuid().ToString(), ".txt"));
+
+            commandLineInvokationService.Setup(x => x.CanCommandBeLocated("python", It.IsAny>(), "--version")).ReturnsAsync(true);
+            PythonCommandService service = new PythonCommandService { CommandLineInvocationService = commandLineInvokationService.Object };
+
+            try
+            {
+                using (StreamWriter writer = File.CreateText(testPath))
+                {
+                    writer.WriteLine("#this is a comment");
+                    writer.WriteLine("knack==0.4.1 #this is another comment");
+                    writer.Flush();
+                }
+
+                var result = await service.ParseFile(testPath);
+                (string, GitComponent) expected = ("knack==0.4.1", null);
+
+                Assert.AreEqual(1, result.Count());
+                Assert.AreEqual(expected, result.First());
+            }
+            finally
+            {
+                if (File.Exists(testPath))
+                {
+                    File.Delete(testPath);
+                }
+            }
+        }
+
+        [TestMethod]
+        public async Task ParseFile_RequirementTxtHasComment_GitComponentsSupported()
+        {
+            await SetupAndParseReqsTxt(requirementstxtBasicGitComponent, parseResult =>
+            {
+                parseResult.Count.Should().Be(1);
+                
+                var tuple = parseResult.Single();
+                tuple.Item1.Should().BeNull();
+                tuple.Item2.Should().NotBeNull();
+
+                var gitComponent = tuple.Item2;
+                gitComponent.RepositoryUrl.Should().Be("https://github.com/vscode-python/jedi-language-server");
+                gitComponent.CommitHash.Should().Be("42823a2598d4b6369e9273c5ad237a48c5d67553");
+            });
+        }
+
+        [TestMethod]
+        public async Task ParseFile_RequirementTxtHasComment_GitComponentAndEnvironmentMarker()
+        {
+            await SetupAndParseReqsTxt(requirementstxtGitComponentAndEnvironmentMarker, parseResult =>
+            {
+                parseResult.Count.Should().Be(1);
+
+                var tuple = parseResult.Single();
+                tuple.Item1.Should().BeNull();
+                tuple.Item2.Should().NotBeNull();
+
+                var gitComponent = tuple.Item2;
+                gitComponent.RepositoryUrl.Should().Be("https://github.com/vscode-python/jedi-language-server");
+                gitComponent.CommitHash.Should().Be("42823a2598d4b6369e9273c5ad237a48c5d67553");
+            });
+        }
+
+        [TestMethod]
+        public async Task ParseFile_RequirementTxtHasComment_GitComponentAndComment()
+        {
+            await SetupAndParseReqsTxt(requirementstxtGitComponentAndComment, parseResult =>
+            {
+                parseResult.Count.Should().Be(1);
+
+                var tuple = parseResult.Single();
+                tuple.Item1.Should().BeNull();
+                tuple.Item2.Should().NotBeNull();
+
+                var gitComponent = tuple.Item2;
+                gitComponent.RepositoryUrl.Should().Be("https://github.com/vscode-python/jedi-language-server");
+                gitComponent.CommitHash.Should().Be("42823a2598d4b6369e9273c5ad237a48c5d67553");
+            });
+        }
+
+        [TestMethod]
+        public async Task ParseFile_RequirementTxtHasComment_GitComponentAndCommentAndEnvironmentMarker()
+        {
+            await SetupAndParseReqsTxt(requirementstxtGitComponentAndCommentAndEnvironmentMarker, parseResult =>
+            {
+                parseResult.Count.Should().Be(1);
+
+                var tuple = parseResult.Single();
+                tuple.Item1.Should().BeNull();
+                tuple.Item2.Should().NotBeNull();
+
+                var gitComponent = tuple.Item2;
+                gitComponent.RepositoryUrl.Should().Be("https://github.com/vscode-python/jedi-language-server");
+                gitComponent.CommitHash.Should().Be("42823a2598d4b6369e9273c5ad237a48c5d67553");
+            });
+        }
+
+        [TestMethod]
+        public async Task ParseFile_RequirementTxtHasComment_GitComponentNotCreatedWhenGivenBranch()
+        {
+            await SetupAndParseReqsTxt(requirementstxtGitComponentBranchInsteadOfCommitId, parseResult =>
+            {
+                parseResult.Count.Should().Be(0);
+            });
+        }
+
+        [TestMethod]
+        public async Task ParseFile_RequirementTxtHasComment_GitComponentNotCreatedWhenGivenRelease()
+        {
+            await SetupAndParseReqsTxt(requirementstxtGitComponentReleaseInsteadOfCommitId, parseResult =>
+            {
+                parseResult.Count.Should().Be(0);
+            });
+        }
+
+        [TestMethod]
+        public async Task ParseFile_RequirementTxtHasComment_GitComponentNotCreatedWhenGivenMalformedCommitHash()
+        {
+            await SetupAndParseReqsTxt(requirementstxtGitComponentCommitIdWrongLength, parseResult =>
+            {
+                parseResult.Count.Should().Be(0);
+            });
+        }
+
+        [TestMethod]
+        public async Task ParseFile_RequirementTxtHasComment_GitComponentsMultiple()
+        {
+            await SetupAndParseReqsTxt(requirementstxtDoubleGitComponents, parseResult =>
+            {
+                parseResult.Count.Should().Be(2);
+
+                var tuple1 = parseResult.First();
+                tuple1.Item1.Should().BeNull();
+                tuple1.Item2.Should().NotBeNull();
+
+                var gitComponent1 = tuple1.Item2;
+                gitComponent1.RepositoryUrl.Should().Be("https://github.com/vscode-python/jedi-language-server");
+                gitComponent1.CommitHash.Should().Be("42823a2598d4b6369e9273c5ad237a48c5d67553");
+
+                var tuple2 = parseResult.Skip(1).First();
+                tuple2.Item1.Should().BeNull();
+                tuple2.Item2.Should().NotBeNull();
+
+                var gitComponent2 = tuple2.Item2;
+                gitComponent2.RepositoryUrl.Should().Be("https://github.com/path/to/package-two");
+                gitComponent2.CommitHash.Should().Be("41b95ec");
+            });
+        }
+
+        [TestMethod]
+        public async Task ParseFile_RequirementTxtHasComment_GitComponentWrappedInRegularComponent()
+        {
+            await SetupAndParseReqsTxt(requirementstxtGitComponentWrappedinRegularComponents, parseResult =>
+            {
+                parseResult.Count.Should().Be(3);
+
+                var tuple1 = parseResult.First();
+                tuple1.Item1.Should().NotBeNull();
+                tuple1.Item2.Should().BeNull();
+
+                var regularComponent1 = tuple1.Item1;
+                regularComponent1.Should().Be("something=1.3");
+
+                var tuple2 = parseResult.Skip(1).First();
+                tuple2.Item1.Should().BeNull();
+                tuple2.Item2.Should().NotBeNull();
+
+                var gitComponent = tuple2.Item2;
+                gitComponent.RepositoryUrl.Should().Be("https://github.com/path/to/package-two");
+                gitComponent.CommitHash.Should().Be("41b95ec");
+
+                var tuple3 = parseResult.ToArray()[2];
+                tuple3.Item1.Should().NotBeNull();
+                tuple3.Item2.Should().BeNull();
+
+                var regularComponent2 = tuple3.Item1;
+                regularComponent2.Should().Be("other=2.1");
+            });
+        }
+
+        private async Task SetupAndParseReqsTxt(string fileToParse, Action> verificationFunction)
+        {
+            var testPath = Path.Join(Directory.GetCurrentDirectory(), string.Join(Guid.NewGuid().ToString(), ".txt"));
+
+            commandLineInvokationService.Setup(x => x.CanCommandBeLocated("python", It.IsAny>(), "--version")).ReturnsAsync(true);
+            PythonCommandService service = new PythonCommandService { CommandLineInvocationService = commandLineInvokationService.Object };
+
+            using (StreamWriter writer = File.CreateText(testPath))
+            {
+                writer.WriteLine(fileToParse);
+                writer.Flush();
+            }
+
+            var result = await service.ParseFile(testPath);
+            verificationFunction(result);
+            if (File.Exists(testPath))
+            {
+                File.Delete(testPath);
+            }
+
+            return 0;
+        }
+
+        private readonly string requirementstxtBasicGitComponent = @"
+git+git://github.com/vscode-python/jedi-language-server@42823a2598d4b6369e9273c5ad237a48c5d67553";
+
+        private readonly string requirementstxtGitComponentAndEnvironmentMarker = @"
+git+git://github.com/vscode-python/jedi-language-server@42823a2598d4b6369e9273c5ad237a48c5d67553 ; python_version >= ""3.6""";
+
+        private readonly string requirementstxtGitComponentAndComment = @"
+git+git://github.com/vscode-python/jedi-language-server@42823a2598d4b6369e9273c5ad237a48c5d67553 # this is a comment";
+
+        private readonly string requirementstxtGitComponentAndCommentAndEnvironmentMarker = @"
+git+git://github.com/vscode-python/jedi-language-server@42823a2598d4b6369e9273c5ad237a48c5d67553 ; python_version >= {""3.6""  # via -r requirements.in";
+
+        private readonly string requirementstxtGitComponentBranchInsteadOfCommitId = @"
+git+git://github.com/path/to/package-two@master#egg=package-two";
+
+        private readonly string requirementstxtGitComponentReleaseInsteadOfCommitId = @"
+git+git://github.com/path/to/package-two@0.1#egg=package-two";
+
+        private readonly string requirementstxtGitComponentCommitIdWrongLength = @"
+git+git://github.com/vscode-python/jedi-language-server@42823a2598d4b6369e9273c5ad237a48c5d6755300000000000";
+
+        private readonly string requirementstxtDoubleGitComponents = @"
+git+git://github.com/vscode-python/jedi-language-server@42823a2598d4b6369e9273c5ad237a48c5d67553 ; python_version >= {""3.6""  # via -r requirements.in
+git+git://github.com/path/to/package-two@41b95ec#egg=package-two";
+
+        private readonly string requirementstxtGitComponentWrappedinRegularComponents = @"
+something=1.3
+git+git://github.com/path/to/package-two@41b95ec#egg=package-two
+other=2.1";
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/PythonVersionTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/PythonVersionTests.cs
new file mode 100644
index 000000000..718c9b9a0
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/PythonVersionTests.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Detectors.Pip;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class PythonVersionTests
+    {
+        [TestMethod]
+        public void TestBasicVersionConstruction()
+        {
+            PythonVersion pythonVersion = new PythonVersion("4!3.2.1.1rc2.post99.dev2");
+
+            Assert.AreEqual(pythonVersion.Epoch, 4);
+            Assert.AreEqual(pythonVersion.Release, "3.2.1.1");
+            Assert.AreEqual(pythonVersion.PreReleaseLabel, "rc");
+            Assert.AreEqual(pythonVersion.PostNumber, 99);
+            Assert.AreEqual(pythonVersion.DevNumber, 2);
+
+            var newPythonVersion = new PythonVersion("0.3m1");
+        }
+
+        [TestMethod]
+        public void TestPythonVersionComplexComparisons()
+        {
+            // This is a list of versions supplied by PEP440 for testing (minus local versions)
+            var versions = new List
+            {
+                "1.0.dev456",
+                "1.0a1",
+                "1.0a2.dev456",
+                "1.0a12.dev456",
+                "1.0a12",
+                "1.0b1.dev456",
+                "1.0b2",
+                "1.0b2.post345.dev456",
+                "1.0b2.post345",
+                "1.0rc1.dev456",
+                "1.0rc1",
+                "1.0",
+                "1.0.post456.dev34",
+                "1.0.post456",
+                "1.1.dev1",
+            }.Select(x => new PythonVersion(x)).ToList();
+
+            for (int i = 1; i < versions.Count; i++)
+            {
+                Assert.IsTrue(versions[i - 1] < versions[i]);
+            }
+        }
+
+        [TestMethod]
+        public void TestVersionValidForSpec()
+        {
+            IList<(IList, IList, IList)> testCases = new List<(IList, IList, IList)>
+            {
+                (new List { "==1.0" }, new List { "1.0" }, new List { "1.0.1", "2.0", "0.1" }),
+                (new List { "==1.4.*" }, new List { "1.4", "1.4.1", "1.4.2", "1.4.3" }, new List { "1.0.1", "2.0", "0.1", "1.5", "1.5.0" }),
+                (new List { ">=1.0" }, new List { "1.0", "1.1", "1.5" }, new List { "0.9" }),
+                (new List { ">=1.0", "<=1.4" }, new List { "1.0", "1.1", "1.4" }, new List { "0.9", "1.5" }),
+                (new List { ">1.0", "<1.4" }, new List { "1.1", "1.3" }, new List { "0.9", "1.5", "1.0", "1.4" }),
+                (new List { ">1.0", "<1.4", "!=1.2" }, new List { "1.1", "1.3" }, new List { "0.9", "1.5", "1.0", "1.4", "1.2" }),
+            };
+
+            foreach (var (specs, validVersions, invalidVersions) in testCases)
+            {
+                Assert.IsTrue(validVersions.All(x => PythonVersionUtilities.VersionValidForSpec(x, specs)));
+                Assert.IsTrue(invalidVersions.All(x => !PythonVersionUtilities.VersionValidForSpec(x, specs)));
+            }
+        }
+
+        [TestMethod]
+        public void TestVersionValidForSpec_VersionIsNotValid_ArgumentExpcetionIsThrown()
+        {
+            Action action = () => PythonVersionUtilities.VersionValidForSpec("notvalid", new List { "==1.0" });
+            action.Should().Throw();
+        }
+
+        [TestMethod]
+        public void TestVersionValidForSpec_SomeSpecIsNotValid_ArgumentExpcetionIsThrown()
+        {
+            Action action = () => PythonVersionUtilities.VersionValidForSpec("1.0.0", new List { "==notvalid" });
+            action.Should().Throw();
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/Resources/project_assets_2_2.json b/test/Microsoft.ComponentDetection.Detectors.Tests/Resources/project_assets_2_2.json
new file mode 100644
index 000000000..52126602d
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/Resources/project_assets_2_2.json
@@ -0,0 +1,9344 @@
+{
+	"version": 3,
+	"targets": {
+		".NETCoreApp,Version=v2.2": {
+			"CommandLineParser/2.8.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/CommandLine.dll": {}
+				}
+			},
+			"coverlet.msbuild/2.5.1": {
+				"type": "package",
+				"build": {
+					"build/netstandard2.0/coverlet.msbuild.props": {},
+					"build/netstandard2.0/coverlet.msbuild.targets": {}
+				}
+			},
+			"DotNet.Glob/2.1.1": {
+				"type": "package",
+				"dependencies": {
+					"NETStandard.Library": "1.6.1"
+				},
+				"compile": {
+					"lib/netstandard1.1/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.1/DotNet.Glob.dll": {}
+				}
+			},
+			"Microsoft.AspNet.WebApi.Client/5.2.7": {
+				"type": "package",
+				"dependencies": {
+					"Newtonsoft.Json": "10.0.1",
+					"Newtonsoft.Json.Bson": "1.0.1"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Net.Http.Formatting.dll": {}
+				}
+			},
+			"Microsoft.NETCore.App/2.2.8": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.DotNetHostPolicy": "2.2.8",
+					"Microsoft.NETCore.Platforms": "2.2.4",
+					"Microsoft.NETCore.Targets": "2.0.0",
+					"NETStandard.Library": "2.0.3"
+				},
+				"compile": {
+					"ref/netcoreapp2.2/Microsoft.CSharp.dll": {},
+					"ref/netcoreapp2.2/Microsoft.VisualBasic.dll": {},
+					"ref/netcoreapp2.2/Microsoft.Win32.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.AppContext.dll": {},
+					"ref/netcoreapp2.2/System.Buffers.dll": {},
+					"ref/netcoreapp2.2/System.Collections.Concurrent.dll": {},
+					"ref/netcoreapp2.2/System.Collections.Immutable.dll": {},
+					"ref/netcoreapp2.2/System.Collections.NonGeneric.dll": {},
+					"ref/netcoreapp2.2/System.Collections.Specialized.dll": {},
+					"ref/netcoreapp2.2/System.Collections.dll": {},
+					"ref/netcoreapp2.2/System.ComponentModel.Annotations.dll": {},
+					"ref/netcoreapp2.2/System.ComponentModel.DataAnnotations.dll": {},
+					"ref/netcoreapp2.2/System.ComponentModel.EventBasedAsync.dll": {},
+					"ref/netcoreapp2.2/System.ComponentModel.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.ComponentModel.TypeConverter.dll": {},
+					"ref/netcoreapp2.2/System.ComponentModel.dll": {},
+					"ref/netcoreapp2.2/System.Configuration.dll": {},
+					"ref/netcoreapp2.2/System.Console.dll": {},
+					"ref/netcoreapp2.2/System.Core.dll": {},
+					"ref/netcoreapp2.2/System.Data.Common.dll": {},
+					"ref/netcoreapp2.2/System.Data.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.Contracts.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.Debug.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.DiagnosticSource.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.FileVersionInfo.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.Process.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.StackTrace.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.TextWriterTraceListener.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.Tools.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.TraceSource.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.Tracing.dll": {},
+					"ref/netcoreapp2.2/System.Drawing.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.Drawing.dll": {},
+					"ref/netcoreapp2.2/System.Dynamic.Runtime.dll": {},
+					"ref/netcoreapp2.2/System.Globalization.Calendars.dll": {},
+					"ref/netcoreapp2.2/System.Globalization.Extensions.dll": {},
+					"ref/netcoreapp2.2/System.Globalization.dll": {},
+					"ref/netcoreapp2.2/System.IO.Compression.Brotli.dll": {},
+					"ref/netcoreapp2.2/System.IO.Compression.FileSystem.dll": {},
+					"ref/netcoreapp2.2/System.IO.Compression.ZipFile.dll": {},
+					"ref/netcoreapp2.2/System.IO.Compression.dll": {},
+					"ref/netcoreapp2.2/System.IO.FileSystem.DriveInfo.dll": {},
+					"ref/netcoreapp2.2/System.IO.FileSystem.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.IO.FileSystem.Watcher.dll": {},
+					"ref/netcoreapp2.2/System.IO.FileSystem.dll": {},
+					"ref/netcoreapp2.2/System.IO.IsolatedStorage.dll": {},
+					"ref/netcoreapp2.2/System.IO.MemoryMappedFiles.dll": {},
+					"ref/netcoreapp2.2/System.IO.Pipes.dll": {},
+					"ref/netcoreapp2.2/System.IO.UnmanagedMemoryStream.dll": {},
+					"ref/netcoreapp2.2/System.IO.dll": {},
+					"ref/netcoreapp2.2/System.Linq.Expressions.dll": {},
+					"ref/netcoreapp2.2/System.Linq.Parallel.dll": {},
+					"ref/netcoreapp2.2/System.Linq.Queryable.dll": {},
+					"ref/netcoreapp2.2/System.Linq.dll": {},
+					"ref/netcoreapp2.2/System.Memory.dll": {},
+					"ref/netcoreapp2.2/System.Net.Http.dll": {},
+					"ref/netcoreapp2.2/System.Net.HttpListener.dll": {},
+					"ref/netcoreapp2.2/System.Net.Mail.dll": {},
+					"ref/netcoreapp2.2/System.Net.NameResolution.dll": {},
+					"ref/netcoreapp2.2/System.Net.NetworkInformation.dll": {},
+					"ref/netcoreapp2.2/System.Net.Ping.dll": {},
+					"ref/netcoreapp2.2/System.Net.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.Net.Requests.dll": {},
+					"ref/netcoreapp2.2/System.Net.Security.dll": {},
+					"ref/netcoreapp2.2/System.Net.ServicePoint.dll": {},
+					"ref/netcoreapp2.2/System.Net.Sockets.dll": {},
+					"ref/netcoreapp2.2/System.Net.WebClient.dll": {},
+					"ref/netcoreapp2.2/System.Net.WebHeaderCollection.dll": {},
+					"ref/netcoreapp2.2/System.Net.WebProxy.dll": {},
+					"ref/netcoreapp2.2/System.Net.WebSockets.Client.dll": {},
+					"ref/netcoreapp2.2/System.Net.WebSockets.dll": {},
+					"ref/netcoreapp2.2/System.Net.dll": {},
+					"ref/netcoreapp2.2/System.Numerics.Vectors.dll": {},
+					"ref/netcoreapp2.2/System.Numerics.dll": {},
+					"ref/netcoreapp2.2/System.ObjectModel.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.DispatchProxy.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.Emit.ILGeneration.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.Emit.Lightweight.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.Emit.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.Extensions.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.Metadata.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.TypeExtensions.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.dll": {},
+					"ref/netcoreapp2.2/System.Resources.Reader.dll": {},
+					"ref/netcoreapp2.2/System.Resources.ResourceManager.dll": {},
+					"ref/netcoreapp2.2/System.Resources.Writer.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.CompilerServices.VisualC.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Extensions.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Handles.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.InteropServices.RuntimeInformation.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.InteropServices.WindowsRuntime.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.InteropServices.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Loader.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Numerics.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Serialization.Formatters.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Serialization.Json.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Serialization.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Serialization.Xml.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Serialization.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.dll": {},
+					"ref/netcoreapp2.2/System.Security.Claims.dll": {},
+					"ref/netcoreapp2.2/System.Security.Cryptography.Algorithms.dll": {},
+					"ref/netcoreapp2.2/System.Security.Cryptography.Csp.dll": {},
+					"ref/netcoreapp2.2/System.Security.Cryptography.Encoding.dll": {},
+					"ref/netcoreapp2.2/System.Security.Cryptography.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.Security.Cryptography.X509Certificates.dll": {},
+					"ref/netcoreapp2.2/System.Security.Principal.dll": {},
+					"ref/netcoreapp2.2/System.Security.SecureString.dll": {},
+					"ref/netcoreapp2.2/System.Security.dll": {},
+					"ref/netcoreapp2.2/System.ServiceModel.Web.dll": {},
+					"ref/netcoreapp2.2/System.ServiceProcess.dll": {},
+					"ref/netcoreapp2.2/System.Text.Encoding.Extensions.dll": {},
+					"ref/netcoreapp2.2/System.Text.Encoding.dll": {},
+					"ref/netcoreapp2.2/System.Text.RegularExpressions.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Overlapped.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Tasks.Dataflow.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Tasks.Extensions.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Tasks.Parallel.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Tasks.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Thread.dll": {},
+					"ref/netcoreapp2.2/System.Threading.ThreadPool.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Timer.dll": {},
+					"ref/netcoreapp2.2/System.Threading.dll": {},
+					"ref/netcoreapp2.2/System.Transactions.Local.dll": {},
+					"ref/netcoreapp2.2/System.Transactions.dll": {},
+					"ref/netcoreapp2.2/System.ValueTuple.dll": {},
+					"ref/netcoreapp2.2/System.Web.HttpUtility.dll": {},
+					"ref/netcoreapp2.2/System.Web.dll": {},
+					"ref/netcoreapp2.2/System.Windows.dll": {},
+					"ref/netcoreapp2.2/System.Xml.Linq.dll": {},
+					"ref/netcoreapp2.2/System.Xml.ReaderWriter.dll": {},
+					"ref/netcoreapp2.2/System.Xml.Serialization.dll": {},
+					"ref/netcoreapp2.2/System.Xml.XDocument.dll": {},
+					"ref/netcoreapp2.2/System.Xml.XPath.XDocument.dll": {},
+					"ref/netcoreapp2.2/System.Xml.XPath.dll": {},
+					"ref/netcoreapp2.2/System.Xml.XmlDocument.dll": {},
+					"ref/netcoreapp2.2/System.Xml.XmlSerializer.dll": {},
+					"ref/netcoreapp2.2/System.Xml.dll": {},
+					"ref/netcoreapp2.2/System.dll": {},
+					"ref/netcoreapp2.2/WindowsBase.dll": {},
+					"ref/netcoreapp2.2/mscorlib.dll": {},
+					"ref/netcoreapp2.2/netstandard.dll": {}
+				},
+				"build": {
+					"build/netcoreapp2.2/Microsoft.NETCore.App.props": {},
+					"build/netcoreapp2.2/Microsoft.NETCore.App.targets": {}
+				}
+			},
+			"Microsoft.NETCore.DotNetAppHost/2.2.8": {
+				"type": "package"
+			},
+			"Microsoft.NETCore.DotNetHostPolicy/2.2.8": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.DotNetHostResolver": "2.2.8"
+				}
+			},
+			"Microsoft.NETCore.DotNetHostResolver/2.2.8": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.DotNetAppHost": "2.2.8"
+				}
+			},
+			"Microsoft.NETCore.Platforms/2.2.4": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.0/_._": {}
+				}
+			},
+			"Microsoft.NETCore.Targets/2.0.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.0/_._": {}
+				}
+			},
+			"Microsoft.Win32.Primitives/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"Microsoft.Win32.Registry/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"System.Collections": "4.3.0",
+					"System.Globalization": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Extensions": "4.3.0",
+					"System.Runtime.Handles": "4.3.0",
+					"System.Runtime.InteropServices": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtimeTargets": {
+					"runtimes/unix/lib/netstandard1.3/Microsoft.Win32.Registry.dll": {
+						"assetType": "runtime",
+						"rid": "unix"
+					},
+					"runtimes/win/lib/netstandard1.3/Microsoft.Win32.Registry.dll": {
+						"assetType": "runtime",
+						"rid": "win"
+					}
+				}
+			},
+			"MinVer/2.5.0": {
+				"type": "package",
+				"build": {
+					"build/MinVer.targets": {}
+				},
+				"buildMultiTargeting": {
+					"buildMultiTargeting/MinVer.targets": {}
+				}
+			},
+			"NETStandard.Library/2.0.3": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0"
+				},
+				"compile": {
+					"lib/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.0/_._": {}
+				},
+				"build": {
+					"build/netstandard2.0/NETStandard.Library.targets": {}
+				}
+			},
+			"Nett/0.10.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/Nett.dll": {}
+				}
+			},
+			"Newtonsoft.Json/12.0.3": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/Newtonsoft.Json.dll": {}
+				}
+			},
+			"Newtonsoft.Json.Bson/1.0.1": {
+				"type": "package",
+				"dependencies": {
+					"NETStandard.Library": "1.6.1",
+					"Newtonsoft.Json": "10.0.1"
+				},
+				"compile": {
+					"lib/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/Newtonsoft.Json.Bson.dll": {}
+				}
+			},
+			"NuGet.Common/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"NuGet.Frameworks": "5.6.0",
+					"System.Diagnostics.Process": "4.3.0",
+					"System.Threading.Thread": "4.3.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.Common.dll": {}
+				}
+			},
+			"NuGet.Configuration/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"NuGet.Common": "5.6.0",
+					"System.Security.Cryptography.ProtectedData": "4.3.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.Configuration.dll": {}
+				}
+			},
+			"NuGet.DependencyResolver.Core/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"NuGet.LibraryModel": "5.6.0",
+					"NuGet.Protocol": "5.6.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.DependencyResolver.Core.dll": {}
+				}
+			},
+			"NuGet.Frameworks/5.6.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.Frameworks.dll": {}
+				}
+			},
+			"NuGet.LibraryModel/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"NuGet.Common": "5.6.0",
+					"NuGet.Versioning": "5.6.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.LibraryModel.dll": {}
+				}
+			},
+			"NuGet.Packaging/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"Newtonsoft.Json": "9.0.1",
+					"NuGet.Configuration": "5.6.0",
+					"NuGet.Versioning": "5.6.0",
+					"System.Dynamic.Runtime": "4.3.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.Packaging.dll": {}
+				}
+			},
+			"NuGet.ProjectModel/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"NuGet.DependencyResolver.Core": "5.6.0",
+					"System.Dynamic.Runtime": "4.3.0",
+					"System.Threading.Thread": "4.3.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.ProjectModel.dll": {}
+				}
+			},
+			"NuGet.Protocol/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"NuGet.Packaging": "5.6.0",
+					"System.Dynamic.Runtime": "4.3.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.Protocol.dll": {}
+				}
+			},
+			"NuGet.Versioning/5.6.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.Versioning.dll": {}
+				}
+			},
+			"Polly/7.0.3": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/Polly.dll": {}
+				}
+			},
+			"runtime.native.System/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0"
+				},
+				"compile": {
+					"lib/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.0/_._": {}
+				}
+			},
+			"SemanticVersioning/1.2.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/SemVer.dll": {}
+				}
+			},
+			"StyleCop.Analyzers/1.0.2": {
+				"type": "package"
+			},
+			"System.Collections/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.Composition.AttributedModel/1.4.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Composition.AttributedModel.dll": {}
+				}
+			},
+			"System.Composition.Convention/1.4.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Composition.AttributedModel": "1.4.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Composition.Convention.dll": {}
+				}
+			},
+			"System.Composition.Hosting/1.4.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Composition.Runtime": "1.4.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Composition.Hosting.dll": {}
+				}
+			},
+			"System.Composition.Runtime/1.4.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Composition.Runtime.dll": {}
+				}
+			},
+			"System.Composition.TypedParts/1.4.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Composition.AttributedModel": "1.4.0",
+					"System.Composition.Hosting": "1.4.0",
+					"System.Composition.Runtime": "1.4.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Composition.TypedParts.dll": {}
+				}
+			},
+			"System.Diagnostics.Debug/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.Diagnostics.Process/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.Win32.Primitives": "4.3.0",
+					"Microsoft.Win32.Registry": "4.3.0",
+					"System.Collections": "4.3.0",
+					"System.Diagnostics.Debug": "4.3.0",
+					"System.Globalization": "4.3.0",
+					"System.IO": "4.3.0",
+					"System.IO.FileSystem": "4.3.0",
+					"System.IO.FileSystem.Primitives": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Extensions": "4.3.0",
+					"System.Runtime.Handles": "4.3.0",
+					"System.Runtime.InteropServices": "4.3.0",
+					"System.Text.Encoding": "4.3.0",
+					"System.Text.Encoding.Extensions": "4.3.0",
+					"System.Threading": "4.3.0",
+					"System.Threading.Tasks": "4.3.0",
+					"System.Threading.Thread": "4.3.0",
+					"System.Threading.ThreadPool": "4.3.0",
+					"runtime.native.System": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.4/_._": {}
+				},
+				"runtimeTargets": {
+					"runtimes/linux/lib/netstandard1.4/System.Diagnostics.Process.dll": {
+						"assetType": "runtime",
+						"rid": "linux"
+					},
+					"runtimes/osx/lib/netstandard1.4/System.Diagnostics.Process.dll": {
+						"assetType": "runtime",
+						"rid": "osx"
+					},
+					"runtimes/win/lib/netstandard1.4/System.Diagnostics.Process.dll": {
+						"assetType": "runtime",
+						"rid": "win"
+					}
+				}
+			},
+			"System.Dynamic.Runtime/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Collections": "4.3.0",
+					"System.Diagnostics.Debug": "4.3.0",
+					"System.Linq": "4.3.0",
+					"System.Linq.Expressions": "4.3.0",
+					"System.ObjectModel": "4.3.0",
+					"System.Reflection": "4.3.0",
+					"System.Reflection.Emit": "4.3.0",
+					"System.Reflection.Emit.ILGeneration": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Reflection.TypeExtensions": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Extensions": "4.3.0",
+					"System.Threading": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Dynamic.Runtime.dll": {}
+				}
+			},
+			"System.Globalization/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.IO/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0",
+					"System.Text.Encoding": "4.3.0",
+					"System.Threading.Tasks": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.5/_._": {}
+				}
+			},
+			"System.IO.FileSystem/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.IO": "4.3.0",
+					"System.IO.FileSystem.Primitives": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Handles": "4.3.0",
+					"System.Text.Encoding": "4.3.0",
+					"System.Threading.Tasks": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.IO.FileSystem.Primitives/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.IO.FileSystem.Primitives.dll": {}
+				}
+			},
+			"System.Linq/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Collections": "4.3.0",
+					"System.Diagnostics.Debug": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Extensions": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.6/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.6/System.Linq.dll": {}
+				}
+			},
+			"System.Linq.Expressions/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Collections": "4.3.0",
+					"System.Diagnostics.Debug": "4.3.0",
+					"System.Globalization": "4.3.0",
+					"System.IO": "4.3.0",
+					"System.Linq": "4.3.0",
+					"System.ObjectModel": "4.3.0",
+					"System.Reflection": "4.3.0",
+					"System.Reflection.Emit": "4.3.0",
+					"System.Reflection.Emit.ILGeneration": "4.3.0",
+					"System.Reflection.Emit.Lightweight": "4.3.0",
+					"System.Reflection.Extensions": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Reflection.TypeExtensions": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Extensions": "4.3.0",
+					"System.Threading": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.6/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.6/System.Linq.Expressions.dll": {}
+				}
+			},
+			"System.ObjectModel/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Collections": "4.3.0",
+					"System.Diagnostics.Debug": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Threading": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.ObjectModel.dll": {}
+				}
+			},
+			"System.Reactive/4.1.2": {
+				"type": "package",
+				"dependencies": {
+					"System.Runtime.InteropServices.WindowsRuntime": "4.3.0",
+					"System.Threading.Tasks.Extensions": "4.5.1"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Reactive.dll": {}
+				}
+			},
+			"System.Reflection/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.IO": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.5/_._": {}
+				}
+			},
+			"System.Reflection.Emit/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.IO": "4.3.0",
+					"System.Reflection": "4.3.0",
+					"System.Reflection.Emit.ILGeneration": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.1/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Reflection.Emit.dll": {}
+				}
+			},
+			"System.Reflection.Emit.ILGeneration/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Reflection": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Reflection.Emit.ILGeneration.dll": {}
+				}
+			},
+			"System.Reflection.Emit.Lightweight/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Reflection": "4.3.0",
+					"System.Reflection.Emit.ILGeneration": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Reflection.Emit.Lightweight.dll": {}
+				}
+			},
+			"System.Reflection.Extensions/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Reflection": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.0/_._": {}
+				}
+			},
+			"System.Reflection.Primitives/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.0/_._": {}
+				}
+			},
+			"System.Reflection.TypeExtensions/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Reflection": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.5/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.5/System.Reflection.TypeExtensions.dll": {}
+				}
+			},
+			"System.Resources.ResourceManager/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Globalization": "4.3.0",
+					"System.Reflection": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.0/_._": {}
+				}
+			},
+			"System.Runtime/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0"
+				},
+				"compile": {
+					"ref/netstandard1.5/_._": {}
+				}
+			},
+			"System.Runtime.Extensions/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.5/_._": {}
+				}
+			},
+			"System.Runtime.Handles/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.Runtime.InteropServices/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Reflection": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Handles": "4.3.0"
+				},
+				"compile": {
+					"ref/netcoreapp1.1/_._": {}
+				}
+			},
+			"System.Runtime.InteropServices.WindowsRuntime/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Runtime.InteropServices.WindowsRuntime.dll": {}
+				}
+			},
+			"System.Runtime.Loader/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.IO": "4.3.0",
+					"System.Reflection": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.5/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.5/System.Runtime.Loader.dll": {}
+				}
+			},
+			"System.Security.Cryptography.Primitives/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Diagnostics.Debug": "4.3.0",
+					"System.Globalization": "4.3.0",
+					"System.IO": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Threading": "4.3.0",
+					"System.Threading.Tasks": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Security.Cryptography.Primitives.dll": {}
+				}
+			},
+			"System.Security.Cryptography.ProtectedData/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.InteropServices": "4.3.0",
+					"System.Security.Cryptography.Primitives": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtimeTargets": {
+					"runtimes/unix/lib/netstandard1.3/System.Security.Cryptography.ProtectedData.dll": {
+						"assetType": "runtime",
+						"rid": "unix"
+					},
+					"runtimes/win/lib/netstandard1.3/System.Security.Cryptography.ProtectedData.dll": {
+						"assetType": "runtime",
+						"rid": "win"
+					}
+				}
+			},
+			"System.Text.Encoding/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.Text.Encoding.Extensions/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0",
+					"System.Text.Encoding": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.Threading/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Runtime": "4.3.0",
+					"System.Threading.Tasks": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Threading.dll": {}
+				}
+			},
+			"System.Threading.Tasks/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.Threading.Tasks.Dataflow/4.9.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Threading.Tasks.Dataflow.dll": {}
+				}
+			},
+			"System.Threading.Tasks.Extensions/4.5.1": {
+				"type": "package",
+				"compile": {
+					"ref/netcoreapp2.1/_._": {}
+				},
+				"runtime": {
+					"lib/netcoreapp2.1/_._": {}
+				}
+			},
+			"System.Threading.Thread/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Threading.Thread.dll": {}
+				}
+			},
+			"System.Threading.ThreadPool/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Handles": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Threading.ThreadPool.dll": {}
+				}
+			},
+			"YamlDotNet/5.3.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/YamlDotNet.dll": {}
+				}
+			},
+			"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common/1.0.0": {
+				"type": "project",
+				"framework": ".NETCoreApp,Version=v2.2",
+				"dependencies": {
+					"Microsoft.AspNet.WebApi.Client": "5.2.7",
+					"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts": "1.0.0",
+					"Newtonsoft.Json": "12.0.3",
+					"System.Composition.AttributedModel": "1.4.0",
+					"System.Composition.Convention": "1.4.0",
+					"System.Composition.Hosting": "1.4.0",
+					"System.Composition.Runtime": "1.4.0",
+					"System.Composition.TypedParts": "1.4.0",
+					"System.Reactive": "4.1.2"
+				},
+				"compile": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common.dll": {}
+				},
+				"runtime": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common.dll": {}
+				}
+			},
+			"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts/1.0.0": {
+				"type": "project",
+				"framework": ".NETCoreApp,Version=v2.2",
+				"dependencies": {
+					"Newtonsoft.Json": "12.0.3",
+					"System.Composition.AttributedModel": "1.4.0",
+					"System.Reactive": "4.1.2"
+				},
+				"compile": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts.dll": {}
+				},
+				"runtime": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts.dll": {}
+				}
+			},
+			"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Detectors/1.0.0": {
+				"type": "project",
+				"framework": ".NETCoreApp,Version=v2.2",
+				"dependencies": {
+					"DotNet.Glob": "2.1.1",
+					"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common": "1.0.0",
+					"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts": "1.0.0",
+					"Nett": "0.10.0",
+					"Newtonsoft.Json": "12.0.3",
+					"NuGet.ProjectModel": "5.6.0",
+					"NuGet.Versioning": "5.6.0",
+					"Polly": "7.0.3",
+					"SemanticVersioning": "1.2.0",
+					"System.Composition.AttributedModel": "1.4.0",
+					"System.Composition.Convention": "1.4.0",
+					"System.Composition.Hosting": "1.4.0",
+					"System.Composition.Runtime": "1.4.0",
+					"System.Composition.TypedParts": "1.4.0",
+					"System.Reactive": "4.1.2",
+					"System.Threading.Tasks.Dataflow": "4.9.0",
+					"yamldotnet": "5.3.0"
+				},
+				"compile": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Detectors.dll": {}
+				},
+				"runtime": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Detectors.dll": {}
+				}
+			},
+			"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Orchestrator/1.0.0": {
+				"type": "project",
+				"framework": ".NETCoreApp,Version=v2.2",
+				"dependencies": {
+					"CommandLineParser": "2.8.0",
+					"DotNet.Glob": "2.1.1",
+					"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common": "1.0.0",
+					"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts": "1.0.0",
+					"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Detectors": "1.0.0",
+					"Newtonsoft.Json": "12.0.3",
+					"Polly": "7.0.3",
+					"System.Composition.AttributedModel": "1.4.0",
+					"System.Composition.Convention": "1.4.0",
+					"System.Composition.Hosting": "1.4.0",
+					"System.Composition.Runtime": "1.4.0",
+					"System.Composition.TypedParts": "1.4.0",
+					"System.Reactive": "4.1.2",
+					"System.Runtime.Loader": "4.3.0",
+					"System.Threading.Tasks.Dataflow": "4.9.0"
+				},
+				"compile": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Orchestrator.dll": {}
+				},
+				"runtime": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Orchestrator.dll": {}
+				}
+			}
+		},
+		".NETCoreApp,Version=v2.2/linux-x64": {
+			"CommandLineParser/2.8.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/CommandLine.dll": {}
+				}
+			},
+			"coverlet.msbuild/2.5.1": {
+				"type": "package",
+				"build": {
+					"build/netstandard2.0/coverlet.msbuild.props": {},
+					"build/netstandard2.0/coverlet.msbuild.targets": {}
+				}
+			},
+			"DotNet.Glob/2.1.1": {
+				"type": "package",
+				"dependencies": {
+					"NETStandard.Library": "1.6.1"
+				},
+				"compile": {
+					"lib/netstandard1.1/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.1/DotNet.Glob.dll": {}
+				}
+			},
+			"Microsoft.AspNet.WebApi.Client/5.2.7": {
+				"type": "package",
+				"dependencies": {
+					"Newtonsoft.Json": "10.0.1",
+					"Newtonsoft.Json.Bson": "1.0.1"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Net.Http.Formatting.dll": {}
+				}
+			},
+			"Microsoft.NETCore.App/2.2.8": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.DotNetHostPolicy": "2.2.8",
+					"Microsoft.NETCore.Platforms": "2.2.4",
+					"Microsoft.NETCore.Targets": "2.0.0",
+					"NETStandard.Library": "2.0.3",
+					"runtime.linux-x64.Microsoft.NETCore.App": "2.2.8"
+				},
+				"compile": {
+					"ref/netcoreapp2.2/Microsoft.CSharp.dll": {},
+					"ref/netcoreapp2.2/Microsoft.VisualBasic.dll": {},
+					"ref/netcoreapp2.2/Microsoft.Win32.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.AppContext.dll": {},
+					"ref/netcoreapp2.2/System.Buffers.dll": {},
+					"ref/netcoreapp2.2/System.Collections.Concurrent.dll": {},
+					"ref/netcoreapp2.2/System.Collections.Immutable.dll": {},
+					"ref/netcoreapp2.2/System.Collections.NonGeneric.dll": {},
+					"ref/netcoreapp2.2/System.Collections.Specialized.dll": {},
+					"ref/netcoreapp2.2/System.Collections.dll": {},
+					"ref/netcoreapp2.2/System.ComponentModel.Annotations.dll": {},
+					"ref/netcoreapp2.2/System.ComponentModel.DataAnnotations.dll": {},
+					"ref/netcoreapp2.2/System.ComponentModel.EventBasedAsync.dll": {},
+					"ref/netcoreapp2.2/System.ComponentModel.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.ComponentModel.TypeConverter.dll": {},
+					"ref/netcoreapp2.2/System.ComponentModel.dll": {},
+					"ref/netcoreapp2.2/System.Configuration.dll": {},
+					"ref/netcoreapp2.2/System.Console.dll": {},
+					"ref/netcoreapp2.2/System.Core.dll": {},
+					"ref/netcoreapp2.2/System.Data.Common.dll": {},
+					"ref/netcoreapp2.2/System.Data.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.Contracts.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.Debug.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.DiagnosticSource.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.FileVersionInfo.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.Process.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.StackTrace.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.TextWriterTraceListener.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.Tools.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.TraceSource.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.Tracing.dll": {},
+					"ref/netcoreapp2.2/System.Drawing.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.Drawing.dll": {},
+					"ref/netcoreapp2.2/System.Dynamic.Runtime.dll": {},
+					"ref/netcoreapp2.2/System.Globalization.Calendars.dll": {},
+					"ref/netcoreapp2.2/System.Globalization.Extensions.dll": {},
+					"ref/netcoreapp2.2/System.Globalization.dll": {},
+					"ref/netcoreapp2.2/System.IO.Compression.Brotli.dll": {},
+					"ref/netcoreapp2.2/System.IO.Compression.FileSystem.dll": {},
+					"ref/netcoreapp2.2/System.IO.Compression.ZipFile.dll": {},
+					"ref/netcoreapp2.2/System.IO.Compression.dll": {},
+					"ref/netcoreapp2.2/System.IO.FileSystem.DriveInfo.dll": {},
+					"ref/netcoreapp2.2/System.IO.FileSystem.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.IO.FileSystem.Watcher.dll": {},
+					"ref/netcoreapp2.2/System.IO.FileSystem.dll": {},
+					"ref/netcoreapp2.2/System.IO.IsolatedStorage.dll": {},
+					"ref/netcoreapp2.2/System.IO.MemoryMappedFiles.dll": {},
+					"ref/netcoreapp2.2/System.IO.Pipes.dll": {},
+					"ref/netcoreapp2.2/System.IO.UnmanagedMemoryStream.dll": {},
+					"ref/netcoreapp2.2/System.IO.dll": {},
+					"ref/netcoreapp2.2/System.Linq.Expressions.dll": {},
+					"ref/netcoreapp2.2/System.Linq.Parallel.dll": {},
+					"ref/netcoreapp2.2/System.Linq.Queryable.dll": {},
+					"ref/netcoreapp2.2/System.Linq.dll": {},
+					"ref/netcoreapp2.2/System.Memory.dll": {},
+					"ref/netcoreapp2.2/System.Net.Http.dll": {},
+					"ref/netcoreapp2.2/System.Net.HttpListener.dll": {},
+					"ref/netcoreapp2.2/System.Net.Mail.dll": {},
+					"ref/netcoreapp2.2/System.Net.NameResolution.dll": {},
+					"ref/netcoreapp2.2/System.Net.NetworkInformation.dll": {},
+					"ref/netcoreapp2.2/System.Net.Ping.dll": {},
+					"ref/netcoreapp2.2/System.Net.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.Net.Requests.dll": {},
+					"ref/netcoreapp2.2/System.Net.Security.dll": {},
+					"ref/netcoreapp2.2/System.Net.ServicePoint.dll": {},
+					"ref/netcoreapp2.2/System.Net.Sockets.dll": {},
+					"ref/netcoreapp2.2/System.Net.WebClient.dll": {},
+					"ref/netcoreapp2.2/System.Net.WebHeaderCollection.dll": {},
+					"ref/netcoreapp2.2/System.Net.WebProxy.dll": {},
+					"ref/netcoreapp2.2/System.Net.WebSockets.Client.dll": {},
+					"ref/netcoreapp2.2/System.Net.WebSockets.dll": {},
+					"ref/netcoreapp2.2/System.Net.dll": {},
+					"ref/netcoreapp2.2/System.Numerics.Vectors.dll": {},
+					"ref/netcoreapp2.2/System.Numerics.dll": {},
+					"ref/netcoreapp2.2/System.ObjectModel.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.DispatchProxy.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.Emit.ILGeneration.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.Emit.Lightweight.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.Emit.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.Extensions.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.Metadata.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.TypeExtensions.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.dll": {},
+					"ref/netcoreapp2.2/System.Resources.Reader.dll": {},
+					"ref/netcoreapp2.2/System.Resources.ResourceManager.dll": {},
+					"ref/netcoreapp2.2/System.Resources.Writer.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.CompilerServices.VisualC.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Extensions.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Handles.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.InteropServices.RuntimeInformation.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.InteropServices.WindowsRuntime.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.InteropServices.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Loader.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Numerics.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Serialization.Formatters.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Serialization.Json.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Serialization.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Serialization.Xml.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Serialization.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.dll": {},
+					"ref/netcoreapp2.2/System.Security.Claims.dll": {},
+					"ref/netcoreapp2.2/System.Security.Cryptography.Algorithms.dll": {},
+					"ref/netcoreapp2.2/System.Security.Cryptography.Csp.dll": {},
+					"ref/netcoreapp2.2/System.Security.Cryptography.Encoding.dll": {},
+					"ref/netcoreapp2.2/System.Security.Cryptography.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.Security.Cryptography.X509Certificates.dll": {},
+					"ref/netcoreapp2.2/System.Security.Principal.dll": {},
+					"ref/netcoreapp2.2/System.Security.SecureString.dll": {},
+					"ref/netcoreapp2.2/System.Security.dll": {},
+					"ref/netcoreapp2.2/System.ServiceModel.Web.dll": {},
+					"ref/netcoreapp2.2/System.ServiceProcess.dll": {},
+					"ref/netcoreapp2.2/System.Text.Encoding.Extensions.dll": {},
+					"ref/netcoreapp2.2/System.Text.Encoding.dll": {},
+					"ref/netcoreapp2.2/System.Text.RegularExpressions.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Overlapped.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Tasks.Dataflow.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Tasks.Extensions.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Tasks.Parallel.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Tasks.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Thread.dll": {},
+					"ref/netcoreapp2.2/System.Threading.ThreadPool.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Timer.dll": {},
+					"ref/netcoreapp2.2/System.Threading.dll": {},
+					"ref/netcoreapp2.2/System.Transactions.Local.dll": {},
+					"ref/netcoreapp2.2/System.Transactions.dll": {},
+					"ref/netcoreapp2.2/System.ValueTuple.dll": {},
+					"ref/netcoreapp2.2/System.Web.HttpUtility.dll": {},
+					"ref/netcoreapp2.2/System.Web.dll": {},
+					"ref/netcoreapp2.2/System.Windows.dll": {},
+					"ref/netcoreapp2.2/System.Xml.Linq.dll": {},
+					"ref/netcoreapp2.2/System.Xml.ReaderWriter.dll": {},
+					"ref/netcoreapp2.2/System.Xml.Serialization.dll": {},
+					"ref/netcoreapp2.2/System.Xml.XDocument.dll": {},
+					"ref/netcoreapp2.2/System.Xml.XPath.XDocument.dll": {},
+					"ref/netcoreapp2.2/System.Xml.XPath.dll": {},
+					"ref/netcoreapp2.2/System.Xml.XmlDocument.dll": {},
+					"ref/netcoreapp2.2/System.Xml.XmlSerializer.dll": {},
+					"ref/netcoreapp2.2/System.Xml.dll": {},
+					"ref/netcoreapp2.2/System.dll": {},
+					"ref/netcoreapp2.2/WindowsBase.dll": {},
+					"ref/netcoreapp2.2/mscorlib.dll": {},
+					"ref/netcoreapp2.2/netstandard.dll": {}
+				},
+				"build": {
+					"build/netcoreapp2.2/Microsoft.NETCore.App.props": {},
+					"build/netcoreapp2.2/Microsoft.NETCore.App.targets": {}
+				}
+			},
+			"Microsoft.NETCore.DotNetAppHost/2.2.8": {
+				"type": "package",
+				"dependencies": {
+					"runtime.linux-x64.Microsoft.NETCore.DotNetAppHost": "2.2.8"
+				}
+			},
+			"Microsoft.NETCore.DotNetHostPolicy/2.2.8": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.DotNetHostResolver": "2.2.8",
+					"runtime.linux-x64.Microsoft.NETCore.DotNetHostPolicy": "2.2.8"
+				}
+			},
+			"Microsoft.NETCore.DotNetHostResolver/2.2.8": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.DotNetAppHost": "2.2.8",
+					"runtime.linux-x64.Microsoft.NETCore.DotNetHostResolver": "2.2.8"
+				}
+			},
+			"Microsoft.NETCore.Platforms/2.2.4": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.0/_._": {}
+				}
+			},
+			"Microsoft.NETCore.Targets/2.0.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.0/_._": {}
+				}
+			},
+			"Microsoft.Win32.Primitives/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"Microsoft.Win32.Registry/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"System.Collections": "4.3.0",
+					"System.Globalization": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Extensions": "4.3.0",
+					"System.Runtime.Handles": "4.3.0",
+					"System.Runtime.InteropServices": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"runtimes/unix/lib/netstandard1.3/Microsoft.Win32.Registry.dll": {}
+				}
+			},
+			"MinVer/2.5.0": {
+				"type": "package",
+				"build": {
+					"build/MinVer.targets": {}
+				},
+				"buildMultiTargeting": {
+					"buildMultiTargeting/MinVer.targets": {}
+				}
+			},
+			"NETStandard.Library/2.0.3": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0"
+				},
+				"compile": {
+					"lib/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.0/_._": {}
+				},
+				"build": {
+					"build/netstandard2.0/NETStandard.Library.targets": {}
+				}
+			},
+			"Nett/0.10.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/Nett.dll": {}
+				}
+			},
+			"Newtonsoft.Json/12.0.3": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/Newtonsoft.Json.dll": {}
+				}
+			},
+			"Newtonsoft.Json.Bson/1.0.1": {
+				"type": "package",
+				"dependencies": {
+					"NETStandard.Library": "1.6.1",
+					"Newtonsoft.Json": "10.0.1"
+				},
+				"compile": {
+					"lib/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/Newtonsoft.Json.Bson.dll": {}
+				}
+			},
+			"NuGet.Common/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"NuGet.Frameworks": "5.6.0",
+					"System.Diagnostics.Process": "4.3.0",
+					"System.Threading.Thread": "4.3.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.Common.dll": {}
+				}
+			},
+			"NuGet.Configuration/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"NuGet.Common": "5.6.0",
+					"System.Security.Cryptography.ProtectedData": "4.3.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.Configuration.dll": {}
+				}
+			},
+			"NuGet.DependencyResolver.Core/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"NuGet.LibraryModel": "5.6.0",
+					"NuGet.Protocol": "5.6.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.DependencyResolver.Core.dll": {}
+				}
+			},
+			"NuGet.Frameworks/5.6.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.Frameworks.dll": {}
+				}
+			},
+			"NuGet.LibraryModel/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"NuGet.Common": "5.6.0",
+					"NuGet.Versioning": "5.6.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.LibraryModel.dll": {}
+				}
+			},
+			"NuGet.Packaging/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"Newtonsoft.Json": "9.0.1",
+					"NuGet.Configuration": "5.6.0",
+					"NuGet.Versioning": "5.6.0",
+					"System.Dynamic.Runtime": "4.3.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.Packaging.dll": {}
+				}
+			},
+			"NuGet.ProjectModel/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"NuGet.DependencyResolver.Core": "5.6.0",
+					"System.Dynamic.Runtime": "4.3.0",
+					"System.Threading.Thread": "4.3.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.ProjectModel.dll": {}
+				}
+			},
+			"NuGet.Protocol/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"NuGet.Packaging": "5.6.0",
+					"System.Dynamic.Runtime": "4.3.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.Protocol.dll": {}
+				}
+			},
+			"NuGet.Versioning/5.6.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.Versioning.dll": {}
+				}
+			},
+			"Polly/7.0.3": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/Polly.dll": {}
+				}
+			},
+			"runtime.linux-x64.Microsoft.NETCore.App/2.2.8": {
+				"type": "package",
+				"compile": {
+					"ref/netstandard/_._": {}
+				},
+				"runtime": {
+					"runtimes/linux-x64/lib/netcoreapp2.2/Microsoft.CSharp.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/Microsoft.VisualBasic.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/Microsoft.Win32.Primitives.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/Microsoft.Win32.Registry.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/SOS.NETCore.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.AppContext.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Buffers.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Collections.Concurrent.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Collections.Immutable.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Collections.NonGeneric.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Collections.Specialized.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Collections.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.ComponentModel.Annotations.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.ComponentModel.DataAnnotations.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.ComponentModel.EventBasedAsync.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.ComponentModel.Primitives.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.ComponentModel.TypeConverter.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.ComponentModel.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Configuration.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Console.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Core.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Data.Common.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Data.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.Contracts.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.Debug.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.DiagnosticSource.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.FileVersionInfo.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.Process.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.StackTrace.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.TextWriterTraceListener.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.Tools.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.TraceSource.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.Tracing.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Drawing.Primitives.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Drawing.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Dynamic.Runtime.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Globalization.Calendars.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Globalization.Extensions.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Globalization.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.Compression.Brotli.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.Compression.FileSystem.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.Compression.ZipFile.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.Compression.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.FileSystem.AccessControl.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.FileSystem.DriveInfo.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.FileSystem.Primitives.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.FileSystem.Watcher.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.FileSystem.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.IsolatedStorage.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.MemoryMappedFiles.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.Pipes.AccessControl.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.Pipes.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.UnmanagedMemoryStream.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Linq.Expressions.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Linq.Parallel.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Linq.Queryable.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Linq.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Memory.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.Http.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.HttpListener.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.Mail.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.NameResolution.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.NetworkInformation.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.Ping.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.Primitives.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.Requests.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.Security.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.ServicePoint.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.Sockets.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.WebClient.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.WebHeaderCollection.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.WebProxy.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.WebSockets.Client.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.WebSockets.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Numerics.Vectors.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Numerics.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.ObjectModel.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Private.DataContractSerialization.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Private.Uri.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Private.Xml.Linq.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Private.Xml.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.DispatchProxy.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.Emit.ILGeneration.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.Emit.Lightweight.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.Emit.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.Extensions.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.Metadata.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.Primitives.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.TypeExtensions.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Resources.Reader.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Resources.ResourceManager.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Resources.Writer.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.CompilerServices.VisualC.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Extensions.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Handles.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.InteropServices.RuntimeInformation.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.InteropServices.WindowsRuntime.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.InteropServices.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Loader.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Numerics.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Formatters.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Json.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Primitives.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Xml.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Serialization.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.AccessControl.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Claims.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Cryptography.Algorithms.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Cryptography.Cng.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Cryptography.Csp.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Cryptography.Encoding.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Cryptography.OpenSsl.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Cryptography.Primitives.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Cryptography.X509Certificates.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Principal.Windows.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Principal.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.SecureString.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.ServiceModel.Web.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.ServiceProcess.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Text.Encoding.Extensions.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Text.Encoding.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Text.RegularExpressions.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.Overlapped.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.Tasks.Dataflow.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.Tasks.Extensions.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.Tasks.Parallel.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.Tasks.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.Thread.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.ThreadPool.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.Timer.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Transactions.Local.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Transactions.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.ValueTuple.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Web.HttpUtility.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Web.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Windows.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.Linq.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.ReaderWriter.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.Serialization.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.XDocument.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.XPath.XDocument.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.XPath.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.XmlDocument.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.XmlSerializer.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/System.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/WindowsBase.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/mscorlib.dll": {},
+					"runtimes/linux-x64/lib/netcoreapp2.2/netstandard.dll": {}
+				},
+				"native": {
+					"runtimes/linux-x64/native/System.Globalization.Native.so": {},
+					"runtimes/linux-x64/native/System.IO.Compression.Native.a": {},
+					"runtimes/linux-x64/native/System.IO.Compression.Native.so": {},
+					"runtimes/linux-x64/native/System.Native.a": {},
+					"runtimes/linux-x64/native/System.Native.so": {},
+					"runtimes/linux-x64/native/System.Net.Http.Native.a": {},
+					"runtimes/linux-x64/native/System.Net.Http.Native.so": {},
+					"runtimes/linux-x64/native/System.Net.Security.Native.a": {},
+					"runtimes/linux-x64/native/System.Net.Security.Native.so": {},
+					"runtimes/linux-x64/native/System.Private.CoreLib.dll": {},
+					"runtimes/linux-x64/native/System.Security.Cryptography.Native.OpenSsl.a": {},
+					"runtimes/linux-x64/native/System.Security.Cryptography.Native.OpenSsl.so": {},
+					"runtimes/linux-x64/native/createdump": {},
+					"runtimes/linux-x64/native/libclrjit.so": {},
+					"runtimes/linux-x64/native/libcoreclr.so": {},
+					"runtimes/linux-x64/native/libcoreclrtraceptprovider.so": {},
+					"runtimes/linux-x64/native/libdbgshim.so": {},
+					"runtimes/linux-x64/native/libmscordaccore.so": {},
+					"runtimes/linux-x64/native/libmscordbi.so": {},
+					"runtimes/linux-x64/native/libsos.so": {},
+					"runtimes/linux-x64/native/libsosplugin.so": {},
+					"runtimes/linux-x64/native/sosdocsunix.txt": {}
+				}
+			},
+			"runtime.linux-x64.Microsoft.NETCore.DotNetAppHost/2.2.8": {
+				"type": "package",
+				"native": {
+					"runtimes/linux-x64/native/apphost": {}
+				}
+			},
+			"runtime.linux-x64.Microsoft.NETCore.DotNetHostPolicy/2.2.8": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.DotNetHostResolver": "2.2.8"
+				},
+				"native": {
+					"runtimes/linux-x64/native/libhostpolicy.so": {}
+				}
+			},
+			"runtime.linux-x64.Microsoft.NETCore.DotNetHostResolver/2.2.8": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.DotNetAppHost": "2.2.8"
+				},
+				"native": {
+					"runtimes/linux-x64/native/libhostfxr.so": {}
+				}
+			},
+			"runtime.native.System/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0"
+				},
+				"compile": {
+					"lib/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.0/_._": {}
+				}
+			},
+			"SemanticVersioning/1.2.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/SemVer.dll": {}
+				}
+			},
+			"StyleCop.Analyzers/1.0.2": {
+				"type": "package"
+			},
+			"System.Collections/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.Composition.AttributedModel/1.4.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Composition.AttributedModel.dll": {}
+				}
+			},
+			"System.Composition.Convention/1.4.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Composition.AttributedModel": "1.4.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Composition.Convention.dll": {}
+				}
+			},
+			"System.Composition.Hosting/1.4.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Composition.Runtime": "1.4.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Composition.Hosting.dll": {}
+				}
+			},
+			"System.Composition.Runtime/1.4.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Composition.Runtime.dll": {}
+				}
+			},
+			"System.Composition.TypedParts/1.4.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Composition.AttributedModel": "1.4.0",
+					"System.Composition.Hosting": "1.4.0",
+					"System.Composition.Runtime": "1.4.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Composition.TypedParts.dll": {}
+				}
+			},
+			"System.Diagnostics.Debug/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.Diagnostics.Process/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.Win32.Primitives": "4.3.0",
+					"Microsoft.Win32.Registry": "4.3.0",
+					"System.Collections": "4.3.0",
+					"System.Diagnostics.Debug": "4.3.0",
+					"System.Globalization": "4.3.0",
+					"System.IO": "4.3.0",
+					"System.IO.FileSystem": "4.3.0",
+					"System.IO.FileSystem.Primitives": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Extensions": "4.3.0",
+					"System.Runtime.Handles": "4.3.0",
+					"System.Runtime.InteropServices": "4.3.0",
+					"System.Text.Encoding": "4.3.0",
+					"System.Text.Encoding.Extensions": "4.3.0",
+					"System.Threading": "4.3.0",
+					"System.Threading.Tasks": "4.3.0",
+					"System.Threading.Thread": "4.3.0",
+					"System.Threading.ThreadPool": "4.3.0",
+					"runtime.native.System": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.4/_._": {}
+				},
+				"runtime": {
+					"runtimes/linux/lib/netstandard1.4/System.Diagnostics.Process.dll": {}
+				}
+			},
+			"System.Dynamic.Runtime/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Collections": "4.3.0",
+					"System.Diagnostics.Debug": "4.3.0",
+					"System.Linq": "4.3.0",
+					"System.Linq.Expressions": "4.3.0",
+					"System.ObjectModel": "4.3.0",
+					"System.Reflection": "4.3.0",
+					"System.Reflection.Emit": "4.3.0",
+					"System.Reflection.Emit.ILGeneration": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Reflection.TypeExtensions": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Extensions": "4.3.0",
+					"System.Threading": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Dynamic.Runtime.dll": {}
+				}
+			},
+			"System.Globalization/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.IO/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0",
+					"System.Text.Encoding": "4.3.0",
+					"System.Threading.Tasks": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.5/_._": {}
+				}
+			},
+			"System.IO.FileSystem/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.IO": "4.3.0",
+					"System.IO.FileSystem.Primitives": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Handles": "4.3.0",
+					"System.Text.Encoding": "4.3.0",
+					"System.Threading.Tasks": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.IO.FileSystem.Primitives/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.IO.FileSystem.Primitives.dll": {}
+				}
+			},
+			"System.Linq/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Collections": "4.3.0",
+					"System.Diagnostics.Debug": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Extensions": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.6/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.6/System.Linq.dll": {}
+				}
+			},
+			"System.Linq.Expressions/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Collections": "4.3.0",
+					"System.Diagnostics.Debug": "4.3.0",
+					"System.Globalization": "4.3.0",
+					"System.IO": "4.3.0",
+					"System.Linq": "4.3.0",
+					"System.ObjectModel": "4.3.0",
+					"System.Reflection": "4.3.0",
+					"System.Reflection.Emit": "4.3.0",
+					"System.Reflection.Emit.ILGeneration": "4.3.0",
+					"System.Reflection.Emit.Lightweight": "4.3.0",
+					"System.Reflection.Extensions": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Reflection.TypeExtensions": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Extensions": "4.3.0",
+					"System.Threading": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.6/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.6/System.Linq.Expressions.dll": {}
+				}
+			},
+			"System.ObjectModel/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Collections": "4.3.0",
+					"System.Diagnostics.Debug": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Threading": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.ObjectModel.dll": {}
+				}
+			},
+			"System.Reactive/4.1.2": {
+				"type": "package",
+				"dependencies": {
+					"System.Runtime.InteropServices.WindowsRuntime": "4.3.0",
+					"System.Threading.Tasks.Extensions": "4.5.1"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Reactive.dll": {}
+				}
+			},
+			"System.Reflection/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.IO": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.5/_._": {}
+				}
+			},
+			"System.Reflection.Emit/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.IO": "4.3.0",
+					"System.Reflection": "4.3.0",
+					"System.Reflection.Emit.ILGeneration": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.1/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Reflection.Emit.dll": {}
+				}
+			},
+			"System.Reflection.Emit.ILGeneration/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Reflection": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Reflection.Emit.ILGeneration.dll": {}
+				}
+			},
+			"System.Reflection.Emit.Lightweight/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Reflection": "4.3.0",
+					"System.Reflection.Emit.ILGeneration": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Reflection.Emit.Lightweight.dll": {}
+				}
+			},
+			"System.Reflection.Extensions/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Reflection": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.0/_._": {}
+				}
+			},
+			"System.Reflection.Primitives/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.0/_._": {}
+				}
+			},
+			"System.Reflection.TypeExtensions/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Reflection": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.5/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.5/System.Reflection.TypeExtensions.dll": {}
+				}
+			},
+			"System.Resources.ResourceManager/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Globalization": "4.3.0",
+					"System.Reflection": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.0/_._": {}
+				}
+			},
+			"System.Runtime/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0"
+				},
+				"compile": {
+					"ref/netstandard1.5/_._": {}
+				}
+			},
+			"System.Runtime.Extensions/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.5/_._": {}
+				}
+			},
+			"System.Runtime.Handles/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.Runtime.InteropServices/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Reflection": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Handles": "4.3.0"
+				},
+				"compile": {
+					"ref/netcoreapp1.1/_._": {}
+				}
+			},
+			"System.Runtime.InteropServices.WindowsRuntime/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Runtime.InteropServices.WindowsRuntime.dll": {}
+				}
+			},
+			"System.Runtime.Loader/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.IO": "4.3.0",
+					"System.Reflection": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.5/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.5/System.Runtime.Loader.dll": {}
+				}
+			},
+			"System.Security.Cryptography.Primitives/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Diagnostics.Debug": "4.3.0",
+					"System.Globalization": "4.3.0",
+					"System.IO": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Threading": "4.3.0",
+					"System.Threading.Tasks": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Security.Cryptography.Primitives.dll": {}
+				}
+			},
+			"System.Security.Cryptography.ProtectedData/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.InteropServices": "4.3.0",
+					"System.Security.Cryptography.Primitives": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"runtimes/unix/lib/netstandard1.3/System.Security.Cryptography.ProtectedData.dll": {}
+				}
+			},
+			"System.Text.Encoding/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.Text.Encoding.Extensions/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0",
+					"System.Text.Encoding": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.Threading/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Runtime": "4.3.0",
+					"System.Threading.Tasks": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Threading.dll": {}
+				}
+			},
+			"System.Threading.Tasks/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.Threading.Tasks.Dataflow/4.9.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Threading.Tasks.Dataflow.dll": {}
+				}
+			},
+			"System.Threading.Tasks.Extensions/4.5.1": {
+				"type": "package",
+				"compile": {
+					"ref/netcoreapp2.1/_._": {}
+				},
+				"runtime": {
+					"lib/netcoreapp2.1/_._": {}
+				}
+			},
+			"System.Threading.Thread/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Threading.Thread.dll": {}
+				}
+			},
+			"System.Threading.ThreadPool/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Handles": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Threading.ThreadPool.dll": {}
+				}
+			},
+			"YamlDotNet/5.3.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/YamlDotNet.dll": {}
+				}
+			},
+			"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common/1.0.0": {
+				"type": "project",
+				"framework": ".NETCoreApp,Version=v2.2",
+				"dependencies": {
+					"Microsoft.AspNet.WebApi.Client": "5.2.7",
+					"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts": "1.0.0",
+					"Newtonsoft.Json": "12.0.3",
+					"System.Composition.AttributedModel": "1.4.0",
+					"System.Composition.Convention": "1.4.0",
+					"System.Composition.Hosting": "1.4.0",
+					"System.Composition.Runtime": "1.4.0",
+					"System.Composition.TypedParts": "1.4.0",
+					"System.Reactive": "4.1.2"
+				},
+				"compile": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common.dll": {}
+				},
+				"runtime": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common.dll": {}
+				}
+			},
+			"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts/1.0.0": {
+				"type": "project",
+				"framework": ".NETCoreApp,Version=v2.2",
+				"dependencies": {
+					"Newtonsoft.Json": "12.0.3",
+					"System.Composition.AttributedModel": "1.4.0",
+					"System.Reactive": "4.1.2"
+				},
+				"compile": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts.dll": {}
+				},
+				"runtime": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts.dll": {}
+				}
+			},
+			"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Detectors/1.0.0": {
+				"type": "project",
+				"framework": ".NETCoreApp,Version=v2.2",
+				"dependencies": {
+					"DotNet.Glob": "2.1.1",
+					"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common": "1.0.0",
+					"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts": "1.0.0",
+					"Nett": "0.10.0",
+					"Newtonsoft.Json": "12.0.3",
+					"NuGet.ProjectModel": "5.6.0",
+					"NuGet.Versioning": "5.6.0",
+					"Polly": "7.0.3",
+					"SemanticVersioning": "1.2.0",
+					"System.Composition.AttributedModel": "1.4.0",
+					"System.Composition.Convention": "1.4.0",
+					"System.Composition.Hosting": "1.4.0",
+					"System.Composition.Runtime": "1.4.0",
+					"System.Composition.TypedParts": "1.4.0",
+					"System.Reactive": "4.1.2",
+					"System.Threading.Tasks.Dataflow": "4.9.0",
+					"yamldotnet": "5.3.0"
+				},
+				"compile": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Detectors.dll": {}
+				},
+				"runtime": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Detectors.dll": {}
+				}
+			},
+			"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Orchestrator/1.0.0": {
+				"type": "project",
+				"framework": ".NETCoreApp,Version=v2.2",
+				"dependencies": {
+					"CommandLineParser": "2.8.0",
+					"DotNet.Glob": "2.1.1",
+					"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common": "1.0.0",
+					"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts": "1.0.0",
+					"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Detectors": "1.0.0",
+					"Newtonsoft.Json": "12.0.3",
+					"Polly": "7.0.3",
+					"System.Composition.AttributedModel": "1.4.0",
+					"System.Composition.Convention": "1.4.0",
+					"System.Composition.Hosting": "1.4.0",
+					"System.Composition.Runtime": "1.4.0",
+					"System.Composition.TypedParts": "1.4.0",
+					"System.Reactive": "4.1.2",
+					"System.Runtime.Loader": "4.3.0",
+					"System.Threading.Tasks.Dataflow": "4.9.0"
+				},
+				"compile": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Orchestrator.dll": {}
+				},
+				"runtime": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Orchestrator.dll": {}
+				}
+			}
+		},
+		".NETCoreApp,Version=v2.2/osx-x64": {
+			"CommandLineParser/2.8.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/CommandLine.dll": {}
+				}
+			},
+			"coverlet.msbuild/2.5.1": {
+				"type": "package",
+				"build": {
+					"build/netstandard2.0/coverlet.msbuild.props": {},
+					"build/netstandard2.0/coverlet.msbuild.targets": {}
+				}
+			},
+			"DotNet.Glob/2.1.1": {
+				"type": "package",
+				"dependencies": {
+					"NETStandard.Library": "1.6.1"
+				},
+				"compile": {
+					"lib/netstandard1.1/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.1/DotNet.Glob.dll": {}
+				}
+			},
+			"Microsoft.AspNet.WebApi.Client/5.2.7": {
+				"type": "package",
+				"dependencies": {
+					"Newtonsoft.Json": "10.0.1",
+					"Newtonsoft.Json.Bson": "1.0.1"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Net.Http.Formatting.dll": {}
+				}
+			},
+			"Microsoft.NETCore.App/2.2.8": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.DotNetHostPolicy": "2.2.8",
+					"Microsoft.NETCore.Platforms": "2.2.4",
+					"Microsoft.NETCore.Targets": "2.0.0",
+					"NETStandard.Library": "2.0.3",
+					"runtime.osx-x64.Microsoft.NETCore.App": "2.2.8"
+				},
+				"compile": {
+					"ref/netcoreapp2.2/Microsoft.CSharp.dll": {},
+					"ref/netcoreapp2.2/Microsoft.VisualBasic.dll": {},
+					"ref/netcoreapp2.2/Microsoft.Win32.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.AppContext.dll": {},
+					"ref/netcoreapp2.2/System.Buffers.dll": {},
+					"ref/netcoreapp2.2/System.Collections.Concurrent.dll": {},
+					"ref/netcoreapp2.2/System.Collections.Immutable.dll": {},
+					"ref/netcoreapp2.2/System.Collections.NonGeneric.dll": {},
+					"ref/netcoreapp2.2/System.Collections.Specialized.dll": {},
+					"ref/netcoreapp2.2/System.Collections.dll": {},
+					"ref/netcoreapp2.2/System.ComponentModel.Annotations.dll": {},
+					"ref/netcoreapp2.2/System.ComponentModel.DataAnnotations.dll": {},
+					"ref/netcoreapp2.2/System.ComponentModel.EventBasedAsync.dll": {},
+					"ref/netcoreapp2.2/System.ComponentModel.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.ComponentModel.TypeConverter.dll": {},
+					"ref/netcoreapp2.2/System.ComponentModel.dll": {},
+					"ref/netcoreapp2.2/System.Configuration.dll": {},
+					"ref/netcoreapp2.2/System.Console.dll": {},
+					"ref/netcoreapp2.2/System.Core.dll": {},
+					"ref/netcoreapp2.2/System.Data.Common.dll": {},
+					"ref/netcoreapp2.2/System.Data.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.Contracts.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.Debug.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.DiagnosticSource.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.FileVersionInfo.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.Process.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.StackTrace.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.TextWriterTraceListener.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.Tools.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.TraceSource.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.Tracing.dll": {},
+					"ref/netcoreapp2.2/System.Drawing.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.Drawing.dll": {},
+					"ref/netcoreapp2.2/System.Dynamic.Runtime.dll": {},
+					"ref/netcoreapp2.2/System.Globalization.Calendars.dll": {},
+					"ref/netcoreapp2.2/System.Globalization.Extensions.dll": {},
+					"ref/netcoreapp2.2/System.Globalization.dll": {},
+					"ref/netcoreapp2.2/System.IO.Compression.Brotli.dll": {},
+					"ref/netcoreapp2.2/System.IO.Compression.FileSystem.dll": {},
+					"ref/netcoreapp2.2/System.IO.Compression.ZipFile.dll": {},
+					"ref/netcoreapp2.2/System.IO.Compression.dll": {},
+					"ref/netcoreapp2.2/System.IO.FileSystem.DriveInfo.dll": {},
+					"ref/netcoreapp2.2/System.IO.FileSystem.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.IO.FileSystem.Watcher.dll": {},
+					"ref/netcoreapp2.2/System.IO.FileSystem.dll": {},
+					"ref/netcoreapp2.2/System.IO.IsolatedStorage.dll": {},
+					"ref/netcoreapp2.2/System.IO.MemoryMappedFiles.dll": {},
+					"ref/netcoreapp2.2/System.IO.Pipes.dll": {},
+					"ref/netcoreapp2.2/System.IO.UnmanagedMemoryStream.dll": {},
+					"ref/netcoreapp2.2/System.IO.dll": {},
+					"ref/netcoreapp2.2/System.Linq.Expressions.dll": {},
+					"ref/netcoreapp2.2/System.Linq.Parallel.dll": {},
+					"ref/netcoreapp2.2/System.Linq.Queryable.dll": {},
+					"ref/netcoreapp2.2/System.Linq.dll": {},
+					"ref/netcoreapp2.2/System.Memory.dll": {},
+					"ref/netcoreapp2.2/System.Net.Http.dll": {},
+					"ref/netcoreapp2.2/System.Net.HttpListener.dll": {},
+					"ref/netcoreapp2.2/System.Net.Mail.dll": {},
+					"ref/netcoreapp2.2/System.Net.NameResolution.dll": {},
+					"ref/netcoreapp2.2/System.Net.NetworkInformation.dll": {},
+					"ref/netcoreapp2.2/System.Net.Ping.dll": {},
+					"ref/netcoreapp2.2/System.Net.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.Net.Requests.dll": {},
+					"ref/netcoreapp2.2/System.Net.Security.dll": {},
+					"ref/netcoreapp2.2/System.Net.ServicePoint.dll": {},
+					"ref/netcoreapp2.2/System.Net.Sockets.dll": {},
+					"ref/netcoreapp2.2/System.Net.WebClient.dll": {},
+					"ref/netcoreapp2.2/System.Net.WebHeaderCollection.dll": {},
+					"ref/netcoreapp2.2/System.Net.WebProxy.dll": {},
+					"ref/netcoreapp2.2/System.Net.WebSockets.Client.dll": {},
+					"ref/netcoreapp2.2/System.Net.WebSockets.dll": {},
+					"ref/netcoreapp2.2/System.Net.dll": {},
+					"ref/netcoreapp2.2/System.Numerics.Vectors.dll": {},
+					"ref/netcoreapp2.2/System.Numerics.dll": {},
+					"ref/netcoreapp2.2/System.ObjectModel.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.DispatchProxy.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.Emit.ILGeneration.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.Emit.Lightweight.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.Emit.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.Extensions.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.Metadata.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.TypeExtensions.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.dll": {},
+					"ref/netcoreapp2.2/System.Resources.Reader.dll": {},
+					"ref/netcoreapp2.2/System.Resources.ResourceManager.dll": {},
+					"ref/netcoreapp2.2/System.Resources.Writer.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.CompilerServices.VisualC.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Extensions.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Handles.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.InteropServices.RuntimeInformation.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.InteropServices.WindowsRuntime.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.InteropServices.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Loader.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Numerics.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Serialization.Formatters.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Serialization.Json.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Serialization.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Serialization.Xml.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Serialization.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.dll": {},
+					"ref/netcoreapp2.2/System.Security.Claims.dll": {},
+					"ref/netcoreapp2.2/System.Security.Cryptography.Algorithms.dll": {},
+					"ref/netcoreapp2.2/System.Security.Cryptography.Csp.dll": {},
+					"ref/netcoreapp2.2/System.Security.Cryptography.Encoding.dll": {},
+					"ref/netcoreapp2.2/System.Security.Cryptography.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.Security.Cryptography.X509Certificates.dll": {},
+					"ref/netcoreapp2.2/System.Security.Principal.dll": {},
+					"ref/netcoreapp2.2/System.Security.SecureString.dll": {},
+					"ref/netcoreapp2.2/System.Security.dll": {},
+					"ref/netcoreapp2.2/System.ServiceModel.Web.dll": {},
+					"ref/netcoreapp2.2/System.ServiceProcess.dll": {},
+					"ref/netcoreapp2.2/System.Text.Encoding.Extensions.dll": {},
+					"ref/netcoreapp2.2/System.Text.Encoding.dll": {},
+					"ref/netcoreapp2.2/System.Text.RegularExpressions.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Overlapped.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Tasks.Dataflow.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Tasks.Extensions.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Tasks.Parallel.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Tasks.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Thread.dll": {},
+					"ref/netcoreapp2.2/System.Threading.ThreadPool.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Timer.dll": {},
+					"ref/netcoreapp2.2/System.Threading.dll": {},
+					"ref/netcoreapp2.2/System.Transactions.Local.dll": {},
+					"ref/netcoreapp2.2/System.Transactions.dll": {},
+					"ref/netcoreapp2.2/System.ValueTuple.dll": {},
+					"ref/netcoreapp2.2/System.Web.HttpUtility.dll": {},
+					"ref/netcoreapp2.2/System.Web.dll": {},
+					"ref/netcoreapp2.2/System.Windows.dll": {},
+					"ref/netcoreapp2.2/System.Xml.Linq.dll": {},
+					"ref/netcoreapp2.2/System.Xml.ReaderWriter.dll": {},
+					"ref/netcoreapp2.2/System.Xml.Serialization.dll": {},
+					"ref/netcoreapp2.2/System.Xml.XDocument.dll": {},
+					"ref/netcoreapp2.2/System.Xml.XPath.XDocument.dll": {},
+					"ref/netcoreapp2.2/System.Xml.XPath.dll": {},
+					"ref/netcoreapp2.2/System.Xml.XmlDocument.dll": {},
+					"ref/netcoreapp2.2/System.Xml.XmlSerializer.dll": {},
+					"ref/netcoreapp2.2/System.Xml.dll": {},
+					"ref/netcoreapp2.2/System.dll": {},
+					"ref/netcoreapp2.2/WindowsBase.dll": {},
+					"ref/netcoreapp2.2/mscorlib.dll": {},
+					"ref/netcoreapp2.2/netstandard.dll": {}
+				},
+				"build": {
+					"build/netcoreapp2.2/Microsoft.NETCore.App.props": {},
+					"build/netcoreapp2.2/Microsoft.NETCore.App.targets": {}
+				}
+			},
+			"Microsoft.NETCore.DotNetAppHost/2.2.8": {
+				"type": "package",
+				"dependencies": {
+					"runtime.osx-x64.Microsoft.NETCore.DotNetAppHost": "2.2.8"
+				}
+			},
+			"Microsoft.NETCore.DotNetHostPolicy/2.2.8": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.DotNetHostResolver": "2.2.8",
+					"runtime.osx-x64.Microsoft.NETCore.DotNetHostPolicy": "2.2.8"
+				}
+			},
+			"Microsoft.NETCore.DotNetHostResolver/2.2.8": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.DotNetAppHost": "2.2.8",
+					"runtime.osx-x64.Microsoft.NETCore.DotNetHostResolver": "2.2.8"
+				}
+			},
+			"Microsoft.NETCore.Platforms/2.2.4": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.0/_._": {}
+				}
+			},
+			"Microsoft.NETCore.Targets/2.0.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.0/_._": {}
+				}
+			},
+			"Microsoft.Win32.Primitives/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"Microsoft.Win32.Registry/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"System.Collections": "4.3.0",
+					"System.Globalization": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Extensions": "4.3.0",
+					"System.Runtime.Handles": "4.3.0",
+					"System.Runtime.InteropServices": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"runtimes/unix/lib/netstandard1.3/Microsoft.Win32.Registry.dll": {}
+				}
+			},
+			"MinVer/2.5.0": {
+				"type": "package",
+				"build": {
+					"build/MinVer.targets": {}
+				},
+				"buildMultiTargeting": {
+					"buildMultiTargeting/MinVer.targets": {}
+				}
+			},
+			"NETStandard.Library/2.0.3": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0"
+				},
+				"compile": {
+					"lib/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.0/_._": {}
+				},
+				"build": {
+					"build/netstandard2.0/NETStandard.Library.targets": {}
+				}
+			},
+			"Nett/0.10.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/Nett.dll": {}
+				}
+			},
+			"Newtonsoft.Json/12.0.3": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/Newtonsoft.Json.dll": {}
+				}
+			},
+			"Newtonsoft.Json.Bson/1.0.1": {
+				"type": "package",
+				"dependencies": {
+					"NETStandard.Library": "1.6.1",
+					"Newtonsoft.Json": "10.0.1"
+				},
+				"compile": {
+					"lib/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/Newtonsoft.Json.Bson.dll": {}
+				}
+			},
+			"NuGet.Common/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"NuGet.Frameworks": "5.6.0",
+					"System.Diagnostics.Process": "4.3.0",
+					"System.Threading.Thread": "4.3.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.Common.dll": {}
+				}
+			},
+			"NuGet.Configuration/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"NuGet.Common": "5.6.0",
+					"System.Security.Cryptography.ProtectedData": "4.3.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.Configuration.dll": {}
+				}
+			},
+			"NuGet.DependencyResolver.Core/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"NuGet.LibraryModel": "5.6.0",
+					"NuGet.Protocol": "5.6.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.DependencyResolver.Core.dll": {}
+				}
+			},
+			"NuGet.Frameworks/5.6.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.Frameworks.dll": {}
+				}
+			},
+			"NuGet.LibraryModel/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"NuGet.Common": "5.6.0",
+					"NuGet.Versioning": "5.6.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.LibraryModel.dll": {}
+				}
+			},
+			"NuGet.Packaging/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"Newtonsoft.Json": "9.0.1",
+					"NuGet.Configuration": "5.6.0",
+					"NuGet.Versioning": "5.6.0",
+					"System.Dynamic.Runtime": "4.3.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.Packaging.dll": {}
+				}
+			},
+			"NuGet.ProjectModel/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"NuGet.DependencyResolver.Core": "5.6.0",
+					"System.Dynamic.Runtime": "4.3.0",
+					"System.Threading.Thread": "4.3.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.ProjectModel.dll": {}
+				}
+			},
+			"NuGet.Protocol/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"NuGet.Packaging": "5.6.0",
+					"System.Dynamic.Runtime": "4.3.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.Protocol.dll": {}
+				}
+			},
+			"NuGet.Versioning/5.6.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.Versioning.dll": {}
+				}
+			},
+			"Polly/7.0.3": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/Polly.dll": {}
+				}
+			},
+			"runtime.native.System/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0"
+				},
+				"compile": {
+					"lib/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.0/_._": {}
+				}
+			},
+			"runtime.osx-x64.Microsoft.NETCore.App/2.2.8": {
+				"type": "package",
+				"compile": {
+					"ref/netstandard/_._": {}
+				},
+				"runtime": {
+					"runtimes/osx-x64/lib/netcoreapp2.2/Microsoft.CSharp.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/Microsoft.VisualBasic.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/Microsoft.Win32.Primitives.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/Microsoft.Win32.Registry.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/SOS.NETCore.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.AppContext.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Buffers.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Collections.Concurrent.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Collections.Immutable.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Collections.NonGeneric.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Collections.Specialized.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Collections.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.ComponentModel.Annotations.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.ComponentModel.DataAnnotations.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.ComponentModel.EventBasedAsync.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.ComponentModel.Primitives.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.ComponentModel.TypeConverter.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.ComponentModel.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Configuration.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Console.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Core.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Data.Common.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Data.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Diagnostics.Contracts.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Diagnostics.Debug.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Diagnostics.DiagnosticSource.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Diagnostics.FileVersionInfo.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Diagnostics.Process.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Diagnostics.StackTrace.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Diagnostics.TextWriterTraceListener.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Diagnostics.Tools.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Diagnostics.TraceSource.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Diagnostics.Tracing.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Drawing.Primitives.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Drawing.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Dynamic.Runtime.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Globalization.Calendars.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Globalization.Extensions.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Globalization.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.Compression.Brotli.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.Compression.FileSystem.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.Compression.ZipFile.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.Compression.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.FileSystem.AccessControl.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.FileSystem.DriveInfo.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.FileSystem.Primitives.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.FileSystem.Watcher.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.FileSystem.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.IsolatedStorage.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.MemoryMappedFiles.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.Pipes.AccessControl.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.Pipes.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.UnmanagedMemoryStream.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Linq.Expressions.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Linq.Parallel.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Linq.Queryable.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Linq.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Memory.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.Http.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.HttpListener.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.Mail.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.NameResolution.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.NetworkInformation.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.Ping.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.Primitives.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.Requests.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.Security.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.ServicePoint.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.Sockets.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.WebClient.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.WebHeaderCollection.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.WebProxy.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.WebSockets.Client.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.WebSockets.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Numerics.Vectors.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Numerics.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.ObjectModel.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Private.DataContractSerialization.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Private.Uri.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Private.Xml.Linq.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Private.Xml.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Reflection.DispatchProxy.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Reflection.Emit.ILGeneration.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Reflection.Emit.Lightweight.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Reflection.Emit.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Reflection.Extensions.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Reflection.Metadata.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Reflection.Primitives.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Reflection.TypeExtensions.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Reflection.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Resources.Reader.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Resources.ResourceManager.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Resources.Writer.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.CompilerServices.VisualC.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.Extensions.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.Handles.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.InteropServices.RuntimeInformation.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.InteropServices.WindowsRuntime.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.InteropServices.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.Loader.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.Numerics.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Formatters.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Json.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Primitives.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Xml.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.Serialization.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.AccessControl.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.Claims.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.Cryptography.Algorithms.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.Cryptography.Cng.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.Cryptography.Csp.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.Cryptography.Encoding.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.Cryptography.OpenSsl.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.Cryptography.Primitives.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.Cryptography.X509Certificates.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.Principal.Windows.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.Principal.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.SecureString.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.ServiceModel.Web.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.ServiceProcess.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Text.Encoding.Extensions.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Text.Encoding.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Text.RegularExpressions.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Threading.Overlapped.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Threading.Tasks.Dataflow.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Threading.Tasks.Extensions.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Threading.Tasks.Parallel.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Threading.Tasks.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Threading.Thread.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Threading.ThreadPool.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Threading.Timer.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Threading.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Transactions.Local.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Transactions.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.ValueTuple.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Web.HttpUtility.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Web.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Windows.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Xml.Linq.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Xml.ReaderWriter.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Xml.Serialization.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Xml.XDocument.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Xml.XPath.XDocument.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Xml.XPath.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Xml.XmlDocument.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Xml.XmlSerializer.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.Xml.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/System.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/WindowsBase.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/mscorlib.dll": {},
+					"runtimes/osx-x64/lib/netcoreapp2.2/netstandard.dll": {}
+				},
+				"native": {
+					"runtimes/osx-x64/native/System.Globalization.Native.dylib": {},
+					"runtimes/osx-x64/native/System.IO.Compression.Native.a": {},
+					"runtimes/osx-x64/native/System.IO.Compression.Native.dylib": {},
+					"runtimes/osx-x64/native/System.Native.a": {},
+					"runtimes/osx-x64/native/System.Native.dylib": {},
+					"runtimes/osx-x64/native/System.Net.Http.Native.a": {},
+					"runtimes/osx-x64/native/System.Net.Http.Native.dylib": {},
+					"runtimes/osx-x64/native/System.Net.Security.Native.a": {},
+					"runtimes/osx-x64/native/System.Net.Security.Native.dylib": {},
+					"runtimes/osx-x64/native/System.Private.CoreLib.dll": {},
+					"runtimes/osx-x64/native/System.Security.Cryptography.Native.Apple.a": {},
+					"runtimes/osx-x64/native/System.Security.Cryptography.Native.Apple.dylib": {},
+					"runtimes/osx-x64/native/System.Security.Cryptography.Native.OpenSsl.a": {},
+					"runtimes/osx-x64/native/System.Security.Cryptography.Native.OpenSsl.dylib": {},
+					"runtimes/osx-x64/native/libclrjit.dylib": {},
+					"runtimes/osx-x64/native/libcoreclr.dylib": {},
+					"runtimes/osx-x64/native/libdbgshim.dylib": {},
+					"runtimes/osx-x64/native/libmscordaccore.dylib": {},
+					"runtimes/osx-x64/native/libmscordbi.dylib": {},
+					"runtimes/osx-x64/native/libsos.dylib": {},
+					"runtimes/osx-x64/native/sosdocsunix.txt": {}
+				}
+			},
+			"runtime.osx-x64.Microsoft.NETCore.DotNetAppHost/2.2.8": {
+				"type": "package",
+				"native": {
+					"runtimes/osx-x64/native/apphost": {}
+				}
+			},
+			"runtime.osx-x64.Microsoft.NETCore.DotNetHostPolicy/2.2.8": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.DotNetHostResolver": "2.2.8"
+				},
+				"native": {
+					"runtimes/osx-x64/native/libhostpolicy.dylib": {}
+				}
+			},
+			"runtime.osx-x64.Microsoft.NETCore.DotNetHostResolver/2.2.8": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.DotNetAppHost": "2.2.8"
+				},
+				"native": {
+					"runtimes/osx-x64/native/libhostfxr.dylib": {}
+				}
+			},
+			"SemanticVersioning/1.2.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/SemVer.dll": {}
+				}
+			},
+			"StyleCop.Analyzers/1.0.2": {
+				"type": "package"
+			},
+			"System.Collections/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.Composition.AttributedModel/1.4.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Composition.AttributedModel.dll": {}
+				}
+			},
+			"System.Composition.Convention/1.4.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Composition.AttributedModel": "1.4.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Composition.Convention.dll": {}
+				}
+			},
+			"System.Composition.Hosting/1.4.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Composition.Runtime": "1.4.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Composition.Hosting.dll": {}
+				}
+			},
+			"System.Composition.Runtime/1.4.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Composition.Runtime.dll": {}
+				}
+			},
+			"System.Composition.TypedParts/1.4.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Composition.AttributedModel": "1.4.0",
+					"System.Composition.Hosting": "1.4.0",
+					"System.Composition.Runtime": "1.4.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Composition.TypedParts.dll": {}
+				}
+			},
+			"System.Diagnostics.Debug/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.Diagnostics.Process/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.Win32.Primitives": "4.3.0",
+					"Microsoft.Win32.Registry": "4.3.0",
+					"System.Collections": "4.3.0",
+					"System.Diagnostics.Debug": "4.3.0",
+					"System.Globalization": "4.3.0",
+					"System.IO": "4.3.0",
+					"System.IO.FileSystem": "4.3.0",
+					"System.IO.FileSystem.Primitives": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Extensions": "4.3.0",
+					"System.Runtime.Handles": "4.3.0",
+					"System.Runtime.InteropServices": "4.3.0",
+					"System.Text.Encoding": "4.3.0",
+					"System.Text.Encoding.Extensions": "4.3.0",
+					"System.Threading": "4.3.0",
+					"System.Threading.Tasks": "4.3.0",
+					"System.Threading.Thread": "4.3.0",
+					"System.Threading.ThreadPool": "4.3.0",
+					"runtime.native.System": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.4/_._": {}
+				},
+				"runtime": {
+					"runtimes/osx/lib/netstandard1.4/System.Diagnostics.Process.dll": {}
+				}
+			},
+			"System.Dynamic.Runtime/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Collections": "4.3.0",
+					"System.Diagnostics.Debug": "4.3.0",
+					"System.Linq": "4.3.0",
+					"System.Linq.Expressions": "4.3.0",
+					"System.ObjectModel": "4.3.0",
+					"System.Reflection": "4.3.0",
+					"System.Reflection.Emit": "4.3.0",
+					"System.Reflection.Emit.ILGeneration": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Reflection.TypeExtensions": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Extensions": "4.3.0",
+					"System.Threading": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Dynamic.Runtime.dll": {}
+				}
+			},
+			"System.Globalization/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.IO/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0",
+					"System.Text.Encoding": "4.3.0",
+					"System.Threading.Tasks": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.5/_._": {}
+				}
+			},
+			"System.IO.FileSystem/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.IO": "4.3.0",
+					"System.IO.FileSystem.Primitives": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Handles": "4.3.0",
+					"System.Text.Encoding": "4.3.0",
+					"System.Threading.Tasks": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.IO.FileSystem.Primitives/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.IO.FileSystem.Primitives.dll": {}
+				}
+			},
+			"System.Linq/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Collections": "4.3.0",
+					"System.Diagnostics.Debug": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Extensions": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.6/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.6/System.Linq.dll": {}
+				}
+			},
+			"System.Linq.Expressions/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Collections": "4.3.0",
+					"System.Diagnostics.Debug": "4.3.0",
+					"System.Globalization": "4.3.0",
+					"System.IO": "4.3.0",
+					"System.Linq": "4.3.0",
+					"System.ObjectModel": "4.3.0",
+					"System.Reflection": "4.3.0",
+					"System.Reflection.Emit": "4.3.0",
+					"System.Reflection.Emit.ILGeneration": "4.3.0",
+					"System.Reflection.Emit.Lightweight": "4.3.0",
+					"System.Reflection.Extensions": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Reflection.TypeExtensions": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Extensions": "4.3.0",
+					"System.Threading": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.6/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.6/System.Linq.Expressions.dll": {}
+				}
+			},
+			"System.ObjectModel/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Collections": "4.3.0",
+					"System.Diagnostics.Debug": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Threading": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.ObjectModel.dll": {}
+				}
+			},
+			"System.Reactive/4.1.2": {
+				"type": "package",
+				"dependencies": {
+					"System.Runtime.InteropServices.WindowsRuntime": "4.3.0",
+					"System.Threading.Tasks.Extensions": "4.5.1"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Reactive.dll": {}
+				}
+			},
+			"System.Reflection/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.IO": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.5/_._": {}
+				}
+			},
+			"System.Reflection.Emit/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.IO": "4.3.0",
+					"System.Reflection": "4.3.0",
+					"System.Reflection.Emit.ILGeneration": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.1/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Reflection.Emit.dll": {}
+				}
+			},
+			"System.Reflection.Emit.ILGeneration/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Reflection": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Reflection.Emit.ILGeneration.dll": {}
+				}
+			},
+			"System.Reflection.Emit.Lightweight/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Reflection": "4.3.0",
+					"System.Reflection.Emit.ILGeneration": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Reflection.Emit.Lightweight.dll": {}
+				}
+			},
+			"System.Reflection.Extensions/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Reflection": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.0/_._": {}
+				}
+			},
+			"System.Reflection.Primitives/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.0/_._": {}
+				}
+			},
+			"System.Reflection.TypeExtensions/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Reflection": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.5/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.5/System.Reflection.TypeExtensions.dll": {}
+				}
+			},
+			"System.Resources.ResourceManager/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Globalization": "4.3.0",
+					"System.Reflection": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.0/_._": {}
+				}
+			},
+			"System.Runtime/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0"
+				},
+				"compile": {
+					"ref/netstandard1.5/_._": {}
+				}
+			},
+			"System.Runtime.Extensions/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.5/_._": {}
+				}
+			},
+			"System.Runtime.Handles/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.Runtime.InteropServices/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Reflection": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Handles": "4.3.0"
+				},
+				"compile": {
+					"ref/netcoreapp1.1/_._": {}
+				}
+			},
+			"System.Runtime.InteropServices.WindowsRuntime/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Runtime.InteropServices.WindowsRuntime.dll": {}
+				}
+			},
+			"System.Runtime.Loader/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.IO": "4.3.0",
+					"System.Reflection": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.5/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.5/System.Runtime.Loader.dll": {}
+				}
+			},
+			"System.Security.Cryptography.Primitives/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Diagnostics.Debug": "4.3.0",
+					"System.Globalization": "4.3.0",
+					"System.IO": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Threading": "4.3.0",
+					"System.Threading.Tasks": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Security.Cryptography.Primitives.dll": {}
+				}
+			},
+			"System.Security.Cryptography.ProtectedData/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.InteropServices": "4.3.0",
+					"System.Security.Cryptography.Primitives": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"runtimes/unix/lib/netstandard1.3/System.Security.Cryptography.ProtectedData.dll": {}
+				}
+			},
+			"System.Text.Encoding/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.Text.Encoding.Extensions/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0",
+					"System.Text.Encoding": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.Threading/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Runtime": "4.3.0",
+					"System.Threading.Tasks": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Threading.dll": {}
+				}
+			},
+			"System.Threading.Tasks/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.Threading.Tasks.Dataflow/4.9.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Threading.Tasks.Dataflow.dll": {}
+				}
+			},
+			"System.Threading.Tasks.Extensions/4.5.1": {
+				"type": "package",
+				"compile": {
+					"ref/netcoreapp2.1/_._": {}
+				},
+				"runtime": {
+					"lib/netcoreapp2.1/_._": {}
+				}
+			},
+			"System.Threading.Thread/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Threading.Thread.dll": {}
+				}
+			},
+			"System.Threading.ThreadPool/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Handles": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Threading.ThreadPool.dll": {}
+				}
+			},
+			"YamlDotNet/5.3.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/YamlDotNet.dll": {}
+				}
+			},
+			"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common/1.0.0": {
+				"type": "project",
+				"framework": ".NETCoreApp,Version=v2.2",
+				"dependencies": {
+					"Microsoft.AspNet.WebApi.Client": "5.2.7",
+					"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts": "1.0.0",
+					"Newtonsoft.Json": "12.0.3",
+					"System.Composition.AttributedModel": "1.4.0",
+					"System.Composition.Convention": "1.4.0",
+					"System.Composition.Hosting": "1.4.0",
+					"System.Composition.Runtime": "1.4.0",
+					"System.Composition.TypedParts": "1.4.0",
+					"System.Reactive": "4.1.2"
+				},
+				"compile": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common.dll": {}
+				},
+				"runtime": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common.dll": {}
+				}
+			},
+			"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts/1.0.0": {
+				"type": "project",
+				"framework": ".NETCoreApp,Version=v2.2",
+				"dependencies": {
+					"Newtonsoft.Json": "12.0.3",
+					"System.Composition.AttributedModel": "1.4.0",
+					"System.Reactive": "4.1.2"
+				},
+				"compile": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts.dll": {}
+				},
+				"runtime": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts.dll": {}
+				}
+			},
+			"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Detectors/1.0.0": {
+				"type": "project",
+				"framework": ".NETCoreApp,Version=v2.2",
+				"dependencies": {
+					"DotNet.Glob": "2.1.1",
+					"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common": "1.0.0",
+					"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts": "1.0.0",
+					"Nett": "0.10.0",
+					"Newtonsoft.Json": "12.0.3",
+					"NuGet.ProjectModel": "5.6.0",
+					"NuGet.Versioning": "5.6.0",
+					"Polly": "7.0.3",
+					"SemanticVersioning": "1.2.0",
+					"System.Composition.AttributedModel": "1.4.0",
+					"System.Composition.Convention": "1.4.0",
+					"System.Composition.Hosting": "1.4.0",
+					"System.Composition.Runtime": "1.4.0",
+					"System.Composition.TypedParts": "1.4.0",
+					"System.Reactive": "4.1.2",
+					"System.Threading.Tasks.Dataflow": "4.9.0",
+					"yamldotnet": "5.3.0"
+				},
+				"compile": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Detectors.dll": {}
+				},
+				"runtime": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Detectors.dll": {}
+				}
+			},
+			"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Orchestrator/1.0.0": {
+				"type": "project",
+				"framework": ".NETCoreApp,Version=v2.2",
+				"dependencies": {
+					"CommandLineParser": "2.8.0",
+					"DotNet.Glob": "2.1.1",
+					"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common": "1.0.0",
+					"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts": "1.0.0",
+					"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Detectors": "1.0.0",
+					"Newtonsoft.Json": "12.0.3",
+					"Polly": "7.0.3",
+					"System.Composition.AttributedModel": "1.4.0",
+					"System.Composition.Convention": "1.4.0",
+					"System.Composition.Hosting": "1.4.0",
+					"System.Composition.Runtime": "1.4.0",
+					"System.Composition.TypedParts": "1.4.0",
+					"System.Reactive": "4.1.2",
+					"System.Runtime.Loader": "4.3.0",
+					"System.Threading.Tasks.Dataflow": "4.9.0"
+				},
+				"compile": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Orchestrator.dll": {}
+				},
+				"runtime": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Orchestrator.dll": {}
+				}
+			}
+		},
+		".NETCoreApp,Version=v2.2/win-x64": {
+			"CommandLineParser/2.8.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/CommandLine.dll": {}
+				}
+			},
+			"coverlet.msbuild/2.5.1": {
+				"type": "package",
+				"build": {
+					"build/netstandard2.0/coverlet.msbuild.props": {},
+					"build/netstandard2.0/coverlet.msbuild.targets": {}
+				}
+			},
+			"DotNet.Glob/2.1.1": {
+				"type": "package",
+				"dependencies": {
+					"NETStandard.Library": "1.6.1"
+				},
+				"compile": {
+					"lib/netstandard1.1/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.1/DotNet.Glob.dll": {}
+				}
+			},
+			"Microsoft.AspNet.WebApi.Client/5.2.7": {
+				"type": "package",
+				"dependencies": {
+					"Newtonsoft.Json": "10.0.1",
+					"Newtonsoft.Json.Bson": "1.0.1"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Net.Http.Formatting.dll": {}
+				}
+			},
+			"Microsoft.NETCore.App/2.2.8": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.DotNetHostPolicy": "2.2.8",
+					"Microsoft.NETCore.Platforms": "2.2.4",
+					"Microsoft.NETCore.Targets": "2.0.0",
+					"NETStandard.Library": "2.0.3",
+					"runtime.win-x64.Microsoft.NETCore.App": "2.2.8"
+				},
+				"compile": {
+					"ref/netcoreapp2.2/Microsoft.CSharp.dll": {},
+					"ref/netcoreapp2.2/Microsoft.VisualBasic.dll": {},
+					"ref/netcoreapp2.2/Microsoft.Win32.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.AppContext.dll": {},
+					"ref/netcoreapp2.2/System.Buffers.dll": {},
+					"ref/netcoreapp2.2/System.Collections.Concurrent.dll": {},
+					"ref/netcoreapp2.2/System.Collections.Immutable.dll": {},
+					"ref/netcoreapp2.2/System.Collections.NonGeneric.dll": {},
+					"ref/netcoreapp2.2/System.Collections.Specialized.dll": {},
+					"ref/netcoreapp2.2/System.Collections.dll": {},
+					"ref/netcoreapp2.2/System.ComponentModel.Annotations.dll": {},
+					"ref/netcoreapp2.2/System.ComponentModel.DataAnnotations.dll": {},
+					"ref/netcoreapp2.2/System.ComponentModel.EventBasedAsync.dll": {},
+					"ref/netcoreapp2.2/System.ComponentModel.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.ComponentModel.TypeConverter.dll": {},
+					"ref/netcoreapp2.2/System.ComponentModel.dll": {},
+					"ref/netcoreapp2.2/System.Configuration.dll": {},
+					"ref/netcoreapp2.2/System.Console.dll": {},
+					"ref/netcoreapp2.2/System.Core.dll": {},
+					"ref/netcoreapp2.2/System.Data.Common.dll": {},
+					"ref/netcoreapp2.2/System.Data.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.Contracts.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.Debug.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.DiagnosticSource.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.FileVersionInfo.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.Process.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.StackTrace.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.TextWriterTraceListener.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.Tools.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.TraceSource.dll": {},
+					"ref/netcoreapp2.2/System.Diagnostics.Tracing.dll": {},
+					"ref/netcoreapp2.2/System.Drawing.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.Drawing.dll": {},
+					"ref/netcoreapp2.2/System.Dynamic.Runtime.dll": {},
+					"ref/netcoreapp2.2/System.Globalization.Calendars.dll": {},
+					"ref/netcoreapp2.2/System.Globalization.Extensions.dll": {},
+					"ref/netcoreapp2.2/System.Globalization.dll": {},
+					"ref/netcoreapp2.2/System.IO.Compression.Brotli.dll": {},
+					"ref/netcoreapp2.2/System.IO.Compression.FileSystem.dll": {},
+					"ref/netcoreapp2.2/System.IO.Compression.ZipFile.dll": {},
+					"ref/netcoreapp2.2/System.IO.Compression.dll": {},
+					"ref/netcoreapp2.2/System.IO.FileSystem.DriveInfo.dll": {},
+					"ref/netcoreapp2.2/System.IO.FileSystem.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.IO.FileSystem.Watcher.dll": {},
+					"ref/netcoreapp2.2/System.IO.FileSystem.dll": {},
+					"ref/netcoreapp2.2/System.IO.IsolatedStorage.dll": {},
+					"ref/netcoreapp2.2/System.IO.MemoryMappedFiles.dll": {},
+					"ref/netcoreapp2.2/System.IO.Pipes.dll": {},
+					"ref/netcoreapp2.2/System.IO.UnmanagedMemoryStream.dll": {},
+					"ref/netcoreapp2.2/System.IO.dll": {},
+					"ref/netcoreapp2.2/System.Linq.Expressions.dll": {},
+					"ref/netcoreapp2.2/System.Linq.Parallel.dll": {},
+					"ref/netcoreapp2.2/System.Linq.Queryable.dll": {},
+					"ref/netcoreapp2.2/System.Linq.dll": {},
+					"ref/netcoreapp2.2/System.Memory.dll": {},
+					"ref/netcoreapp2.2/System.Net.Http.dll": {},
+					"ref/netcoreapp2.2/System.Net.HttpListener.dll": {},
+					"ref/netcoreapp2.2/System.Net.Mail.dll": {},
+					"ref/netcoreapp2.2/System.Net.NameResolution.dll": {},
+					"ref/netcoreapp2.2/System.Net.NetworkInformation.dll": {},
+					"ref/netcoreapp2.2/System.Net.Ping.dll": {},
+					"ref/netcoreapp2.2/System.Net.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.Net.Requests.dll": {},
+					"ref/netcoreapp2.2/System.Net.Security.dll": {},
+					"ref/netcoreapp2.2/System.Net.ServicePoint.dll": {},
+					"ref/netcoreapp2.2/System.Net.Sockets.dll": {},
+					"ref/netcoreapp2.2/System.Net.WebClient.dll": {},
+					"ref/netcoreapp2.2/System.Net.WebHeaderCollection.dll": {},
+					"ref/netcoreapp2.2/System.Net.WebProxy.dll": {},
+					"ref/netcoreapp2.2/System.Net.WebSockets.Client.dll": {},
+					"ref/netcoreapp2.2/System.Net.WebSockets.dll": {},
+					"ref/netcoreapp2.2/System.Net.dll": {},
+					"ref/netcoreapp2.2/System.Numerics.Vectors.dll": {},
+					"ref/netcoreapp2.2/System.Numerics.dll": {},
+					"ref/netcoreapp2.2/System.ObjectModel.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.DispatchProxy.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.Emit.ILGeneration.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.Emit.Lightweight.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.Emit.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.Extensions.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.Metadata.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.TypeExtensions.dll": {},
+					"ref/netcoreapp2.2/System.Reflection.dll": {},
+					"ref/netcoreapp2.2/System.Resources.Reader.dll": {},
+					"ref/netcoreapp2.2/System.Resources.ResourceManager.dll": {},
+					"ref/netcoreapp2.2/System.Resources.Writer.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.CompilerServices.VisualC.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Extensions.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Handles.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.InteropServices.RuntimeInformation.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.InteropServices.WindowsRuntime.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.InteropServices.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Loader.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Numerics.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Serialization.Formatters.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Serialization.Json.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Serialization.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Serialization.Xml.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.Serialization.dll": {},
+					"ref/netcoreapp2.2/System.Runtime.dll": {},
+					"ref/netcoreapp2.2/System.Security.Claims.dll": {},
+					"ref/netcoreapp2.2/System.Security.Cryptography.Algorithms.dll": {},
+					"ref/netcoreapp2.2/System.Security.Cryptography.Csp.dll": {},
+					"ref/netcoreapp2.2/System.Security.Cryptography.Encoding.dll": {},
+					"ref/netcoreapp2.2/System.Security.Cryptography.Primitives.dll": {},
+					"ref/netcoreapp2.2/System.Security.Cryptography.X509Certificates.dll": {},
+					"ref/netcoreapp2.2/System.Security.Principal.dll": {},
+					"ref/netcoreapp2.2/System.Security.SecureString.dll": {},
+					"ref/netcoreapp2.2/System.Security.dll": {},
+					"ref/netcoreapp2.2/System.ServiceModel.Web.dll": {},
+					"ref/netcoreapp2.2/System.ServiceProcess.dll": {},
+					"ref/netcoreapp2.2/System.Text.Encoding.Extensions.dll": {},
+					"ref/netcoreapp2.2/System.Text.Encoding.dll": {},
+					"ref/netcoreapp2.2/System.Text.RegularExpressions.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Overlapped.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Tasks.Dataflow.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Tasks.Extensions.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Tasks.Parallel.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Tasks.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Thread.dll": {},
+					"ref/netcoreapp2.2/System.Threading.ThreadPool.dll": {},
+					"ref/netcoreapp2.2/System.Threading.Timer.dll": {},
+					"ref/netcoreapp2.2/System.Threading.dll": {},
+					"ref/netcoreapp2.2/System.Transactions.Local.dll": {},
+					"ref/netcoreapp2.2/System.Transactions.dll": {},
+					"ref/netcoreapp2.2/System.ValueTuple.dll": {},
+					"ref/netcoreapp2.2/System.Web.HttpUtility.dll": {},
+					"ref/netcoreapp2.2/System.Web.dll": {},
+					"ref/netcoreapp2.2/System.Windows.dll": {},
+					"ref/netcoreapp2.2/System.Xml.Linq.dll": {},
+					"ref/netcoreapp2.2/System.Xml.ReaderWriter.dll": {},
+					"ref/netcoreapp2.2/System.Xml.Serialization.dll": {},
+					"ref/netcoreapp2.2/System.Xml.XDocument.dll": {},
+					"ref/netcoreapp2.2/System.Xml.XPath.XDocument.dll": {},
+					"ref/netcoreapp2.2/System.Xml.XPath.dll": {},
+					"ref/netcoreapp2.2/System.Xml.XmlDocument.dll": {},
+					"ref/netcoreapp2.2/System.Xml.XmlSerializer.dll": {},
+					"ref/netcoreapp2.2/System.Xml.dll": {},
+					"ref/netcoreapp2.2/System.dll": {},
+					"ref/netcoreapp2.2/WindowsBase.dll": {},
+					"ref/netcoreapp2.2/mscorlib.dll": {},
+					"ref/netcoreapp2.2/netstandard.dll": {}
+				},
+				"build": {
+					"build/netcoreapp2.2/Microsoft.NETCore.App.props": {},
+					"build/netcoreapp2.2/Microsoft.NETCore.App.targets": {}
+				}
+			},
+			"Microsoft.NETCore.DotNetAppHost/2.2.8": {
+				"type": "package",
+				"dependencies": {
+					"runtime.win-x64.Microsoft.NETCore.DotNetAppHost": "2.2.8"
+				}
+			},
+			"Microsoft.NETCore.DotNetHostPolicy/2.2.8": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.DotNetHostResolver": "2.2.8",
+					"runtime.win-x64.Microsoft.NETCore.DotNetHostPolicy": "2.2.8"
+				}
+			},
+			"Microsoft.NETCore.DotNetHostResolver/2.2.8": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.DotNetAppHost": "2.2.8",
+					"runtime.win-x64.Microsoft.NETCore.DotNetHostResolver": "2.2.8"
+				}
+			},
+			"Microsoft.NETCore.Platforms/2.2.4": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.0/_._": {}
+				}
+			},
+			"Microsoft.NETCore.Targets/2.0.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.0/_._": {}
+				}
+			},
+			"Microsoft.Win32.Primitives/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"Microsoft.Win32.Registry/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"System.Collections": "4.3.0",
+					"System.Globalization": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Extensions": "4.3.0",
+					"System.Runtime.Handles": "4.3.0",
+					"System.Runtime.InteropServices": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"runtimes/win/lib/netstandard1.3/Microsoft.Win32.Registry.dll": {}
+				}
+			},
+			"MinVer/2.5.0": {
+				"type": "package",
+				"build": {
+					"build/MinVer.targets": {}
+				},
+				"buildMultiTargeting": {
+					"buildMultiTargeting/MinVer.targets": {}
+				}
+			},
+			"NETStandard.Library/2.0.3": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0"
+				},
+				"compile": {
+					"lib/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.0/_._": {}
+				},
+				"build": {
+					"build/netstandard2.0/NETStandard.Library.targets": {}
+				}
+			},
+			"Nett/0.10.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/Nett.dll": {}
+				}
+			},
+			"Newtonsoft.Json/12.0.3": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/Newtonsoft.Json.dll": {}
+				}
+			},
+			"Newtonsoft.Json.Bson/1.0.1": {
+				"type": "package",
+				"dependencies": {
+					"NETStandard.Library": "1.6.1",
+					"Newtonsoft.Json": "10.0.1"
+				},
+				"compile": {
+					"lib/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/Newtonsoft.Json.Bson.dll": {}
+				}
+			},
+			"NuGet.Common/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"NuGet.Frameworks": "5.6.0",
+					"System.Diagnostics.Process": "4.3.0",
+					"System.Threading.Thread": "4.3.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.Common.dll": {}
+				}
+			},
+			"NuGet.Configuration/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"NuGet.Common": "5.6.0",
+					"System.Security.Cryptography.ProtectedData": "4.3.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.Configuration.dll": {}
+				}
+			},
+			"NuGet.DependencyResolver.Core/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"NuGet.LibraryModel": "5.6.0",
+					"NuGet.Protocol": "5.6.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.DependencyResolver.Core.dll": {}
+				}
+			},
+			"NuGet.Frameworks/5.6.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.Frameworks.dll": {}
+				}
+			},
+			"NuGet.LibraryModel/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"NuGet.Common": "5.6.0",
+					"NuGet.Versioning": "5.6.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.LibraryModel.dll": {}
+				}
+			},
+			"NuGet.Packaging/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"Newtonsoft.Json": "9.0.1",
+					"NuGet.Configuration": "5.6.0",
+					"NuGet.Versioning": "5.6.0",
+					"System.Dynamic.Runtime": "4.3.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.Packaging.dll": {}
+				}
+			},
+			"NuGet.ProjectModel/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"NuGet.DependencyResolver.Core": "5.6.0",
+					"System.Dynamic.Runtime": "4.3.0",
+					"System.Threading.Thread": "4.3.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.ProjectModel.dll": {}
+				}
+			},
+			"NuGet.Protocol/5.6.0": {
+				"type": "package",
+				"dependencies": {
+					"NuGet.Packaging": "5.6.0",
+					"System.Dynamic.Runtime": "4.3.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.Protocol.dll": {}
+				}
+			},
+			"NuGet.Versioning/5.6.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/NuGet.Versioning.dll": {}
+				}
+			},
+			"Polly/7.0.3": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/Polly.dll": {}
+				}
+			},
+			"runtime.native.System/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0"
+				},
+				"compile": {
+					"lib/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.0/_._": {}
+				}
+			},
+			"runtime.win-x64.Microsoft.NETCore.App/2.2.8": {
+				"type": "package",
+				"compile": {
+					"ref/netstandard/_._": {}
+				},
+				"runtime": {
+					"runtimes/win-x64/lib/netcoreapp2.2/Microsoft.CSharp.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/Microsoft.VisualBasic.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/Microsoft.Win32.Primitives.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/Microsoft.Win32.Registry.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/SOS.NETCore.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.AppContext.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Buffers.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Collections.Concurrent.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Collections.Immutable.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Collections.NonGeneric.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Collections.Specialized.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Collections.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.ComponentModel.Annotations.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.ComponentModel.DataAnnotations.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.ComponentModel.EventBasedAsync.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.ComponentModel.Primitives.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.ComponentModel.TypeConverter.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.ComponentModel.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Configuration.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Console.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Core.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Data.Common.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Data.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Diagnostics.Contracts.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Diagnostics.Debug.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Diagnostics.DiagnosticSource.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Diagnostics.FileVersionInfo.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Diagnostics.Process.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Diagnostics.StackTrace.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Diagnostics.TextWriterTraceListener.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Diagnostics.Tools.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Diagnostics.TraceSource.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Diagnostics.Tracing.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Drawing.Primitives.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Drawing.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Dynamic.Runtime.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Globalization.Calendars.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Globalization.Extensions.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Globalization.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.IO.Compression.Brotli.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.IO.Compression.FileSystem.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.IO.Compression.ZipFile.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.IO.Compression.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.IO.FileSystem.AccessControl.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.IO.FileSystem.DriveInfo.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.IO.FileSystem.Primitives.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.IO.FileSystem.Watcher.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.IO.FileSystem.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.IO.IsolatedStorage.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.IO.MemoryMappedFiles.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.IO.Pipes.AccessControl.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.IO.Pipes.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.IO.UnmanagedMemoryStream.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.IO.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Linq.Expressions.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Linq.Parallel.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Linq.Queryable.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Linq.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Memory.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Net.Http.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Net.HttpListener.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Net.Mail.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Net.NameResolution.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Net.NetworkInformation.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Net.Ping.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Net.Primitives.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Net.Requests.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Net.Security.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Net.ServicePoint.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Net.Sockets.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Net.WebClient.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Net.WebHeaderCollection.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Net.WebProxy.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Net.WebSockets.Client.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Net.WebSockets.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Net.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Numerics.Vectors.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Numerics.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.ObjectModel.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Private.DataContractSerialization.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Private.Uri.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Private.Xml.Linq.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Private.Xml.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Reflection.DispatchProxy.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Reflection.Emit.ILGeneration.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Reflection.Emit.Lightweight.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Reflection.Emit.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Reflection.Extensions.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Reflection.Metadata.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Reflection.Primitives.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Reflection.TypeExtensions.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Reflection.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Resources.Reader.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Resources.ResourceManager.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Resources.Writer.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.CompilerServices.VisualC.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.Extensions.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.Handles.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.InteropServices.RuntimeInformation.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.InteropServices.WindowsRuntime.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.InteropServices.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.Loader.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.Numerics.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Formatters.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Json.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Primitives.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Xml.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.Serialization.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Security.AccessControl.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Security.Claims.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Security.Cryptography.Algorithms.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Security.Cryptography.Cng.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Security.Cryptography.Csp.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Security.Cryptography.Encoding.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Security.Cryptography.OpenSsl.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Security.Cryptography.Primitives.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Security.Cryptography.X509Certificates.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Security.Principal.Windows.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Security.Principal.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Security.SecureString.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Security.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.ServiceModel.Web.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.ServiceProcess.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Text.Encoding.Extensions.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Text.Encoding.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Text.RegularExpressions.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Threading.Overlapped.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Threading.Tasks.Dataflow.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Threading.Tasks.Extensions.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Threading.Tasks.Parallel.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Threading.Tasks.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Threading.Thread.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Threading.ThreadPool.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Threading.Timer.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Threading.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Transactions.Local.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Transactions.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.ValueTuple.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Web.HttpUtility.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Web.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Windows.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Xml.Linq.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Xml.ReaderWriter.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Xml.Serialization.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Xml.XDocument.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Xml.XPath.XDocument.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Xml.XPath.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Xml.XmlDocument.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Xml.XmlSerializer.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.Xml.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/System.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/WindowsBase.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/mscorlib.dll": {},
+					"runtimes/win-x64/lib/netcoreapp2.2/netstandard.dll": {}
+				},
+				"native": {
+					"runtimes/win-x64/native/Microsoft.DiaSymReader.Native.amd64.dll": {},
+					"runtimes/win-x64/native/System.Private.CoreLib.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-console-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-datetime-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-debug-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-errorhandling-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-file-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-file-l1-2-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-file-l2-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-handle-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-heap-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-interlocked-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-libraryloader-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-localization-l1-2-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-memory-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-namedpipe-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-processenvironment-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-processthreads-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-processthreads-l1-1-1.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-profile-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-rtlsupport-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-string-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-synch-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-synch-l1-2-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-sysinfo-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-timezone-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-core-util-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-crt-conio-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-crt-convert-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-crt-environment-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-crt-filesystem-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-crt-heap-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-crt-locale-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-crt-math-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-crt-multibyte-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-crt-private-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-crt-process-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-crt-runtime-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-crt-stdio-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-crt-string-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-crt-time-l1-1-0.dll": {},
+					"runtimes/win-x64/native/api-ms-win-crt-utility-l1-1-0.dll": {},
+					"runtimes/win-x64/native/clrcompression.dll": {},
+					"runtimes/win-x64/native/clretwrc.dll": {},
+					"runtimes/win-x64/native/clrjit.dll": {},
+					"runtimes/win-x64/native/coreclr.dll": {},
+					"runtimes/win-x64/native/dbgshim.dll": {},
+					"runtimes/win-x64/native/mscordaccore.dll": {},
+					"runtimes/win-x64/native/mscordaccore_amd64_amd64_4.6.28207.03.dll": {},
+					"runtimes/win-x64/native/mscordbi.dll": {},
+					"runtimes/win-x64/native/mscorrc.debug.dll": {},
+					"runtimes/win-x64/native/mscorrc.dll": {},
+					"runtimes/win-x64/native/sos.dll": {},
+					"runtimes/win-x64/native/sos_amd64_amd64_4.6.28207.03.dll": {},
+					"runtimes/win-x64/native/ucrtbase.dll": {}
+				}
+			},
+			"runtime.win-x64.Microsoft.NETCore.DotNetAppHost/2.2.8": {
+				"type": "package",
+				"native": {
+					"runtimes/win-x64/native/apphost.exe": {}
+				}
+			},
+			"runtime.win-x64.Microsoft.NETCore.DotNetHostPolicy/2.2.8": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.DotNetHostResolver": "2.2.8"
+				},
+				"native": {
+					"runtimes/win-x64/native/hostpolicy.dll": {}
+				}
+			},
+			"runtime.win-x64.Microsoft.NETCore.DotNetHostResolver/2.2.8": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.DotNetAppHost": "2.2.8"
+				},
+				"native": {
+					"runtimes/win-x64/native/hostfxr.dll": {}
+				}
+			},
+			"SemanticVersioning/1.2.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/SemVer.dll": {}
+				}
+			},
+			"StyleCop.Analyzers/1.0.2": {
+				"type": "package"
+			},
+			"System.Collections/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.Composition.AttributedModel/1.4.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Composition.AttributedModel.dll": {}
+				}
+			},
+			"System.Composition.Convention/1.4.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Composition.AttributedModel": "1.4.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Composition.Convention.dll": {}
+				}
+			},
+			"System.Composition.Hosting/1.4.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Composition.Runtime": "1.4.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Composition.Hosting.dll": {}
+				}
+			},
+			"System.Composition.Runtime/1.4.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Composition.Runtime.dll": {}
+				}
+			},
+			"System.Composition.TypedParts/1.4.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Composition.AttributedModel": "1.4.0",
+					"System.Composition.Hosting": "1.4.0",
+					"System.Composition.Runtime": "1.4.0"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Composition.TypedParts.dll": {}
+				}
+			},
+			"System.Diagnostics.Debug/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.Diagnostics.Process/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.Win32.Primitives": "4.3.0",
+					"Microsoft.Win32.Registry": "4.3.0",
+					"System.Collections": "4.3.0",
+					"System.Diagnostics.Debug": "4.3.0",
+					"System.Globalization": "4.3.0",
+					"System.IO": "4.3.0",
+					"System.IO.FileSystem": "4.3.0",
+					"System.IO.FileSystem.Primitives": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Extensions": "4.3.0",
+					"System.Runtime.Handles": "4.3.0",
+					"System.Runtime.InteropServices": "4.3.0",
+					"System.Text.Encoding": "4.3.0",
+					"System.Text.Encoding.Extensions": "4.3.0",
+					"System.Threading": "4.3.0",
+					"System.Threading.Tasks": "4.3.0",
+					"System.Threading.Thread": "4.3.0",
+					"System.Threading.ThreadPool": "4.3.0",
+					"runtime.native.System": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.4/_._": {}
+				},
+				"runtime": {
+					"runtimes/win/lib/netstandard1.4/System.Diagnostics.Process.dll": {}
+				}
+			},
+			"System.Dynamic.Runtime/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Collections": "4.3.0",
+					"System.Diagnostics.Debug": "4.3.0",
+					"System.Linq": "4.3.0",
+					"System.Linq.Expressions": "4.3.0",
+					"System.ObjectModel": "4.3.0",
+					"System.Reflection": "4.3.0",
+					"System.Reflection.Emit": "4.3.0",
+					"System.Reflection.Emit.ILGeneration": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Reflection.TypeExtensions": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Extensions": "4.3.0",
+					"System.Threading": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Dynamic.Runtime.dll": {}
+				}
+			},
+			"System.Globalization/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.IO/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0",
+					"System.Text.Encoding": "4.3.0",
+					"System.Threading.Tasks": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.5/_._": {}
+				}
+			},
+			"System.IO.FileSystem/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.IO": "4.3.0",
+					"System.IO.FileSystem.Primitives": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Handles": "4.3.0",
+					"System.Text.Encoding": "4.3.0",
+					"System.Threading.Tasks": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.IO.FileSystem.Primitives/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.IO.FileSystem.Primitives.dll": {}
+				}
+			},
+			"System.Linq/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Collections": "4.3.0",
+					"System.Diagnostics.Debug": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Extensions": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.6/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.6/System.Linq.dll": {}
+				}
+			},
+			"System.Linq.Expressions/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Collections": "4.3.0",
+					"System.Diagnostics.Debug": "4.3.0",
+					"System.Globalization": "4.3.0",
+					"System.IO": "4.3.0",
+					"System.Linq": "4.3.0",
+					"System.ObjectModel": "4.3.0",
+					"System.Reflection": "4.3.0",
+					"System.Reflection.Emit": "4.3.0",
+					"System.Reflection.Emit.ILGeneration": "4.3.0",
+					"System.Reflection.Emit.Lightweight": "4.3.0",
+					"System.Reflection.Extensions": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Reflection.TypeExtensions": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Extensions": "4.3.0",
+					"System.Threading": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.6/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.6/System.Linq.Expressions.dll": {}
+				}
+			},
+			"System.ObjectModel/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Collections": "4.3.0",
+					"System.Diagnostics.Debug": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Threading": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.ObjectModel.dll": {}
+				}
+			},
+			"System.Reactive/4.1.2": {
+				"type": "package",
+				"dependencies": {
+					"System.Runtime.InteropServices.WindowsRuntime": "4.3.0",
+					"System.Threading.Tasks.Extensions": "4.5.1"
+				},
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Reactive.dll": {}
+				}
+			},
+			"System.Reflection/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.IO": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.5/_._": {}
+				}
+			},
+			"System.Reflection.Emit/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.IO": "4.3.0",
+					"System.Reflection": "4.3.0",
+					"System.Reflection.Emit.ILGeneration": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.1/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Reflection.Emit.dll": {}
+				}
+			},
+			"System.Reflection.Emit.ILGeneration/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Reflection": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Reflection.Emit.ILGeneration.dll": {}
+				}
+			},
+			"System.Reflection.Emit.Lightweight/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Reflection": "4.3.0",
+					"System.Reflection.Emit.ILGeneration": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Reflection.Emit.Lightweight.dll": {}
+				}
+			},
+			"System.Reflection.Extensions/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Reflection": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.0/_._": {}
+				}
+			},
+			"System.Reflection.Primitives/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.0/_._": {}
+				}
+			},
+			"System.Reflection.TypeExtensions/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Reflection": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.5/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.5/System.Reflection.TypeExtensions.dll": {}
+				}
+			},
+			"System.Resources.ResourceManager/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Globalization": "4.3.0",
+					"System.Reflection": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.0/_._": {}
+				}
+			},
+			"System.Runtime/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0"
+				},
+				"compile": {
+					"ref/netstandard1.5/_._": {}
+				}
+			},
+			"System.Runtime.Extensions/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.5/_._": {}
+				}
+			},
+			"System.Runtime.Handles/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.Runtime.InteropServices/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Reflection": "4.3.0",
+					"System.Reflection.Primitives": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Handles": "4.3.0"
+				},
+				"compile": {
+					"ref/netcoreapp1.1/_._": {}
+				}
+			},
+			"System.Runtime.InteropServices.WindowsRuntime/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Runtime.InteropServices.WindowsRuntime.dll": {}
+				}
+			},
+			"System.Runtime.Loader/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.IO": "4.3.0",
+					"System.Reflection": "4.3.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.5/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.5/System.Runtime.Loader.dll": {}
+				}
+			},
+			"System.Security.Cryptography.Primitives/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Diagnostics.Debug": "4.3.0",
+					"System.Globalization": "4.3.0",
+					"System.IO": "4.3.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Threading": "4.3.0",
+					"System.Threading.Tasks": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Security.Cryptography.Primitives.dll": {}
+				}
+			},
+			"System.Security.Cryptography.ProtectedData/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"System.Resources.ResourceManager": "4.3.0",
+					"System.Runtime": "4.3.0",
+					"System.Runtime.InteropServices": "4.3.0",
+					"System.Security.Cryptography.Primitives": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"runtimes/win/lib/netstandard1.3/System.Security.Cryptography.ProtectedData.dll": {}
+				}
+			},
+			"System.Text.Encoding/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.Text.Encoding.Extensions/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0",
+					"System.Text.Encoding": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.Threading/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Runtime": "4.3.0",
+					"System.Threading.Tasks": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Threading.dll": {}
+				}
+			},
+			"System.Threading.Tasks/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"Microsoft.NETCore.Platforms": "1.1.0",
+					"Microsoft.NETCore.Targets": "1.1.0",
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				}
+			},
+			"System.Threading.Tasks.Dataflow/4.9.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard2.0/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard2.0/System.Threading.Tasks.Dataflow.dll": {}
+				}
+			},
+			"System.Threading.Tasks.Extensions/4.5.1": {
+				"type": "package",
+				"compile": {
+					"ref/netcoreapp2.1/_._": {}
+				},
+				"runtime": {
+					"lib/netcoreapp2.1/_._": {}
+				}
+			},
+			"System.Threading.Thread/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Runtime": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Threading.Thread.dll": {}
+				}
+			},
+			"System.Threading.ThreadPool/4.3.0": {
+				"type": "package",
+				"dependencies": {
+					"System.Runtime": "4.3.0",
+					"System.Runtime.Handles": "4.3.0"
+				},
+				"compile": {
+					"ref/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/System.Threading.ThreadPool.dll": {}
+				}
+			},
+			"YamlDotNet/5.3.0": {
+				"type": "package",
+				"compile": {
+					"lib/netstandard1.3/_._": {}
+				},
+				"runtime": {
+					"lib/netstandard1.3/YamlDotNet.dll": {}
+				}
+			},
+			"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common/1.0.0": {
+				"type": "project",
+				"framework": ".NETCoreApp,Version=v2.2",
+				"dependencies": {
+					"Microsoft.AspNet.WebApi.Client": "5.2.7",
+					"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts": "1.0.0",
+					"Newtonsoft.Json": "12.0.3",
+					"System.Composition.AttributedModel": "1.4.0",
+					"System.Composition.Convention": "1.4.0",
+					"System.Composition.Hosting": "1.4.0",
+					"System.Composition.Runtime": "1.4.0",
+					"System.Composition.TypedParts": "1.4.0",
+					"System.Reactive": "4.1.2"
+				},
+				"compile": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common.dll": {}
+				},
+				"runtime": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common.dll": {}
+				}
+			},
+			"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts/1.0.0": {
+				"type": "project",
+				"framework": ".NETCoreApp,Version=v2.2",
+				"dependencies": {
+					"Newtonsoft.Json": "12.0.3",
+					"System.Composition.AttributedModel": "1.4.0",
+					"System.Reactive": "4.1.2"
+				},
+				"compile": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts.dll": {}
+				},
+				"runtime": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts.dll": {}
+				}
+			},
+			"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Detectors/1.0.0": {
+				"type": "project",
+				"framework": ".NETCoreApp,Version=v2.2",
+				"dependencies": {
+					"DotNet.Glob": "2.1.1",
+					"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common": "1.0.0",
+					"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts": "1.0.0",
+					"Nett": "0.10.0",
+					"Newtonsoft.Json": "12.0.3",
+					"NuGet.ProjectModel": "5.6.0",
+					"NuGet.Versioning": "5.6.0",
+					"Polly": "7.0.3",
+					"SemanticVersioning": "1.2.0",
+					"System.Composition.AttributedModel": "1.4.0",
+					"System.Composition.Convention": "1.4.0",
+					"System.Composition.Hosting": "1.4.0",
+					"System.Composition.Runtime": "1.4.0",
+					"System.Composition.TypedParts": "1.4.0",
+					"System.Reactive": "4.1.2",
+					"System.Threading.Tasks.Dataflow": "4.9.0",
+					"yamldotnet": "5.3.0"
+				},
+				"compile": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Detectors.dll": {}
+				},
+				"runtime": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Detectors.dll": {}
+				}
+			},
+			"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Orchestrator/1.0.0": {
+				"type": "project",
+				"framework": ".NETCoreApp,Version=v2.2",
+				"dependencies": {
+					"CommandLineParser": "2.8.0",
+					"DotNet.Glob": "2.1.1",
+					"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common": "1.0.0",
+					"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts": "1.0.0",
+					"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Detectors": "1.0.0",
+					"Newtonsoft.Json": "12.0.3",
+					"Polly": "7.0.3",
+					"System.Composition.AttributedModel": "1.4.0",
+					"System.Composition.Convention": "1.4.0",
+					"System.Composition.Hosting": "1.4.0",
+					"System.Composition.Runtime": "1.4.0",
+					"System.Composition.TypedParts": "1.4.0",
+					"System.Reactive": "4.1.2",
+					"System.Runtime.Loader": "4.3.0",
+					"System.Threading.Tasks.Dataflow": "4.9.0"
+				},
+				"compile": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Orchestrator.dll": {}
+				},
+				"runtime": {
+					"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Orchestrator.dll": {}
+				}
+			}
+		}
+	},
+	"libraries": {
+		"CommandLineParser/2.8.0": {
+			"sha512": "eco2HlKQBY4Joz9odHigzGpVzv6pjsXnY5lziioMveQxr+i2Z7xYcIOMeZTgYiqnMtMAbXMXsVhrNfWO5vJS8Q==",
+			"type": "package",
+			"path": "commandlineparser/2.8.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"CommandLine20.png",
+				"License.md",
+				"README.md",
+				"commandlineparser.2.8.0.nupkg.sha512",
+				"commandlineparser.nuspec",
+				"lib/net40/CommandLine.dll",
+				"lib/net40/CommandLine.xml",
+				"lib/net45/CommandLine.dll",
+				"lib/net45/CommandLine.xml",
+				"lib/net461/CommandLine.dll",
+				"lib/net461/CommandLine.xml",
+				"lib/netstandard2.0/CommandLine.dll",
+				"lib/netstandard2.0/CommandLine.xml"
+			]
+		},
+		"coverlet.msbuild/2.5.1": {
+			"sha512": "+2+jvqh6pMWEDUhx3gkxo8zqxAPiac/2Ky8KpvuujJfM9JPne6eUiscnvt4hnKet2EKX+PpEiW4NQYdMMMzhqQ==",
+			"type": "package",
+			"path": "coverlet.msbuild/2.5.1",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"build/netstandard2.0/ConsoleTables.dll",
+				"build/netstandard2.0/Microsoft.Build.Framework.dll",
+				"build/netstandard2.0/Microsoft.Build.Utilities.Core.dll",
+				"build/netstandard2.0/Microsoft.Extensions.FileSystemGlobbing.dll",
+				"build/netstandard2.0/Mono.Cecil.Mdb.dll",
+				"build/netstandard2.0/Mono.Cecil.Pdb.dll",
+				"build/netstandard2.0/Mono.Cecil.Rocks.dll",
+				"build/netstandard2.0/Mono.Cecil.dll",
+				"build/netstandard2.0/Newtonsoft.Json.dll",
+				"build/netstandard2.0/coverlet.core.dll",
+				"build/netstandard2.0/coverlet.msbuild.props",
+				"build/netstandard2.0/coverlet.msbuild.targets",
+				"build/netstandard2.0/coverlet.msbuild.tasks.deps.json",
+				"build/netstandard2.0/coverlet.msbuild.tasks.dll",
+				"build/netstandard2.0/coverlet.template.dll",
+				"build/netstandard2.0/runtimes/unix/lib/netstandard1.3/System.Text.Encoding.CodePages.dll",
+				"build/netstandard2.0/runtimes/win/lib/netstandard1.3/System.Text.Encoding.CodePages.dll",
+				"coverlet.msbuild.2.5.1.nupkg.sha512",
+				"coverlet.msbuild.nuspec"
+			]
+		},
+		"DotNet.Glob/2.1.1": {
+			"sha512": "bi9K+JHUZ0fZ1QqUI5MM1FY5WNMxIE4DSTBMMC5iFS/oxqcbcA5rwuYAsKwI/UHQOfyOLc/j5K24SN46h4Z+xQ==",
+			"type": "package",
+			"path": "dotnet.glob/2.1.1",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"dotnet.glob.2.1.1.nupkg.sha512",
+				"dotnet.glob.nuspec",
+				"lib/net40/DotNet.Glob.dll",
+				"lib/net45/DotNet.Glob.dll",
+				"lib/net46/DotNet.Glob.dll",
+				"lib/netstandard1.1/DotNet.Glob.dll"
+			]
+		},
+		"Microsoft.AspNet.WebApi.Client/5.2.7": {
+			"sha512": "/76fAHknzvFqbznS6Uj2sOyE9rJB3PltY+f53TH8dX9RiGhk02EhuFCWljSj5nnqKaTsmma8DFR50OGyQ4yJ1g==",
+			"type": "package",
+			"path": "microsoft.aspnet.webapi.client/5.2.7",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"lib/net45/System.Net.Http.Formatting.dll",
+				"lib/net45/System.Net.Http.Formatting.xml",
+				"lib/netstandard2.0/System.Net.Http.Formatting.dll",
+				"lib/netstandard2.0/System.Net.Http.Formatting.xml",
+				"lib/portable-wp8+netcore45+net45+wp81+wpa81/System.Net.Http.Formatting.dll",
+				"lib/portable-wp8+netcore45+net45+wp81+wpa81/System.Net.Http.Formatting.xml",
+				"microsoft.aspnet.webapi.client.5.2.7.nupkg.sha512",
+				"microsoft.aspnet.webapi.client.nuspec"
+			]
+		},
+		"Microsoft.NETCore.App/2.2.8": {
+			"sha512": "GOxlvyc8hFrnhDjYlm25JJ7PwoyeoOpZzcg6ZgF8n8l6VxezNupRkkTeA2ek1WsspN0CdAoA8e7iDVNU84/F+Q==",
+			"type": "package",
+			"path": "microsoft.netcore.app/2.2.8",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.TXT",
+				"Microsoft.NETCore.App.versions.txt",
+				"THIRD-PARTY-NOTICES.TXT",
+				"build/netcoreapp2.2/Microsoft.NETCore.App.PlatformManifest.txt",
+				"build/netcoreapp2.2/Microsoft.NETCore.App.props",
+				"build/netcoreapp2.2/Microsoft.NETCore.App.targets",
+				"microsoft.netcore.app.2.2.8.nupkg.sha512",
+				"microsoft.netcore.app.nuspec",
+				"ref/netcoreapp2.2/Microsoft.CSharp.dll",
+				"ref/netcoreapp2.2/Microsoft.CSharp.xml",
+				"ref/netcoreapp2.2/Microsoft.VisualBasic.dll",
+				"ref/netcoreapp2.2/Microsoft.VisualBasic.xml",
+				"ref/netcoreapp2.2/Microsoft.Win32.Primitives.dll",
+				"ref/netcoreapp2.2/Microsoft.Win32.Primitives.xml",
+				"ref/netcoreapp2.2/System.AppContext.dll",
+				"ref/netcoreapp2.2/System.Buffers.dll",
+				"ref/netcoreapp2.2/System.Buffers.xml",
+				"ref/netcoreapp2.2/System.Collections.Concurrent.dll",
+				"ref/netcoreapp2.2/System.Collections.Concurrent.xml",
+				"ref/netcoreapp2.2/System.Collections.Immutable.dll",
+				"ref/netcoreapp2.2/System.Collections.Immutable.xml",
+				"ref/netcoreapp2.2/System.Collections.NonGeneric.dll",
+				"ref/netcoreapp2.2/System.Collections.NonGeneric.xml",
+				"ref/netcoreapp2.2/System.Collections.Specialized.dll",
+				"ref/netcoreapp2.2/System.Collections.Specialized.xml",
+				"ref/netcoreapp2.2/System.Collections.dll",
+				"ref/netcoreapp2.2/System.Collections.xml",
+				"ref/netcoreapp2.2/System.ComponentModel.Annotations.dll",
+				"ref/netcoreapp2.2/System.ComponentModel.Annotations.xml",
+				"ref/netcoreapp2.2/System.ComponentModel.DataAnnotations.dll",
+				"ref/netcoreapp2.2/System.ComponentModel.EventBasedAsync.dll",
+				"ref/netcoreapp2.2/System.ComponentModel.EventBasedAsync.xml",
+				"ref/netcoreapp2.2/System.ComponentModel.Primitives.dll",
+				"ref/netcoreapp2.2/System.ComponentModel.Primitives.xml",
+				"ref/netcoreapp2.2/System.ComponentModel.TypeConverter.dll",
+				"ref/netcoreapp2.2/System.ComponentModel.TypeConverter.xml",
+				"ref/netcoreapp2.2/System.ComponentModel.dll",
+				"ref/netcoreapp2.2/System.ComponentModel.xml",
+				"ref/netcoreapp2.2/System.Configuration.dll",
+				"ref/netcoreapp2.2/System.Console.dll",
+				"ref/netcoreapp2.2/System.Console.xml",
+				"ref/netcoreapp2.2/System.Core.dll",
+				"ref/netcoreapp2.2/System.Data.Common.dll",
+				"ref/netcoreapp2.2/System.Data.Common.xml",
+				"ref/netcoreapp2.2/System.Data.dll",
+				"ref/netcoreapp2.2/System.Diagnostics.Contracts.dll",
+				"ref/netcoreapp2.2/System.Diagnostics.Contracts.xml",
+				"ref/netcoreapp2.2/System.Diagnostics.Debug.dll",
+				"ref/netcoreapp2.2/System.Diagnostics.Debug.xml",
+				"ref/netcoreapp2.2/System.Diagnostics.DiagnosticSource.dll",
+				"ref/netcoreapp2.2/System.Diagnostics.DiagnosticSource.xml",
+				"ref/netcoreapp2.2/System.Diagnostics.FileVersionInfo.dll",
+				"ref/netcoreapp2.2/System.Diagnostics.FileVersionInfo.xml",
+				"ref/netcoreapp2.2/System.Diagnostics.Process.dll",
+				"ref/netcoreapp2.2/System.Diagnostics.Process.xml",
+				"ref/netcoreapp2.2/System.Diagnostics.StackTrace.dll",
+				"ref/netcoreapp2.2/System.Diagnostics.StackTrace.xml",
+				"ref/netcoreapp2.2/System.Diagnostics.TextWriterTraceListener.dll",
+				"ref/netcoreapp2.2/System.Diagnostics.TextWriterTraceListener.xml",
+				"ref/netcoreapp2.2/System.Diagnostics.Tools.dll",
+				"ref/netcoreapp2.2/System.Diagnostics.Tools.xml",
+				"ref/netcoreapp2.2/System.Diagnostics.TraceSource.dll",
+				"ref/netcoreapp2.2/System.Diagnostics.TraceSource.xml",
+				"ref/netcoreapp2.2/System.Diagnostics.Tracing.dll",
+				"ref/netcoreapp2.2/System.Diagnostics.Tracing.xml",
+				"ref/netcoreapp2.2/System.Drawing.Primitives.dll",
+				"ref/netcoreapp2.2/System.Drawing.Primitives.xml",
+				"ref/netcoreapp2.2/System.Drawing.dll",
+				"ref/netcoreapp2.2/System.Dynamic.Runtime.dll",
+				"ref/netcoreapp2.2/System.Globalization.Calendars.dll",
+				"ref/netcoreapp2.2/System.Globalization.Extensions.dll",
+				"ref/netcoreapp2.2/System.Globalization.dll",
+				"ref/netcoreapp2.2/System.IO.Compression.Brotli.dll",
+				"ref/netcoreapp2.2/System.IO.Compression.FileSystem.dll",
+				"ref/netcoreapp2.2/System.IO.Compression.ZipFile.dll",
+				"ref/netcoreapp2.2/System.IO.Compression.ZipFile.xml",
+				"ref/netcoreapp2.2/System.IO.Compression.dll",
+				"ref/netcoreapp2.2/System.IO.Compression.xml",
+				"ref/netcoreapp2.2/System.IO.FileSystem.DriveInfo.dll",
+				"ref/netcoreapp2.2/System.IO.FileSystem.DriveInfo.xml",
+				"ref/netcoreapp2.2/System.IO.FileSystem.Primitives.dll",
+				"ref/netcoreapp2.2/System.IO.FileSystem.Watcher.dll",
+				"ref/netcoreapp2.2/System.IO.FileSystem.Watcher.xml",
+				"ref/netcoreapp2.2/System.IO.FileSystem.dll",
+				"ref/netcoreapp2.2/System.IO.FileSystem.xml",
+				"ref/netcoreapp2.2/System.IO.IsolatedStorage.dll",
+				"ref/netcoreapp2.2/System.IO.IsolatedStorage.xml",
+				"ref/netcoreapp2.2/System.IO.MemoryMappedFiles.dll",
+				"ref/netcoreapp2.2/System.IO.MemoryMappedFiles.xml",
+				"ref/netcoreapp2.2/System.IO.Pipes.dll",
+				"ref/netcoreapp2.2/System.IO.Pipes.xml",
+				"ref/netcoreapp2.2/System.IO.UnmanagedMemoryStream.dll",
+				"ref/netcoreapp2.2/System.IO.dll",
+				"ref/netcoreapp2.2/System.Linq.Expressions.dll",
+				"ref/netcoreapp2.2/System.Linq.Expressions.xml",
+				"ref/netcoreapp2.2/System.Linq.Parallel.dll",
+				"ref/netcoreapp2.2/System.Linq.Parallel.xml",
+				"ref/netcoreapp2.2/System.Linq.Queryable.dll",
+				"ref/netcoreapp2.2/System.Linq.Queryable.xml",
+				"ref/netcoreapp2.2/System.Linq.dll",
+				"ref/netcoreapp2.2/System.Linq.xml",
+				"ref/netcoreapp2.2/System.Memory.dll",
+				"ref/netcoreapp2.2/System.Memory.xml",
+				"ref/netcoreapp2.2/System.Net.Http.dll",
+				"ref/netcoreapp2.2/System.Net.Http.xml",
+				"ref/netcoreapp2.2/System.Net.HttpListener.dll",
+				"ref/netcoreapp2.2/System.Net.HttpListener.xml",
+				"ref/netcoreapp2.2/System.Net.Mail.dll",
+				"ref/netcoreapp2.2/System.Net.Mail.xml",
+				"ref/netcoreapp2.2/System.Net.NameResolution.dll",
+				"ref/netcoreapp2.2/System.Net.NameResolution.xml",
+				"ref/netcoreapp2.2/System.Net.NetworkInformation.dll",
+				"ref/netcoreapp2.2/System.Net.NetworkInformation.xml",
+				"ref/netcoreapp2.2/System.Net.Ping.dll",
+				"ref/netcoreapp2.2/System.Net.Ping.xml",
+				"ref/netcoreapp2.2/System.Net.Primitives.dll",
+				"ref/netcoreapp2.2/System.Net.Primitives.xml",
+				"ref/netcoreapp2.2/System.Net.Requests.dll",
+				"ref/netcoreapp2.2/System.Net.Requests.xml",
+				"ref/netcoreapp2.2/System.Net.Security.dll",
+				"ref/netcoreapp2.2/System.Net.Security.xml",
+				"ref/netcoreapp2.2/System.Net.ServicePoint.dll",
+				"ref/netcoreapp2.2/System.Net.ServicePoint.xml",
+				"ref/netcoreapp2.2/System.Net.Sockets.dll",
+				"ref/netcoreapp2.2/System.Net.Sockets.xml",
+				"ref/netcoreapp2.2/System.Net.WebClient.dll",
+				"ref/netcoreapp2.2/System.Net.WebClient.xml",
+				"ref/netcoreapp2.2/System.Net.WebHeaderCollection.dll",
+				"ref/netcoreapp2.2/System.Net.WebHeaderCollection.xml",
+				"ref/netcoreapp2.2/System.Net.WebProxy.dll",
+				"ref/netcoreapp2.2/System.Net.WebProxy.xml",
+				"ref/netcoreapp2.2/System.Net.WebSockets.Client.dll",
+				"ref/netcoreapp2.2/System.Net.WebSockets.Client.xml",
+				"ref/netcoreapp2.2/System.Net.WebSockets.dll",
+				"ref/netcoreapp2.2/System.Net.WebSockets.xml",
+				"ref/netcoreapp2.2/System.Net.dll",
+				"ref/netcoreapp2.2/System.Numerics.Vectors.dll",
+				"ref/netcoreapp2.2/System.Numerics.Vectors.xml",
+				"ref/netcoreapp2.2/System.Numerics.dll",
+				"ref/netcoreapp2.2/System.ObjectModel.dll",
+				"ref/netcoreapp2.2/System.ObjectModel.xml",
+				"ref/netcoreapp2.2/System.Reflection.DispatchProxy.dll",
+				"ref/netcoreapp2.2/System.Reflection.DispatchProxy.xml",
+				"ref/netcoreapp2.2/System.Reflection.Emit.ILGeneration.dll",
+				"ref/netcoreapp2.2/System.Reflection.Emit.ILGeneration.xml",
+				"ref/netcoreapp2.2/System.Reflection.Emit.Lightweight.dll",
+				"ref/netcoreapp2.2/System.Reflection.Emit.Lightweight.xml",
+				"ref/netcoreapp2.2/System.Reflection.Emit.dll",
+				"ref/netcoreapp2.2/System.Reflection.Emit.xml",
+				"ref/netcoreapp2.2/System.Reflection.Extensions.dll",
+				"ref/netcoreapp2.2/System.Reflection.Metadata.dll",
+				"ref/netcoreapp2.2/System.Reflection.Metadata.xml",
+				"ref/netcoreapp2.2/System.Reflection.Primitives.dll",
+				"ref/netcoreapp2.2/System.Reflection.Primitives.xml",
+				"ref/netcoreapp2.2/System.Reflection.TypeExtensions.dll",
+				"ref/netcoreapp2.2/System.Reflection.TypeExtensions.xml",
+				"ref/netcoreapp2.2/System.Reflection.dll",
+				"ref/netcoreapp2.2/System.Resources.Reader.dll",
+				"ref/netcoreapp2.2/System.Resources.ResourceManager.dll",
+				"ref/netcoreapp2.2/System.Resources.ResourceManager.xml",
+				"ref/netcoreapp2.2/System.Resources.Writer.dll",
+				"ref/netcoreapp2.2/System.Resources.Writer.xml",
+				"ref/netcoreapp2.2/System.Runtime.CompilerServices.VisualC.dll",
+				"ref/netcoreapp2.2/System.Runtime.CompilerServices.VisualC.xml",
+				"ref/netcoreapp2.2/System.Runtime.Extensions.dll",
+				"ref/netcoreapp2.2/System.Runtime.Extensions.xml",
+				"ref/netcoreapp2.2/System.Runtime.Handles.dll",
+				"ref/netcoreapp2.2/System.Runtime.InteropServices.RuntimeInformation.dll",
+				"ref/netcoreapp2.2/System.Runtime.InteropServices.RuntimeInformation.xml",
+				"ref/netcoreapp2.2/System.Runtime.InteropServices.WindowsRuntime.dll",
+				"ref/netcoreapp2.2/System.Runtime.InteropServices.WindowsRuntime.xml",
+				"ref/netcoreapp2.2/System.Runtime.InteropServices.dll",
+				"ref/netcoreapp2.2/System.Runtime.InteropServices.xml",
+				"ref/netcoreapp2.2/System.Runtime.Loader.dll",
+				"ref/netcoreapp2.2/System.Runtime.Loader.xml",
+				"ref/netcoreapp2.2/System.Runtime.Numerics.dll",
+				"ref/netcoreapp2.2/System.Runtime.Numerics.xml",
+				"ref/netcoreapp2.2/System.Runtime.Serialization.Formatters.dll",
+				"ref/netcoreapp2.2/System.Runtime.Serialization.Formatters.xml",
+				"ref/netcoreapp2.2/System.Runtime.Serialization.Json.dll",
+				"ref/netcoreapp2.2/System.Runtime.Serialization.Json.xml",
+				"ref/netcoreapp2.2/System.Runtime.Serialization.Primitives.dll",
+				"ref/netcoreapp2.2/System.Runtime.Serialization.Primitives.xml",
+				"ref/netcoreapp2.2/System.Runtime.Serialization.Xml.dll",
+				"ref/netcoreapp2.2/System.Runtime.Serialization.Xml.xml",
+				"ref/netcoreapp2.2/System.Runtime.Serialization.dll",
+				"ref/netcoreapp2.2/System.Runtime.dll",
+				"ref/netcoreapp2.2/System.Runtime.xml",
+				"ref/netcoreapp2.2/System.Security.Claims.dll",
+				"ref/netcoreapp2.2/System.Security.Claims.xml",
+				"ref/netcoreapp2.2/System.Security.Cryptography.Algorithms.dll",
+				"ref/netcoreapp2.2/System.Security.Cryptography.Algorithms.xml",
+				"ref/netcoreapp2.2/System.Security.Cryptography.Csp.dll",
+				"ref/netcoreapp2.2/System.Security.Cryptography.Csp.xml",
+				"ref/netcoreapp2.2/System.Security.Cryptography.Encoding.dll",
+				"ref/netcoreapp2.2/System.Security.Cryptography.Encoding.xml",
+				"ref/netcoreapp2.2/System.Security.Cryptography.Primitives.dll",
+				"ref/netcoreapp2.2/System.Security.Cryptography.Primitives.xml",
+				"ref/netcoreapp2.2/System.Security.Cryptography.X509Certificates.dll",
+				"ref/netcoreapp2.2/System.Security.Cryptography.X509Certificates.xml",
+				"ref/netcoreapp2.2/System.Security.Principal.dll",
+				"ref/netcoreapp2.2/System.Security.Principal.xml",
+				"ref/netcoreapp2.2/System.Security.SecureString.dll",
+				"ref/netcoreapp2.2/System.Security.dll",
+				"ref/netcoreapp2.2/System.ServiceModel.Web.dll",
+				"ref/netcoreapp2.2/System.ServiceProcess.dll",
+				"ref/netcoreapp2.2/System.Text.Encoding.Extensions.dll",
+				"ref/netcoreapp2.2/System.Text.Encoding.Extensions.xml",
+				"ref/netcoreapp2.2/System.Text.Encoding.dll",
+				"ref/netcoreapp2.2/System.Text.RegularExpressions.dll",
+				"ref/netcoreapp2.2/System.Text.RegularExpressions.xml",
+				"ref/netcoreapp2.2/System.Threading.Overlapped.dll",
+				"ref/netcoreapp2.2/System.Threading.Overlapped.xml",
+				"ref/netcoreapp2.2/System.Threading.Tasks.Dataflow.dll",
+				"ref/netcoreapp2.2/System.Threading.Tasks.Dataflow.xml",
+				"ref/netcoreapp2.2/System.Threading.Tasks.Extensions.dll",
+				"ref/netcoreapp2.2/System.Threading.Tasks.Extensions.xml",
+				"ref/netcoreapp2.2/System.Threading.Tasks.Parallel.dll",
+				"ref/netcoreapp2.2/System.Threading.Tasks.Parallel.xml",
+				"ref/netcoreapp2.2/System.Threading.Tasks.dll",
+				"ref/netcoreapp2.2/System.Threading.Tasks.xml",
+				"ref/netcoreapp2.2/System.Threading.Thread.dll",
+				"ref/netcoreapp2.2/System.Threading.Thread.xml",
+				"ref/netcoreapp2.2/System.Threading.ThreadPool.dll",
+				"ref/netcoreapp2.2/System.Threading.ThreadPool.xml",
+				"ref/netcoreapp2.2/System.Threading.Timer.dll",
+				"ref/netcoreapp2.2/System.Threading.Timer.xml",
+				"ref/netcoreapp2.2/System.Threading.dll",
+				"ref/netcoreapp2.2/System.Threading.xml",
+				"ref/netcoreapp2.2/System.Transactions.Local.dll",
+				"ref/netcoreapp2.2/System.Transactions.Local.xml",
+				"ref/netcoreapp2.2/System.Transactions.dll",
+				"ref/netcoreapp2.2/System.ValueTuple.dll",
+				"ref/netcoreapp2.2/System.Web.HttpUtility.dll",
+				"ref/netcoreapp2.2/System.Web.HttpUtility.xml",
+				"ref/netcoreapp2.2/System.Web.dll",
+				"ref/netcoreapp2.2/System.Windows.dll",
+				"ref/netcoreapp2.2/System.Xml.Linq.dll",
+				"ref/netcoreapp2.2/System.Xml.ReaderWriter.dll",
+				"ref/netcoreapp2.2/System.Xml.ReaderWriter.xml",
+				"ref/netcoreapp2.2/System.Xml.Serialization.dll",
+				"ref/netcoreapp2.2/System.Xml.XDocument.dll",
+				"ref/netcoreapp2.2/System.Xml.XDocument.xml",
+				"ref/netcoreapp2.2/System.Xml.XPath.XDocument.dll",
+				"ref/netcoreapp2.2/System.Xml.XPath.XDocument.xml",
+				"ref/netcoreapp2.2/System.Xml.XPath.dll",
+				"ref/netcoreapp2.2/System.Xml.XPath.xml",
+				"ref/netcoreapp2.2/System.Xml.XmlDocument.dll",
+				"ref/netcoreapp2.2/System.Xml.XmlSerializer.dll",
+				"ref/netcoreapp2.2/System.Xml.XmlSerializer.xml",
+				"ref/netcoreapp2.2/System.Xml.dll",
+				"ref/netcoreapp2.2/System.dll",
+				"ref/netcoreapp2.2/WindowsBase.dll",
+				"ref/netcoreapp2.2/mscorlib.dll",
+				"ref/netcoreapp2.2/netstandard.dll",
+				"runtime.json"
+			]
+		},
+		"Microsoft.NETCore.DotNetAppHost/2.2.8": {
+			"sha512": "Lh1F6z41levvtfC3KuuiQe9ppWKRP1oIB42vP1QNQE4uumo95h+LpjPDeysX1DlTjCzG0BVGSUEpCW5fHkni7w==",
+			"type": "package",
+			"path": "microsoft.netcore.dotnetapphost/2.2.8",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.TXT",
+				"THIRD-PARTY-NOTICES.TXT",
+				"microsoft.netcore.dotnetapphost.2.2.8.nupkg.sha512",
+				"microsoft.netcore.dotnetapphost.nuspec",
+				"runtime.json"
+			]
+		},
+		"Microsoft.NETCore.DotNetHostPolicy/2.2.8": {
+			"sha512": "rOHr0Dk87vaiq9d1hMpXETB4IKq1jIiPQlVKNUjRGilK/cjOcadhsk+1MsrJ/GnM3eovhy8zW2PGkN8pYEolnw==",
+			"type": "package",
+			"path": "microsoft.netcore.dotnethostpolicy/2.2.8",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.TXT",
+				"THIRD-PARTY-NOTICES.TXT",
+				"microsoft.netcore.dotnethostpolicy.2.2.8.nupkg.sha512",
+				"microsoft.netcore.dotnethostpolicy.nuspec",
+				"runtime.json"
+			]
+		},
+		"Microsoft.NETCore.DotNetHostResolver/2.2.8": {
+			"sha512": "culLr+x2GvUkXVGi4ULZ7jmWJEhuAMyS7iTWBlkWnqbKtYJ36ZlgHbw/6qTm82790gJemEFeo9RehDwfRXfJzA==",
+			"type": "package",
+			"path": "microsoft.netcore.dotnethostresolver/2.2.8",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.TXT",
+				"THIRD-PARTY-NOTICES.TXT",
+				"microsoft.netcore.dotnethostresolver.2.2.8.nupkg.sha512",
+				"microsoft.netcore.dotnethostresolver.nuspec",
+				"runtime.json"
+			]
+		},
+		"Microsoft.NETCore.Platforms/2.2.4": {
+			"sha512": "ZeCe9PRhMpKzVWrNgTvWpLjJigppErzN663lJOqAzcx0xjXpcAMpIImFI46IE1gze18VWw6bbfo7JDkcaRWuOg==",
+			"type": "package",
+			"path": "microsoft.netcore.platforms/2.2.4",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.TXT",
+				"THIRD-PARTY-NOTICES.TXT",
+				"lib/netstandard1.0/_._",
+				"microsoft.netcore.platforms.2.2.4.nupkg.sha512",
+				"microsoft.netcore.platforms.nuspec",
+				"runtime.json",
+				"useSharedDesignerContext.txt",
+				"version.txt"
+			]
+		},
+		"Microsoft.NETCore.Targets/2.0.0": {
+			"sha512": "odP/tJj1z6GylFpNo7pMtbd/xQgTC3Ex2If63dRTL38bBNMwsBnJ+RceUIyHdRBC0oik/3NehYT+oECwBhIM3Q==",
+			"type": "package",
+			"path": "microsoft.netcore.targets/2.0.0",
+			"files": [
+				".nupkg.metadata",
+				"LICENSE.TXT",
+				"THIRD-PARTY-NOTICES.TXT",
+				"lib/netstandard1.0/_._",
+				"microsoft.netcore.targets.2.0.0.nupkg.sha512",
+				"microsoft.netcore.targets.nuspec",
+				"runtime.json",
+				"useSharedDesignerContext.txt",
+				"version.txt"
+			]
+		},
+		"Microsoft.Win32.Primitives/4.3.0": {
+			"sha512": "9l/mZyHnvWCxqqBotSz/biGAJVQYrFoe2PGrhuXMNFeDy3RpB9XrRDWmiHQavhwud4fBAvi86QpRk3TLdzDYWg==",
+			"type": "package",
+			"path": "microsoft.win32.primitives/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net46/Microsoft.Win32.Primitives.dll",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"microsoft.win32.primitives.4.3.0.nupkg.sha512",
+				"microsoft.win32.primitives.nuspec",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net46/Microsoft.Win32.Primitives.dll",
+				"ref/netstandard1.3/Microsoft.Win32.Primitives.dll",
+				"ref/netstandard1.3/Microsoft.Win32.Primitives.xml",
+				"ref/netstandard1.3/de/Microsoft.Win32.Primitives.xml",
+				"ref/netstandard1.3/es/Microsoft.Win32.Primitives.xml",
+				"ref/netstandard1.3/fr/Microsoft.Win32.Primitives.xml",
+				"ref/netstandard1.3/it/Microsoft.Win32.Primitives.xml",
+				"ref/netstandard1.3/ja/Microsoft.Win32.Primitives.xml",
+				"ref/netstandard1.3/ko/Microsoft.Win32.Primitives.xml",
+				"ref/netstandard1.3/ru/Microsoft.Win32.Primitives.xml",
+				"ref/netstandard1.3/zh-hans/Microsoft.Win32.Primitives.xml",
+				"ref/netstandard1.3/zh-hant/Microsoft.Win32.Primitives.xml",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._"
+			]
+		},
+		"Microsoft.Win32.Registry/4.3.0": {
+			"sha512": "4f0ZyVP4QvGOroa0I7UTwgNXWxUDkaiRf3fpSXLG7rcQnUSh8UFrIRtg8WqDRwhsBnMbKN/2WoiWVGuf/w7VSw==",
+			"type": "package",
+			"path": "microsoft.win32.registry/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/net46/Microsoft.Win32.Registry.dll",
+				"microsoft.win32.registry.4.3.0.nupkg.sha512",
+				"microsoft.win32.registry.nuspec",
+				"ref/net46/Microsoft.Win32.Registry.dll",
+				"ref/netstandard1.3/Microsoft.Win32.Registry.dll",
+				"ref/netstandard1.3/Microsoft.Win32.Registry.xml",
+				"ref/netstandard1.3/de/Microsoft.Win32.Registry.xml",
+				"ref/netstandard1.3/es/Microsoft.Win32.Registry.xml",
+				"ref/netstandard1.3/fr/Microsoft.Win32.Registry.xml",
+				"ref/netstandard1.3/it/Microsoft.Win32.Registry.xml",
+				"ref/netstandard1.3/ja/Microsoft.Win32.Registry.xml",
+				"ref/netstandard1.3/ko/Microsoft.Win32.Registry.xml",
+				"ref/netstandard1.3/ru/Microsoft.Win32.Registry.xml",
+				"ref/netstandard1.3/zh-hans/Microsoft.Win32.Registry.xml",
+				"ref/netstandard1.3/zh-hant/Microsoft.Win32.Registry.xml",
+				"runtimes/unix/lib/netstandard1.3/Microsoft.Win32.Registry.dll",
+				"runtimes/win/lib/net46/Microsoft.Win32.Registry.dll",
+				"runtimes/win/lib/netcore50/_._",
+				"runtimes/win/lib/netstandard1.3/Microsoft.Win32.Registry.dll"
+			]
+		},
+		"MinVer/2.5.0": {
+			"sha512": "+vgY+COxnu93nZEVYScloRuboNRIYkElokxTdtKLt6isr/f6GllPt0oLfrHj7fzxgj7SC5xMZg5c2qvd6qyHDQ==",
+			"type": "package",
+			"path": "minver/2.5.0",
+			"hasTools": true,
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"build/MinVer.targets",
+				"buildMultiTargeting/MinVer.targets",
+				"minver.2.5.0.nupkg.sha512",
+				"minver.nuspec",
+				"minver.png",
+				"minver/McMaster.Extensions.CommandLineUtils.dll",
+				"minver/MinVer.Lib.dll",
+				"minver/MinVer.Lib.pdb",
+				"minver/MinVer.deps.json",
+				"minver/MinVer.dll",
+				"minver/MinVer.pdb",
+				"minver/MinVer.runtimeconfig.json"
+			]
+		},
+		"NETStandard.Library/2.0.3": {
+			"sha512": "VmImnWywqGJMIIbkn2cAPHn+dZ7FR8LivkZ9BNMUZeQxBQ7e8S6ElMV51dyOA4tZjNycXlqvF7ALO4IbYdAi6w==",
+			"type": "package",
+			"path": "netstandard.library/2.0.3",
+			"files": [
+				".nupkg.metadata",
+				"LICENSE.TXT",
+				"THIRD-PARTY-NOTICES.TXT",
+				"build/netstandard2.0/NETStandard.Library.targets",
+				"build/netstandard2.0/ref/Microsoft.Win32.Primitives.dll",
+				"build/netstandard2.0/ref/System.AppContext.dll",
+				"build/netstandard2.0/ref/System.Collections.Concurrent.dll",
+				"build/netstandard2.0/ref/System.Collections.NonGeneric.dll",
+				"build/netstandard2.0/ref/System.Collections.Specialized.dll",
+				"build/netstandard2.0/ref/System.Collections.dll",
+				"build/netstandard2.0/ref/System.ComponentModel.Composition.dll",
+				"build/netstandard2.0/ref/System.ComponentModel.EventBasedAsync.dll",
+				"build/netstandard2.0/ref/System.ComponentModel.Primitives.dll",
+				"build/netstandard2.0/ref/System.ComponentModel.TypeConverter.dll",
+				"build/netstandard2.0/ref/System.ComponentModel.dll",
+				"build/netstandard2.0/ref/System.Console.dll",
+				"build/netstandard2.0/ref/System.Core.dll",
+				"build/netstandard2.0/ref/System.Data.Common.dll",
+				"build/netstandard2.0/ref/System.Data.dll",
+				"build/netstandard2.0/ref/System.Diagnostics.Contracts.dll",
+				"build/netstandard2.0/ref/System.Diagnostics.Debug.dll",
+				"build/netstandard2.0/ref/System.Diagnostics.FileVersionInfo.dll",
+				"build/netstandard2.0/ref/System.Diagnostics.Process.dll",
+				"build/netstandard2.0/ref/System.Diagnostics.StackTrace.dll",
+				"build/netstandard2.0/ref/System.Diagnostics.TextWriterTraceListener.dll",
+				"build/netstandard2.0/ref/System.Diagnostics.Tools.dll",
+				"build/netstandard2.0/ref/System.Diagnostics.TraceSource.dll",
+				"build/netstandard2.0/ref/System.Diagnostics.Tracing.dll",
+				"build/netstandard2.0/ref/System.Drawing.Primitives.dll",
+				"build/netstandard2.0/ref/System.Drawing.dll",
+				"build/netstandard2.0/ref/System.Dynamic.Runtime.dll",
+				"build/netstandard2.0/ref/System.Globalization.Calendars.dll",
+				"build/netstandard2.0/ref/System.Globalization.Extensions.dll",
+				"build/netstandard2.0/ref/System.Globalization.dll",
+				"build/netstandard2.0/ref/System.IO.Compression.FileSystem.dll",
+				"build/netstandard2.0/ref/System.IO.Compression.ZipFile.dll",
+				"build/netstandard2.0/ref/System.IO.Compression.dll",
+				"build/netstandard2.0/ref/System.IO.FileSystem.DriveInfo.dll",
+				"build/netstandard2.0/ref/System.IO.FileSystem.Primitives.dll",
+				"build/netstandard2.0/ref/System.IO.FileSystem.Watcher.dll",
+				"build/netstandard2.0/ref/System.IO.FileSystem.dll",
+				"build/netstandard2.0/ref/System.IO.IsolatedStorage.dll",
+				"build/netstandard2.0/ref/System.IO.MemoryMappedFiles.dll",
+				"build/netstandard2.0/ref/System.IO.Pipes.dll",
+				"build/netstandard2.0/ref/System.IO.UnmanagedMemoryStream.dll",
+				"build/netstandard2.0/ref/System.IO.dll",
+				"build/netstandard2.0/ref/System.Linq.Expressions.dll",
+				"build/netstandard2.0/ref/System.Linq.Parallel.dll",
+				"build/netstandard2.0/ref/System.Linq.Queryable.dll",
+				"build/netstandard2.0/ref/System.Linq.dll",
+				"build/netstandard2.0/ref/System.Net.Http.dll",
+				"build/netstandard2.0/ref/System.Net.NameResolution.dll",
+				"build/netstandard2.0/ref/System.Net.NetworkInformation.dll",
+				"build/netstandard2.0/ref/System.Net.Ping.dll",
+				"build/netstandard2.0/ref/System.Net.Primitives.dll",
+				"build/netstandard2.0/ref/System.Net.Requests.dll",
+				"build/netstandard2.0/ref/System.Net.Security.dll",
+				"build/netstandard2.0/ref/System.Net.Sockets.dll",
+				"build/netstandard2.0/ref/System.Net.WebHeaderCollection.dll",
+				"build/netstandard2.0/ref/System.Net.WebSockets.Client.dll",
+				"build/netstandard2.0/ref/System.Net.WebSockets.dll",
+				"build/netstandard2.0/ref/System.Net.dll",
+				"build/netstandard2.0/ref/System.Numerics.dll",
+				"build/netstandard2.0/ref/System.ObjectModel.dll",
+				"build/netstandard2.0/ref/System.Reflection.Extensions.dll",
+				"build/netstandard2.0/ref/System.Reflection.Primitives.dll",
+				"build/netstandard2.0/ref/System.Reflection.dll",
+				"build/netstandard2.0/ref/System.Resources.Reader.dll",
+				"build/netstandard2.0/ref/System.Resources.ResourceManager.dll",
+				"build/netstandard2.0/ref/System.Resources.Writer.dll",
+				"build/netstandard2.0/ref/System.Runtime.CompilerServices.VisualC.dll",
+				"build/netstandard2.0/ref/System.Runtime.Extensions.dll",
+				"build/netstandard2.0/ref/System.Runtime.Handles.dll",
+				"build/netstandard2.0/ref/System.Runtime.InteropServices.RuntimeInformation.dll",
+				"build/netstandard2.0/ref/System.Runtime.InteropServices.dll",
+				"build/netstandard2.0/ref/System.Runtime.Numerics.dll",
+				"build/netstandard2.0/ref/System.Runtime.Serialization.Formatters.dll",
+				"build/netstandard2.0/ref/System.Runtime.Serialization.Json.dll",
+				"build/netstandard2.0/ref/System.Runtime.Serialization.Primitives.dll",
+				"build/netstandard2.0/ref/System.Runtime.Serialization.Xml.dll",
+				"build/netstandard2.0/ref/System.Runtime.Serialization.dll",
+				"build/netstandard2.0/ref/System.Runtime.dll",
+				"build/netstandard2.0/ref/System.Security.Claims.dll",
+				"build/netstandard2.0/ref/System.Security.Cryptography.Algorithms.dll",
+				"build/netstandard2.0/ref/System.Security.Cryptography.Csp.dll",
+				"build/netstandard2.0/ref/System.Security.Cryptography.Encoding.dll",
+				"build/netstandard2.0/ref/System.Security.Cryptography.Primitives.dll",
+				"build/netstandard2.0/ref/System.Security.Cryptography.X509Certificates.dll",
+				"build/netstandard2.0/ref/System.Security.Principal.dll",
+				"build/netstandard2.0/ref/System.Security.SecureString.dll",
+				"build/netstandard2.0/ref/System.ServiceModel.Web.dll",
+				"build/netstandard2.0/ref/System.Text.Encoding.Extensions.dll",
+				"build/netstandard2.0/ref/System.Text.Encoding.dll",
+				"build/netstandard2.0/ref/System.Text.RegularExpressions.dll",
+				"build/netstandard2.0/ref/System.Threading.Overlapped.dll",
+				"build/netstandard2.0/ref/System.Threading.Tasks.Parallel.dll",
+				"build/netstandard2.0/ref/System.Threading.Tasks.dll",
+				"build/netstandard2.0/ref/System.Threading.Thread.dll",
+				"build/netstandard2.0/ref/System.Threading.ThreadPool.dll",
+				"build/netstandard2.0/ref/System.Threading.Timer.dll",
+				"build/netstandard2.0/ref/System.Threading.dll",
+				"build/netstandard2.0/ref/System.Transactions.dll",
+				"build/netstandard2.0/ref/System.ValueTuple.dll",
+				"build/netstandard2.0/ref/System.Web.dll",
+				"build/netstandard2.0/ref/System.Windows.dll",
+				"build/netstandard2.0/ref/System.Xml.Linq.dll",
+				"build/netstandard2.0/ref/System.Xml.ReaderWriter.dll",
+				"build/netstandard2.0/ref/System.Xml.Serialization.dll",
+				"build/netstandard2.0/ref/System.Xml.XDocument.dll",
+				"build/netstandard2.0/ref/System.Xml.XPath.XDocument.dll",
+				"build/netstandard2.0/ref/System.Xml.XPath.dll",
+				"build/netstandard2.0/ref/System.Xml.XmlDocument.dll",
+				"build/netstandard2.0/ref/System.Xml.XmlSerializer.dll",
+				"build/netstandard2.0/ref/System.Xml.dll",
+				"build/netstandard2.0/ref/System.dll",
+				"build/netstandard2.0/ref/mscorlib.dll",
+				"build/netstandard2.0/ref/netstandard.dll",
+				"build/netstandard2.0/ref/netstandard.xml",
+				"lib/netstandard1.0/_._",
+				"netstandard.library.2.0.3.nupkg.sha512",
+				"netstandard.library.nuspec",
+				"paket-installmodel.cache"
+			]
+		},
+		"Nett/0.10.0": {
+			"sha512": "qR4/qq4EZdMkVHn3YLjIZLo6kELbBbZ3v+lNfF7rNAX8ZEU7/2Phj/5sXsgv42H9NAvId3W6YLL45rrHr4xrrw==",
+			"type": "package",
+			"path": "nett/0.10.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"lib/Net40/Nett.dll",
+				"lib/Net40/Nett.xml",
+				"lib/netstandard2.0/Nett.dll",
+				"lib/netstandard2.0/Nett.xml",
+				"nett.0.10.0.nupkg.sha512",
+				"nett.nuspec"
+			]
+		},
+		"Newtonsoft.Json/12.0.3": {
+			"sha512": "6mgjfnRB4jKMlzHSl+VD+oUc1IebOZabkbyWj2RiTgWwYPPuaK1H97G1sHqGwPlS5npiF5Q0OrxN1wni2n5QWg==",
+			"type": "package",
+			"path": "newtonsoft.json/12.0.3",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.md",
+				"lib/net20/Newtonsoft.Json.dll",
+				"lib/net20/Newtonsoft.Json.xml",
+				"lib/net35/Newtonsoft.Json.dll",
+				"lib/net35/Newtonsoft.Json.xml",
+				"lib/net40/Newtonsoft.Json.dll",
+				"lib/net40/Newtonsoft.Json.xml",
+				"lib/net45/Newtonsoft.Json.dll",
+				"lib/net45/Newtonsoft.Json.xml",
+				"lib/netstandard1.0/Newtonsoft.Json.dll",
+				"lib/netstandard1.0/Newtonsoft.Json.xml",
+				"lib/netstandard1.3/Newtonsoft.Json.dll",
+				"lib/netstandard1.3/Newtonsoft.Json.xml",
+				"lib/netstandard2.0/Newtonsoft.Json.dll",
+				"lib/netstandard2.0/Newtonsoft.Json.xml",
+				"lib/portable-net40+sl5+win8+wp8+wpa81/Newtonsoft.Json.dll",
+				"lib/portable-net40+sl5+win8+wp8+wpa81/Newtonsoft.Json.xml",
+				"lib/portable-net45+win8+wp8+wpa81/Newtonsoft.Json.dll",
+				"lib/portable-net45+win8+wp8+wpa81/Newtonsoft.Json.xml",
+				"newtonsoft.json.12.0.3.nupkg.sha512",
+				"newtonsoft.json.nuspec",
+				"packageIcon.png",
+				"paket-installmodel.cache"
+			]
+		},
+		"Newtonsoft.Json.Bson/1.0.1": {
+			"sha512": "5PYT/IqQ+UK31AmZiSS102R6EsTo+LGTSI8bp7WAUqDKaF4wHXD8U9u4WxTI1vc64tYi++8p3dk3WWNqPFgldw==",
+			"type": "package",
+			"path": "newtonsoft.json.bson/1.0.1",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"lib/net45/Newtonsoft.Json.Bson.dll",
+				"lib/net45/Newtonsoft.Json.Bson.xml",
+				"lib/netstandard1.3/Newtonsoft.Json.Bson.dll",
+				"lib/netstandard1.3/Newtonsoft.Json.Bson.xml",
+				"newtonsoft.json.bson.1.0.1.nupkg.sha512",
+				"newtonsoft.json.bson.nuspec"
+			]
+		},
+		"NuGet.Common/5.6.0": {
+			"sha512": "N6EvbGxGOvDgf92osTE7o9GKeLuzRodLo6iiG/TGcUvW9sBt9oKWzB5C16MaubIKBZ8Z4J4ri8gTGUM3mEwPkg==",
+			"type": "package",
+			"path": "nuget.common/5.6.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"lib/net472/NuGet.Common.dll",
+				"lib/net472/NuGet.Common.xml",
+				"lib/netstandard2.0/NuGet.Common.dll",
+				"lib/netstandard2.0/NuGet.Common.xml",
+				"nuget.common.5.6.0.nupkg.sha512",
+				"nuget.common.nuspec"
+			]
+		},
+		"NuGet.Configuration/5.6.0": {
+			"sha512": "l4ePqw6Tml1SEhxYv8EWg/h0AiNnNi7dOnUU/8ovZ9e0e7z25AJRGt8aBwzIzntgEkRzL0KnwMuXy5oTM+wszg==",
+			"type": "package",
+			"path": "nuget.configuration/5.6.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"lib/net472/NuGet.Configuration.dll",
+				"lib/net472/NuGet.Configuration.xml",
+				"lib/netstandard2.0/NuGet.Configuration.dll",
+				"lib/netstandard2.0/NuGet.Configuration.xml",
+				"nuget.configuration.5.6.0.nupkg.sha512",
+				"nuget.configuration.nuspec"
+			]
+		},
+		"NuGet.DependencyResolver.Core/5.6.0": {
+			"sha512": "7+m4dXvMn+pJ/GUsl4DvuEU3G+CvhiQLlHPT4VtBiQGeHF0PQtxYW9YhLaLZYE/p/dJ6ipvR2E5X5wzncQvIiA==",
+			"type": "package",
+			"path": "nuget.dependencyresolver.core/5.6.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"lib/net472/NuGet.DependencyResolver.Core.dll",
+				"lib/net472/NuGet.DependencyResolver.Core.xml",
+				"lib/netstandard2.0/NuGet.DependencyResolver.Core.dll",
+				"lib/netstandard2.0/NuGet.DependencyResolver.Core.xml",
+				"nuget.dependencyresolver.core.5.6.0.nupkg.sha512",
+				"nuget.dependencyresolver.core.nuspec"
+			]
+		},
+		"NuGet.Frameworks/5.6.0": {
+			"sha512": "QKl4ieKQnDnNybLxk5y7TbIHAiYLVXxapCV7AmIr8gVk2wzPwETerlNwks1jCiknfjRPxtAf6XKNln+Q6G4RPA==",
+			"type": "package",
+			"path": "nuget.frameworks/5.6.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"lib/net40/NuGet.Frameworks.dll",
+				"lib/net40/NuGet.Frameworks.xml",
+				"lib/net472/NuGet.Frameworks.dll",
+				"lib/net472/NuGet.Frameworks.xml",
+				"lib/netstandard2.0/NuGet.Frameworks.dll",
+				"lib/netstandard2.0/NuGet.Frameworks.xml",
+				"nuget.frameworks.5.6.0.nupkg.sha512",
+				"nuget.frameworks.nuspec"
+			]
+		},
+		"NuGet.LibraryModel/5.6.0": {
+			"sha512": "DlGXw0LHwq0DZpQGi6jEk/aSrhyYoBlO3ZGA+tVLtL6rnrs/BWFQQ05OhtANsDNObp0ikjVN29/dn4DPCn2J3w==",
+			"type": "package",
+			"path": "nuget.librarymodel/5.6.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"lib/net472/NuGet.LibraryModel.dll",
+				"lib/net472/NuGet.LibraryModel.xml",
+				"lib/netstandard2.0/NuGet.LibraryModel.dll",
+				"lib/netstandard2.0/NuGet.LibraryModel.xml",
+				"nuget.librarymodel.5.6.0.nupkg.sha512",
+				"nuget.librarymodel.nuspec"
+			]
+		},
+		"NuGet.Packaging/5.6.0": {
+			"sha512": "av0vDlf071JdpP5bdX2wN+sEYFp/TkLGGArpl3mn5w7pHp7eeLYGWYpYRmRIABSkA15gxMB+P1QH1LNch5Kqhg==",
+			"type": "package",
+			"path": "nuget.packaging/5.6.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"lib/net472/NuGet.Packaging.dll",
+				"lib/net472/NuGet.Packaging.xml",
+				"lib/netstandard2.0/NuGet.Packaging.dll",
+				"lib/netstandard2.0/NuGet.Packaging.xml",
+				"nuget.packaging.5.6.0.nupkg.sha512",
+				"nuget.packaging.nuspec"
+			]
+		},
+		"NuGet.ProjectModel/5.6.0": {
+			"sha512": "gdFVa2p7lM+Nj1QvWY2+9Ww81aoaNm8Lcvq8fwJ9H1JpdJEs+tWUa+v7KDgJgRe3GzgrYn20R9Vsd34BBctqOg==",
+			"type": "package",
+			"path": "nuget.projectmodel/5.6.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"lib/net472/NuGet.ProjectModel.dll",
+				"lib/net472/NuGet.ProjectModel.xml",
+				"lib/netstandard2.0/NuGet.ProjectModel.dll",
+				"lib/netstandard2.0/NuGet.ProjectModel.xml",
+				"nuget.projectmodel.5.6.0.nupkg.sha512",
+				"nuget.projectmodel.nuspec"
+			]
+		},
+		"NuGet.Protocol/5.6.0": {
+			"sha512": "95ekDA23ypvzf138qhFkSQAkjykTikB1dKpRLBBD8Ugm9Cfib2VEWZQ3QCMwg/xdEDpoVN9/GtF+J3JYV0eBaA==",
+			"type": "package",
+			"path": "nuget.protocol/5.6.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"lib/net472/NuGet.Protocol.dll",
+				"lib/net472/NuGet.Protocol.xml",
+				"lib/netstandard2.0/NuGet.Protocol.dll",
+				"lib/netstandard2.0/NuGet.Protocol.xml",
+				"nuget.protocol.5.6.0.nupkg.sha512",
+				"nuget.protocol.nuspec"
+			]
+		},
+		"NuGet.Versioning/5.6.0": {
+			"sha512": "UIUh1X1XXSOJwc18a7ssTmCAyN7sDXOTfNJN9HfJnWX8NStzQeZq/6RHfSHYMxUjECk6n5alCQjUA51C36R46A==",
+			"type": "package",
+			"path": "nuget.versioning/5.6.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"lib/net472/NuGet.Versioning.dll",
+				"lib/net472/NuGet.Versioning.xml",
+				"lib/netstandard2.0/NuGet.Versioning.dll",
+				"lib/netstandard2.0/NuGet.Versioning.xml",
+				"nuget.versioning.5.6.0.nupkg.sha512",
+				"nuget.versioning.nuspec"
+			]
+		},
+		"Polly/7.0.3": {
+			"sha512": "wSHZLI31uJls8XCd2/HH7iMxmvTNGZeq4Zxya6A+0bruoKL4HYtkrF5PyUc38bYXhIR85xAL0nQc/L7BAdELrw==",
+			"type": "package",
+			"path": "polly/7.0.3",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"lib/netstandard1.1/Polly.deps.json",
+				"lib/netstandard1.1/Polly.dll",
+				"lib/netstandard1.1/Polly.pdb",
+				"lib/netstandard1.1/Polly.xml",
+				"lib/netstandard2.0/Polly.deps.json",
+				"lib/netstandard2.0/Polly.dll",
+				"lib/netstandard2.0/Polly.pdb",
+				"lib/netstandard2.0/Polly.xml",
+				"polly.7.0.3.nupkg.sha512",
+				"polly.nuspec"
+			]
+		},
+		"runtime.linux-x64.Microsoft.NETCore.App/2.2.8": {
+			"sha512": "8bByMNcOVfmxtyk46y6bKHXI1G0k8c0P2hxPaw75o4ARDCe+oA/Qb+iMwJidtTKICjBu8NFLrdiDR9/rHzKKUg==",
+			"type": "package",
+			"path": "runtime.linux-x64.microsoft.netcore.app/2.2.8",
+			"hasTools": true,
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.TXT",
+				"Microsoft.NETCore.App.versions.txt",
+				"THIRD-PARTY-NOTICES.TXT",
+				"ref/netstandard/_._",
+				"runtime.linux-x64.microsoft.netcore.app.2.2.8.nupkg.sha512",
+				"runtime.linux-x64.microsoft.netcore.app.nuspec",
+				"runtimes/linux-x64/lib/netcoreapp2.2/Microsoft.CSharp.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/Microsoft.CSharp.ni.{5446b4ac-9e78-4b30-93dc-c51c36d6c090}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/Microsoft.VisualBasic.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/Microsoft.VisualBasic.ni.{031a11ae-2ba5-4aa0-a923-78f3f5a75b42}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/Microsoft.Win32.Primitives.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/Microsoft.Win32.Primitives.ni.{a57916d6-0a0e-4632-8bc9-984f01bceb94}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/Microsoft.Win32.Registry.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/Microsoft.Win32.Registry.ni.{1a52b890-2931-4de7-8b9e-7ba908853ce8}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/SOS.NETCore.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/SOS.NETCore.ni.{f6fbff4e-31a2-4e95-b4c4-676c01ac0d9b}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.AppContext.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.AppContext.ni.{3a7851da-2fa5-4f41-848d-c5af73db6609}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Buffers.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Buffers.ni.{c1fb38d8-63a8-4e1a-ad62-25daf8a21828}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Collections.Concurrent.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Collections.Concurrent.ni.{54366e28-9847-4ec1-9fd2-f5defa7bc6c9}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Collections.Immutable.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Collections.Immutable.ni.{d0c3f9a5-c5ea-4d28-8354-5e981301087f}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Collections.NonGeneric.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Collections.NonGeneric.ni.{d7558cc2-1dc6-4999-8d30-afbf9f6e16d1}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Collections.Specialized.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Collections.Specialized.ni.{3808edd5-db6f-4814-b004-2fbb299f9e9b}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Collections.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Collections.ni.{3aba3dc7-64ff-4c25-ba74-5e2df535ff88}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.ComponentModel.Annotations.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.ComponentModel.Annotations.ni.{ae6fad0f-dfbb-4d86-8113-78d449943ac9}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.ComponentModel.DataAnnotations.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.ComponentModel.DataAnnotations.ni.{59cd88e8-6124-4f6c-be99-ee046ba987ce}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.ComponentModel.EventBasedAsync.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.ComponentModel.EventBasedAsync.ni.{7c12aa4b-9a58-40a2-b4eb-b9e66f9c6869}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.ComponentModel.Primitives.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.ComponentModel.Primitives.ni.{286e75f4-2705-490a-bc1a-a8fbc098d876}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.ComponentModel.TypeConverter.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.ComponentModel.TypeConverter.ni.{a919e8ed-ca2a-4779-be12-33c0189e4896}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.ComponentModel.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.ComponentModel.ni.{552215b8-4ba4-4dad-8010-a16e72f6e9fc}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Configuration.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Configuration.ni.{ecde7dc1-8b6b-4d81-a9d7-c629b55ee651}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Console.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Console.ni.{5d6acd80-8ee6-4898-9a98-945168d82e65}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Core.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Core.ni.{8c78ba34-ca25-4a12-b216-350dcb7d928c}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Data.Common.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Data.Common.ni.{f2c1bc24-b1b2-4e4e-b63d-aa6a4dc39253}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Data.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Data.ni.{73053d68-355d-4ec7-8cce-b4c89b682f90}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.Contracts.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.Contracts.ni.{aa2991c5-64ab-4e3c-ba38-ea24b822d531}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.Debug.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.Debug.ni.{f83ea2b0-fa16-4a44-9310-c60d108d0212}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.DiagnosticSource.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.DiagnosticSource.ni.{be006aed-e4aa-4b59-a375-becd975f8d54}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.FileVersionInfo.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.FileVersionInfo.ni.{1450afc6-fa62-417c-ba84-ccde672e6d55}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.Process.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.Process.ni.{6d49b1f8-f69e-4f71-9920-3026d4edac2a}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.StackTrace.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.StackTrace.ni.{156a719e-9ce2-4b8e-99c1-8b67c54b1a1b}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.TextWriterTraceListener.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.TextWriterTraceListener.ni.{4f43da9a-5b13-46e2-95ea-26577d76fcaf}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.Tools.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.Tools.ni.{250d4416-6696-40f3-b63b-df67d97c68d0}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.TraceSource.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.TraceSource.ni.{b962e1c1-9f1a-4d90-9ea6-c93aefa111aa}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.Tracing.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Diagnostics.Tracing.ni.{9137fcd5-4efc-41bc-8e00-6f1f92a6eab5}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Drawing.Primitives.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Drawing.Primitives.ni.{634b0194-9bcb-4fd3-b173-b1aea9eaebb8}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Drawing.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Drawing.ni.{a6ca4f6a-8c35-4080-8be8-3395b398253c}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Dynamic.Runtime.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Dynamic.Runtime.ni.{e8714437-9a05-4b03-85ac-60dd526bde03}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Globalization.Calendars.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Globalization.Calendars.ni.{404fb648-5455-43c7-b116-2f0a2dbe9c90}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Globalization.Extensions.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Globalization.Extensions.ni.{ee7e0755-a9ad-41bd-8a2f-53fb1f3211c7}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Globalization.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Globalization.ni.{52c8ed56-f56b-457c-914a-7fa98bce6383}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.Compression.Brotli.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.Compression.Brotli.ni.{91225069-55d0-4e2c-9151-70cdc63f47eb}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.Compression.FileSystem.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.Compression.FileSystem.ni.{ce9039ed-d311-418d-9c57-5533b409cc90}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.Compression.ZipFile.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.Compression.ZipFile.ni.{9e0a832a-4ad4-450f-a88e-4588d5699755}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.Compression.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.Compression.ni.{4185ccce-85a0-49c0-a6b4-ce8eb86febfa}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.FileSystem.AccessControl.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.FileSystem.AccessControl.ni.{fd35313f-4132-4965-af69-3379cecb562f}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.FileSystem.DriveInfo.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.FileSystem.DriveInfo.ni.{c3f9070a-1271-49d0-acd0-24c000809ae0}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.FileSystem.Primitives.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.FileSystem.Primitives.ni.{41bd4ec1-f271-4129-8ecc-1419fb644845}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.FileSystem.Watcher.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.FileSystem.Watcher.ni.{595565be-382f-4521-8290-d6b020b5d835}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.FileSystem.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.FileSystem.ni.{9f88d287-1acf-4595-a2e3-9bb9acdbfcc5}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.IsolatedStorage.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.IsolatedStorage.ni.{91b52fec-9194-4481-b672-74fb90312812}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.MemoryMappedFiles.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.MemoryMappedFiles.ni.{e2a02424-48f5-4a24-9e41-50ac5839a11f}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.Pipes.AccessControl.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.Pipes.AccessControl.ni.{03105a94-6390-41f6-9f96-0aff4144106e}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.Pipes.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.Pipes.ni.{70b2dd94-c605-4c09-bede-6ace64340d50}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.UnmanagedMemoryStream.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.UnmanagedMemoryStream.ni.{ac2348c9-2c5b-4fcf-9903-7ec9dfea7402}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.IO.ni.{b652629a-e572-43a5-bab2-a78f9d9260a6}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Linq.Expressions.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Linq.Expressions.ni.{062d78d0-7030-480d-9bc8-da39a2d972c9}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Linq.Parallel.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Linq.Parallel.ni.{3cf8e3a1-ff00-4baa-8ad8-109dcedd67b2}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Linq.Queryable.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Linq.Queryable.ni.{1a7ab755-6a5b-4ffd-b913-e071c92a11b2}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Linq.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Linq.ni.{ec5b84c1-fa57-4c97-a8ed-c32f4e907871}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Memory.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Memory.ni.{5ce2446d-fe40-485f-a7ac-4322bf8d023e}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.Http.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.Http.ni.{b2e51289-34a0-4ba9-813b-48f2290b225c}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.HttpListener.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.HttpListener.ni.{0a73e81b-2a90-4011-a662-383a8c0f60dd}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.Mail.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.Mail.ni.{6a8f2583-a963-43df-b5e8-452f077e8cfd}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.NameResolution.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.NameResolution.ni.{99d82576-3a25-4823-a64e-cb4125ae3d15}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.NetworkInformation.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.NetworkInformation.ni.{f1450592-0022-4c26-9638-cba7692623bb}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.Ping.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.Ping.ni.{8940c0a8-598c-4d87-bbd0-846e948e59de}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.Primitives.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.Primitives.ni.{9c967619-c1aa-4d25-ad03-b042ed00c7b7}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.Requests.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.Requests.ni.{3ce4d211-8261-4a8f-a721-274f67b07b23}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.Security.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.Security.ni.{597e4348-4977-41ba-a120-f93fd19cf149}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.ServicePoint.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.ServicePoint.ni.{8f27e0c5-c213-4b74-9e77-8c7008e04a3d}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.Sockets.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.Sockets.ni.{47d8a4e7-a08b-4c39-8fc0-2f76b8f40f5f}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.WebClient.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.WebClient.ni.{61751156-5cc1-49be-8551-0473146da16f}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.WebHeaderCollection.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.WebHeaderCollection.ni.{5eee16d7-3e6f-4bf7-b3f6-5e99dcee836e}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.WebProxy.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.WebProxy.ni.{65269d7c-cdd4-4f7a-8c59-5bfa949ba55d}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.WebSockets.Client.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.WebSockets.Client.ni.{d1651eba-4f3a-44a6-b449-97943ca4657f}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.WebSockets.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.WebSockets.ni.{af889f66-a047-4637-a282-c08dbe893ce4}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Net.ni.{958fd545-869a-4b20-a988-5a80d875375c}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Numerics.Vectors.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Numerics.Vectors.ni.{23bb31ce-879b-4e44-a25f-0832b68553aa}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Numerics.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Numerics.ni.{25eb404a-7669-411d-ad41-6b4d138acd7d}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.ObjectModel.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.ObjectModel.ni.{6b322332-a4ef-4bd3-bf81-9b0de8c67ab0}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Private.DataContractSerialization.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Private.DataContractSerialization.ni.{5d178b45-4654-453d-b3c9-73c7eec4924d}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Private.Uri.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Private.Uri.ni.{b2591560-5aa5-49fd-8e4d-b04f9f0b82ad}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Private.Xml.Linq.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Private.Xml.Linq.ni.{b1af64a0-e893-4f39-8d35-46be00f04e23}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Private.Xml.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Private.Xml.ni.{491da070-17e9-44aa-8cdc-738318374574}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.DispatchProxy.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.DispatchProxy.ni.{b873e227-bfab-4b4c-8789-9d53e3a66b08}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.Emit.ILGeneration.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.Emit.ILGeneration.ni.{b15af431-5bdb-4646-a5ec-7255b8d45c2e}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.Emit.Lightweight.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.Emit.Lightweight.ni.{4ef94769-d2d3-4a2a-bb71-dd1759e9c853}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.Emit.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.Emit.ni.{659d953e-aafc-4bf1-ae68-295ac449fbc2}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.Extensions.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.Extensions.ni.{03a3aac4-3683-4b3b-8ecc-efe571ad5892}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.Metadata.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.Metadata.ni.{aeaf0e2c-5d86-46d8-bfdb-8e5f8221eb1b}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.Primitives.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.Primitives.ni.{3a43fcff-54f8-45e3-b1ff-6dafb34bb183}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.TypeExtensions.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.TypeExtensions.ni.{cb01e343-31a3-4216-86f6-d83da360063c}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Reflection.ni.{a7a8aec7-2851-4307-ac68-0b4795954ea9}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Resources.Reader.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Resources.Reader.ni.{9ddfc97b-786d-4a0c-8dfa-b179ea6e7adb}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Resources.ResourceManager.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Resources.ResourceManager.ni.{f1a2dbb4-9243-4525-ba41-a827b1484a03}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Resources.Writer.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Resources.Writer.ni.{dafa6c70-40b1-475e-bbc5-9a820c7a4733}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.CompilerServices.VisualC.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.CompilerServices.VisualC.ni.{962ae5ca-5d61-495a-84f1-d063859d03b1}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Extensions.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Extensions.ni.{496d5832-3672-42de-82e5-f0937829480f}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Handles.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Handles.ni.{edd0a82f-ed63-4ebe-8331-9417dd502adc}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.InteropServices.RuntimeInformation.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.InteropServices.RuntimeInformation.ni.{28f7985c-42ed-4e63-838e-4b21fdbf8f06}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.InteropServices.WindowsRuntime.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.InteropServices.WindowsRuntime.ni.{2bb0b44a-a28e-4849-a6dd-a769da96e867}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.InteropServices.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.InteropServices.ni.{860ea5a4-0966-4a36-ab8f-f5880498802d}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Loader.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Loader.ni.{466cb8c6-f5f8-4036-8b28-dfa30e290d14}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Numerics.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Numerics.ni.{8ef36233-dde0-4376-b7ce-6717c3d2df62}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Formatters.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Formatters.ni.{6eb5f4e1-03fd-434f-bd36-0f9a2df3e21b}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Json.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Json.ni.{fb898814-c753-4216-bf57-0f5bdc02f950}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Primitives.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Primitives.ni.{0add7c60-c3c5-4981-a6e3-58bfaed57dd1}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Xml.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Xml.ni.{b3e06eb4-7f0c-4d18-8926-0c7e2df33768}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Serialization.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.Serialization.ni.{fbd03e7a-08d4-4fbf-b04e-f66767c5175d}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Runtime.ni.{248b9ac7-a403-473e-9e7c-41b45addf1e1}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.AccessControl.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.AccessControl.ni.{eb01bf40-20fd-4050-acc6-2ce4dd92f302}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Claims.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Claims.ni.{c4c249e6-d234-426a-88fd-d7259eb56ecd}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Cryptography.Algorithms.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Cryptography.Algorithms.ni.{87d0509f-685c-4064-834f-67a67cf9f715}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Cryptography.Cng.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Cryptography.Cng.ni.{06abdb3e-c613-4a0e-8c4c-0834ec48cbf7}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Cryptography.Csp.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Cryptography.Csp.ni.{15232058-4ed5-48dd-99e5-09dc303a5be9}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Cryptography.Encoding.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Cryptography.Encoding.ni.{22ff94a2-c18e-4689-b210-60aecc7186b5}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Cryptography.OpenSsl.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Cryptography.OpenSsl.ni.{6d3bc01f-b826-49e6-a9d4-763be48caf23}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Cryptography.Primitives.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Cryptography.Primitives.ni.{e36a71fc-b7f9-401a-b4de-a93f048e79e0}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Cryptography.X509Certificates.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Cryptography.X509Certificates.ni.{be508089-8185-402b-8e4d-39c1697747f3}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Principal.Windows.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Principal.Windows.ni.{5359ed79-ade5-4464-b6a1-ce0c87fc3c09}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Principal.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.Principal.ni.{1669d8a0-1a28-46f1-b4fc-7f5346dde322}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.SecureString.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.SecureString.ni.{e484103d-605d-4d33-96eb-de2c49346805}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Security.ni.{10b474ee-e56c-421b-bc25-1e6a1c0d6043}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.ServiceModel.Web.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.ServiceModel.Web.ni.{8afa0f81-64bb-47a0-a857-4fee301671f0}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.ServiceProcess.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.ServiceProcess.ni.{75e1d847-328b-46b6-a5b1-09aa70158fbc}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Text.Encoding.Extensions.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Text.Encoding.Extensions.ni.{9681e200-823e-481f-a3ef-4371d5720816}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Text.Encoding.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Text.Encoding.ni.{f22a369d-6448-4e52-a0c3-9ed29d716d16}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Text.RegularExpressions.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Text.RegularExpressions.ni.{bf365ffb-cb72-4cea-95c5-17fd8fe7b177}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.Overlapped.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.Overlapped.ni.{80f01b78-96ac-4d7a-b4e2-acdb55b59e28}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.Tasks.Dataflow.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.Tasks.Dataflow.ni.{0727fe8d-7820-4fcb-919b-572f8030a1e4}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.Tasks.Extensions.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.Tasks.Extensions.ni.{43aacac2-4f91-47dc-b83c-cc52cef06f47}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.Tasks.Parallel.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.Tasks.Parallel.ni.{fb77da04-cd2d-42a5-baee-e1d9df7568d9}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.Tasks.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.Tasks.ni.{7a25ce6e-844f-47d3-b3c6-3e36ebd57535}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.Thread.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.Thread.ni.{f90a6cb4-54a7-4c9f-87ea-25ed0ce39f64}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.ThreadPool.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.ThreadPool.ni.{8e2767c6-29f5-43d0-bbab-1993cc512fe8}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.Timer.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.Timer.ni.{0571fa8c-c382-467f-9459-9ac90ab204ae}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Threading.ni.{f2f1c0ce-24de-4331-a38c-2c487f37be75}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Transactions.Local.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Transactions.Local.ni.{b1afe66c-1c66-48cc-bf0c-250a80d533e3}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Transactions.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Transactions.ni.{e34512ea-c4fd-481d-9ed4-c50c470ad637}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.ValueTuple.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.ValueTuple.ni.{53c64b34-741b-43ef-a103-19c163cbc08b}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Web.HttpUtility.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Web.HttpUtility.ni.{32e6b061-d82b-423f-b5bb-e4f3117a6ce5}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Web.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Web.ni.{3d2b9612-ed6a-4e8b-8e8c-48d01547c414}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Windows.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Windows.ni.{6b1287f0-7485-4bd0-9ff4-fc0302baf186}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.Linq.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.Linq.ni.{de49ee51-7175-4a1c-b5c8-b75e424b8503}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.ReaderWriter.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.ReaderWriter.ni.{2cf751a4-1a96-44a5-932d-d926bc8311ea}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.Serialization.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.Serialization.ni.{7e495f1e-9e57-4b23-acd9-6e1d93d1789c}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.XDocument.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.XDocument.ni.{3ca9a3fd-5150-4f45-8f8b-014f2f9ba5a1}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.XPath.XDocument.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.XPath.XDocument.ni.{300ef4f1-6391-4cde-966c-31b686d959b4}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.XPath.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.XPath.ni.{4f87680f-1a23-4e5a-9c3d-b09c30430aad}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.XmlDocument.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.XmlDocument.ni.{516f65fa-c1bd-4693-abf3-18147520f75d}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.XmlSerializer.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.XmlSerializer.ni.{b8b08991-e6bc-4d85-a194-858135944317}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.Xml.ni.{b479a61e-8614-478e-88a9-854e59e3c823}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/System.ni.{aef768c7-c1ac-4dd5-bf87-1bdca558f74e}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/WindowsBase.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/WindowsBase.ni.{4a6141df-2307-4e5b-91d3-3fe4e349e593}.map",
+				"runtimes/linux-x64/lib/netcoreapp2.2/mscorlib.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/netstandard.dll",
+				"runtimes/linux-x64/lib/netcoreapp2.2/netstandard.ni.{193b1740-5ea8-4a13-9047-df5263aef333}.map",
+				"runtimes/linux-x64/native/System.Globalization.Native.so",
+				"runtimes/linux-x64/native/System.IO.Compression.Native.a",
+				"runtimes/linux-x64/native/System.IO.Compression.Native.so",
+				"runtimes/linux-x64/native/System.Native.a",
+				"runtimes/linux-x64/native/System.Native.so",
+				"runtimes/linux-x64/native/System.Net.Http.Native.a",
+				"runtimes/linux-x64/native/System.Net.Http.Native.so",
+				"runtimes/linux-x64/native/System.Net.Security.Native.a",
+				"runtimes/linux-x64/native/System.Net.Security.Native.so",
+				"runtimes/linux-x64/native/System.Private.CoreLib.dll",
+				"runtimes/linux-x64/native/System.Security.Cryptography.Native.OpenSsl.a",
+				"runtimes/linux-x64/native/System.Security.Cryptography.Native.OpenSsl.so",
+				"runtimes/linux-x64/native/createdump",
+				"runtimes/linux-x64/native/libclrjit.so",
+				"runtimes/linux-x64/native/libcoreclr.so",
+				"runtimes/linux-x64/native/libcoreclrtraceptprovider.so",
+				"runtimes/linux-x64/native/libdbgshim.so",
+				"runtimes/linux-x64/native/libmscordaccore.so",
+				"runtimes/linux-x64/native/libmscordbi.so",
+				"runtimes/linux-x64/native/libsos.so",
+				"runtimes/linux-x64/native/libsosplugin.so",
+				"runtimes/linux-x64/native/sosdocsunix.txt",
+				"tools/crossgen"
+			]
+		},
+		"runtime.linux-x64.Microsoft.NETCore.DotNetAppHost/2.2.8": {
+			"sha512": "4ddMGErcPf38AZIS1RpKmk4HBEnqObsZB1EzpEz0xWUK6ek/8ShnC667k13SoxWXa0mq1ZFXJX83eISJm0pkpg==",
+			"type": "package",
+			"path": "runtime.linux-x64.microsoft.netcore.dotnetapphost/2.2.8",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.TXT",
+				"THIRD-PARTY-NOTICES.TXT",
+				"runtime.linux-x64.microsoft.netcore.dotnetapphost.2.2.8.nupkg.sha512",
+				"runtime.linux-x64.microsoft.netcore.dotnetapphost.nuspec",
+				"runtimes/linux-x64/native/apphost",
+				"version.txt"
+			]
+		},
+		"runtime.linux-x64.Microsoft.NETCore.DotNetHostPolicy/2.2.8": {
+			"sha512": "304x/iQW4OS2VyqHOW0JkFICvU8K3aeUvaMTWIbnKYQQLLEYLP4meXdoqlbRVAB4EWSnMz2pJYGLL7Rrtppnpw==",
+			"type": "package",
+			"path": "runtime.linux-x64.microsoft.netcore.dotnethostpolicy/2.2.8",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.TXT",
+				"THIRD-PARTY-NOTICES.TXT",
+				"runtime.linux-x64.microsoft.netcore.dotnethostpolicy.2.2.8.nupkg.sha512",
+				"runtime.linux-x64.microsoft.netcore.dotnethostpolicy.nuspec",
+				"runtimes/linux-x64/native/libhostpolicy.so",
+				"version.txt"
+			]
+		},
+		"runtime.linux-x64.Microsoft.NETCore.DotNetHostResolver/2.2.8": {
+			"sha512": "k8j4nfWK2R6PASV0ZhsLbE2USgUAaJ53hDkN+HVWcu9jiRlHATbX4oHnV2daZ5xvbiJR5AZixes2MoA4p1hNIg==",
+			"type": "package",
+			"path": "runtime.linux-x64.microsoft.netcore.dotnethostresolver/2.2.8",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.TXT",
+				"THIRD-PARTY-NOTICES.TXT",
+				"runtime.linux-x64.microsoft.netcore.dotnethostresolver.2.2.8.nupkg.sha512",
+				"runtime.linux-x64.microsoft.netcore.dotnethostresolver.nuspec",
+				"runtimes/linux-x64/native/libhostfxr.so",
+				"version.txt"
+			]
+		},
+		"runtime.native.System/4.3.0": {
+			"sha512": "wNJH9fbFmdLp9iJeXzGQhr2GxkJbon+CCWoaNho4MNbroklzBka6u7r2k6Jw9IYHWVFy4/gCqAwu8AUE4s+RBw==",
+			"type": "package",
+			"path": "runtime.native.system/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/netstandard1.0/_._",
+				"runtime.native.system.4.3.0.nupkg.sha512",
+				"runtime.native.system.nuspec"
+			]
+		},
+		"runtime.osx-x64.Microsoft.NETCore.App/2.2.8": {
+			"sha512": "GnJ1bS+FJx+N434bckqO3ZRJ02AUXYpTNlvuADqb6lCsjXYCyrqhEBhAMfj6Jdk/ePcf+M0yMdC8hTx+F6cVsQ==",
+			"type": "package",
+			"path": "runtime.osx-x64.microsoft.netcore.app/2.2.8",
+			"hasTools": true,
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.TXT",
+				"Microsoft.NETCore.App.versions.txt",
+				"THIRD-PARTY-NOTICES.TXT",
+				"ref/netstandard/_._",
+				"runtime.osx-x64.microsoft.netcore.app.2.2.8.nupkg.sha512",
+				"runtime.osx-x64.microsoft.netcore.app.nuspec",
+				"runtimes/osx-x64/lib/netcoreapp2.2/Microsoft.CSharp.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/Microsoft.VisualBasic.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/Microsoft.Win32.Primitives.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/Microsoft.Win32.Registry.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/SOS.NETCore.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.AppContext.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Buffers.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Collections.Concurrent.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Collections.Immutable.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Collections.NonGeneric.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Collections.Specialized.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Collections.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.ComponentModel.Annotations.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.ComponentModel.DataAnnotations.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.ComponentModel.EventBasedAsync.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.ComponentModel.Primitives.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.ComponentModel.TypeConverter.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.ComponentModel.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Configuration.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Console.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Core.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Data.Common.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Data.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Diagnostics.Contracts.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Diagnostics.Debug.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Diagnostics.DiagnosticSource.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Diagnostics.FileVersionInfo.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Diagnostics.Process.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Diagnostics.StackTrace.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Diagnostics.TextWriterTraceListener.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Diagnostics.Tools.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Diagnostics.TraceSource.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Diagnostics.Tracing.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Drawing.Primitives.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Drawing.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Dynamic.Runtime.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Globalization.Calendars.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Globalization.Extensions.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Globalization.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.Compression.Brotli.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.Compression.FileSystem.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.Compression.ZipFile.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.Compression.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.FileSystem.AccessControl.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.FileSystem.DriveInfo.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.FileSystem.Primitives.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.FileSystem.Watcher.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.FileSystem.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.IsolatedStorage.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.MemoryMappedFiles.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.Pipes.AccessControl.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.Pipes.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.UnmanagedMemoryStream.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.IO.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Linq.Expressions.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Linq.Parallel.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Linq.Queryable.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Linq.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Memory.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.Http.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.HttpListener.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.Mail.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.NameResolution.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.NetworkInformation.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.Ping.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.Primitives.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.Requests.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.Security.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.ServicePoint.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.Sockets.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.WebClient.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.WebHeaderCollection.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.WebProxy.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.WebSockets.Client.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.WebSockets.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Net.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Numerics.Vectors.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Numerics.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.ObjectModel.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Private.DataContractSerialization.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Private.Uri.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Private.Xml.Linq.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Private.Xml.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Reflection.DispatchProxy.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Reflection.Emit.ILGeneration.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Reflection.Emit.Lightweight.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Reflection.Emit.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Reflection.Extensions.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Reflection.Metadata.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Reflection.Primitives.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Reflection.TypeExtensions.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Reflection.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Resources.Reader.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Resources.ResourceManager.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Resources.Writer.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.CompilerServices.VisualC.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.Extensions.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.Handles.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.InteropServices.RuntimeInformation.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.InteropServices.WindowsRuntime.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.InteropServices.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.Loader.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.Numerics.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Formatters.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Json.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Primitives.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Xml.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.Serialization.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Runtime.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.AccessControl.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.Claims.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.Cryptography.Algorithms.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.Cryptography.Cng.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.Cryptography.Csp.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.Cryptography.Encoding.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.Cryptography.OpenSsl.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.Cryptography.Primitives.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.Cryptography.X509Certificates.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.Principal.Windows.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.Principal.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.SecureString.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Security.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.ServiceModel.Web.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.ServiceProcess.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Text.Encoding.Extensions.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Text.Encoding.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Text.RegularExpressions.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Threading.Overlapped.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Threading.Tasks.Dataflow.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Threading.Tasks.Extensions.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Threading.Tasks.Parallel.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Threading.Tasks.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Threading.Thread.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Threading.ThreadPool.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Threading.Timer.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Threading.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Transactions.Local.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Transactions.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.ValueTuple.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Web.HttpUtility.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Web.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Windows.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Xml.Linq.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Xml.ReaderWriter.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Xml.Serialization.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Xml.XDocument.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Xml.XPath.XDocument.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Xml.XPath.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Xml.XmlDocument.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Xml.XmlSerializer.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.Xml.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/System.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/WindowsBase.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/mscorlib.dll",
+				"runtimes/osx-x64/lib/netcoreapp2.2/netstandard.dll",
+				"runtimes/osx-x64/native/System.Globalization.Native.dylib",
+				"runtimes/osx-x64/native/System.IO.Compression.Native.a",
+				"runtimes/osx-x64/native/System.IO.Compression.Native.dylib",
+				"runtimes/osx-x64/native/System.Native.a",
+				"runtimes/osx-x64/native/System.Native.dylib",
+				"runtimes/osx-x64/native/System.Net.Http.Native.a",
+				"runtimes/osx-x64/native/System.Net.Http.Native.dylib",
+				"runtimes/osx-x64/native/System.Net.Security.Native.a",
+				"runtimes/osx-x64/native/System.Net.Security.Native.dylib",
+				"runtimes/osx-x64/native/System.Private.CoreLib.dll",
+				"runtimes/osx-x64/native/System.Security.Cryptography.Native.Apple.a",
+				"runtimes/osx-x64/native/System.Security.Cryptography.Native.Apple.dylib",
+				"runtimes/osx-x64/native/System.Security.Cryptography.Native.OpenSsl.a",
+				"runtimes/osx-x64/native/System.Security.Cryptography.Native.OpenSsl.dylib",
+				"runtimes/osx-x64/native/libclrjit.dylib",
+				"runtimes/osx-x64/native/libcoreclr.dylib",
+				"runtimes/osx-x64/native/libdbgshim.dylib",
+				"runtimes/osx-x64/native/libmscordaccore.dylib",
+				"runtimes/osx-x64/native/libmscordbi.dylib",
+				"runtimes/osx-x64/native/libsos.dylib",
+				"runtimes/osx-x64/native/sosdocsunix.txt",
+				"tools/crossgen"
+			]
+		},
+		"runtime.osx-x64.Microsoft.NETCore.DotNetAppHost/2.2.8": {
+			"sha512": "oge9wpVTvIM7VBWi+euS+SPXT2GH1JJ8F9dL2iIocoR5Hbq7e+dN82CJeSIsoGE6MrGzeDSxUmAkImqHR6fUoA==",
+			"type": "package",
+			"path": "runtime.osx-x64.microsoft.netcore.dotnetapphost/2.2.8",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.TXT",
+				"THIRD-PARTY-NOTICES.TXT",
+				"runtime.osx-x64.microsoft.netcore.dotnetapphost.2.2.8.nupkg.sha512",
+				"runtime.osx-x64.microsoft.netcore.dotnetapphost.nuspec",
+				"runtimes/osx-x64/native/apphost",
+				"version.txt"
+			]
+		},
+		"runtime.osx-x64.Microsoft.NETCore.DotNetHostPolicy/2.2.8": {
+			"sha512": "ws9v76wfkwD/tyoqESxknlxlcyOYtboCYwC/H7gkzG0xmp+3S4oORg2vSEGe1bZmAkT4RBLtABoY9VuZrzirGA==",
+			"type": "package",
+			"path": "runtime.osx-x64.microsoft.netcore.dotnethostpolicy/2.2.8",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.TXT",
+				"THIRD-PARTY-NOTICES.TXT",
+				"runtime.osx-x64.microsoft.netcore.dotnethostpolicy.2.2.8.nupkg.sha512",
+				"runtime.osx-x64.microsoft.netcore.dotnethostpolicy.nuspec",
+				"runtimes/osx-x64/native/libhostpolicy.dylib",
+				"version.txt"
+			]
+		},
+		"runtime.osx-x64.Microsoft.NETCore.DotNetHostResolver/2.2.8": {
+			"sha512": "wKCujjj47CU6tdVj47Bw1EHx2V0xVPSiaE3q6Rk6YuUgkOeKwz4o6r6NZdSo6DgJxO0Ui8Zv7DFz0MPSZTduHQ==",
+			"type": "package",
+			"path": "runtime.osx-x64.microsoft.netcore.dotnethostresolver/2.2.8",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.TXT",
+				"THIRD-PARTY-NOTICES.TXT",
+				"runtime.osx-x64.microsoft.netcore.dotnethostresolver.2.2.8.nupkg.sha512",
+				"runtime.osx-x64.microsoft.netcore.dotnethostresolver.nuspec",
+				"runtimes/osx-x64/native/libhostfxr.dylib",
+				"version.txt"
+			]
+		},
+		"runtime.win-x64.Microsoft.NETCore.App/2.2.8": {
+			"sha512": "xbce7KYr9G4eE0F0+9MWZWHufvQilIbRQHcZVqk+K/Rk4KVcTs7agMVEQXVJlo75flhqRMDtrjEjEPp152UrAg==",
+			"type": "package",
+			"path": "runtime.win-x64.microsoft.netcore.app/2.2.8",
+			"hasTools": true,
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.TXT",
+				"Microsoft.NETCore.App.versions.txt",
+				"THIRD-PARTY-NOTICES.TXT",
+				"ref/netstandard/_._",
+				"runtime.win-x64.microsoft.netcore.app.2.2.8.nupkg.sha512",
+				"runtime.win-x64.microsoft.netcore.app.nuspec",
+				"runtimes/win-x64/lib/netcoreapp2.2/Microsoft.CSharp.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/Microsoft.VisualBasic.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/Microsoft.Win32.Primitives.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/Microsoft.Win32.Registry.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/SOS.NETCore.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.AppContext.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Buffers.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Collections.Concurrent.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Collections.Immutable.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Collections.NonGeneric.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Collections.Specialized.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Collections.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.ComponentModel.Annotations.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.ComponentModel.DataAnnotations.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.ComponentModel.EventBasedAsync.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.ComponentModel.Primitives.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.ComponentModel.TypeConverter.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.ComponentModel.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Configuration.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Console.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Core.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Data.Common.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Data.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Diagnostics.Contracts.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Diagnostics.Debug.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Diagnostics.DiagnosticSource.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Diagnostics.FileVersionInfo.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Diagnostics.Process.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Diagnostics.StackTrace.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Diagnostics.TextWriterTraceListener.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Diagnostics.Tools.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Diagnostics.TraceSource.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Diagnostics.Tracing.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Drawing.Primitives.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Drawing.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Dynamic.Runtime.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Globalization.Calendars.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Globalization.Extensions.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Globalization.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.IO.Compression.Brotli.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.IO.Compression.FileSystem.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.IO.Compression.ZipFile.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.IO.Compression.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.IO.FileSystem.AccessControl.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.IO.FileSystem.DriveInfo.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.IO.FileSystem.Primitives.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.IO.FileSystem.Watcher.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.IO.FileSystem.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.IO.IsolatedStorage.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.IO.MemoryMappedFiles.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.IO.Pipes.AccessControl.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.IO.Pipes.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.IO.UnmanagedMemoryStream.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.IO.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Linq.Expressions.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Linq.Parallel.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Linq.Queryable.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Linq.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Memory.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Net.Http.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Net.HttpListener.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Net.Mail.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Net.NameResolution.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Net.NetworkInformation.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Net.Ping.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Net.Primitives.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Net.Requests.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Net.Security.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Net.ServicePoint.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Net.Sockets.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Net.WebClient.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Net.WebHeaderCollection.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Net.WebProxy.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Net.WebSockets.Client.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Net.WebSockets.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Net.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Numerics.Vectors.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Numerics.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.ObjectModel.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Private.DataContractSerialization.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Private.Uri.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Private.Xml.Linq.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Private.Xml.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Reflection.DispatchProxy.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Reflection.Emit.ILGeneration.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Reflection.Emit.Lightweight.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Reflection.Emit.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Reflection.Extensions.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Reflection.Metadata.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Reflection.Primitives.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Reflection.TypeExtensions.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Reflection.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Resources.Reader.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Resources.ResourceManager.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Resources.Writer.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.CompilerServices.VisualC.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.Extensions.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.Handles.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.InteropServices.RuntimeInformation.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.InteropServices.WindowsRuntime.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.InteropServices.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.Loader.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.Numerics.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Formatters.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Json.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Primitives.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.Serialization.Xml.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.Serialization.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Runtime.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Security.AccessControl.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Security.Claims.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Security.Cryptography.Algorithms.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Security.Cryptography.Cng.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Security.Cryptography.Csp.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Security.Cryptography.Encoding.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Security.Cryptography.OpenSsl.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Security.Cryptography.Primitives.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Security.Cryptography.X509Certificates.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Security.Principal.Windows.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Security.Principal.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Security.SecureString.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Security.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.ServiceModel.Web.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.ServiceProcess.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Text.Encoding.Extensions.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Text.Encoding.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Text.RegularExpressions.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Threading.Overlapped.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Threading.Tasks.Dataflow.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Threading.Tasks.Extensions.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Threading.Tasks.Parallel.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Threading.Tasks.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Threading.Thread.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Threading.ThreadPool.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Threading.Timer.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Threading.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Transactions.Local.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Transactions.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.ValueTuple.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Web.HttpUtility.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Web.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Windows.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Xml.Linq.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Xml.ReaderWriter.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Xml.Serialization.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Xml.XDocument.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Xml.XPath.XDocument.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Xml.XPath.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Xml.XmlDocument.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Xml.XmlSerializer.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.Xml.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/System.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/WindowsBase.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/mscorlib.dll",
+				"runtimes/win-x64/lib/netcoreapp2.2/netstandard.dll",
+				"runtimes/win-x64/native/Microsoft.DiaSymReader.Native.amd64.dll",
+				"runtimes/win-x64/native/System.Private.CoreLib.dll",
+				"runtimes/win-x64/native/api-ms-win-core-console-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-core-datetime-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-core-debug-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-core-errorhandling-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-core-file-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-core-file-l1-2-0.dll",
+				"runtimes/win-x64/native/api-ms-win-core-file-l2-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-core-handle-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-core-heap-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-core-interlocked-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-core-libraryloader-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-core-localization-l1-2-0.dll",
+				"runtimes/win-x64/native/api-ms-win-core-memory-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-core-namedpipe-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-core-processenvironment-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-core-processthreads-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-core-processthreads-l1-1-1.dll",
+				"runtimes/win-x64/native/api-ms-win-core-profile-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-core-rtlsupport-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-core-string-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-core-synch-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-core-synch-l1-2-0.dll",
+				"runtimes/win-x64/native/api-ms-win-core-sysinfo-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-core-timezone-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-core-util-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-crt-conio-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-crt-convert-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-crt-environment-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-crt-filesystem-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-crt-heap-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-crt-locale-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-crt-math-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-crt-multibyte-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-crt-private-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-crt-process-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-crt-runtime-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-crt-stdio-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-crt-string-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-crt-time-l1-1-0.dll",
+				"runtimes/win-x64/native/api-ms-win-crt-utility-l1-1-0.dll",
+				"runtimes/win-x64/native/clrcompression.dll",
+				"runtimes/win-x64/native/clretwrc.dll",
+				"runtimes/win-x64/native/clrjit.dll",
+				"runtimes/win-x64/native/coreclr.dll",
+				"runtimes/win-x64/native/dbgshim.dll",
+				"runtimes/win-x64/native/mscordaccore.dll",
+				"runtimes/win-x64/native/mscordaccore_amd64_amd64_4.6.28207.03.dll",
+				"runtimes/win-x64/native/mscordbi.dll",
+				"runtimes/win-x64/native/mscorrc.debug.dll",
+				"runtimes/win-x64/native/mscorrc.dll",
+				"runtimes/win-x64/native/sos.dll",
+				"runtimes/win-x64/native/sos_amd64_amd64_4.6.28207.03.dll",
+				"runtimes/win-x64/native/ucrtbase.dll",
+				"tools/crossgen.exe"
+			]
+		},
+		"runtime.win-x64.Microsoft.NETCore.DotNetAppHost/2.2.8": {
+			"sha512": "wppdPUeNyq/six2b/r1K/TR1e3Nw+QHNgkq+HNSCxVG4posR5vPNkWXM5n6YpPKQuMCzwOgSJBvhFuyJIepr3g==",
+			"type": "package",
+			"path": "runtime.win-x64.microsoft.netcore.dotnetapphost/2.2.8",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.TXT",
+				"THIRD-PARTY-NOTICES.TXT",
+				"runtime.win-x64.microsoft.netcore.dotnetapphost.2.2.8.nupkg.sha512",
+				"runtime.win-x64.microsoft.netcore.dotnetapphost.nuspec",
+				"runtimes/win-x64/native/apphost.exe",
+				"version.txt"
+			]
+		},
+		"runtime.win-x64.Microsoft.NETCore.DotNetHostPolicy/2.2.8": {
+			"sha512": "e5L+Ys9WG4vl5ryUTnBLpIg4lQzv6vUhAUm5SfuoFqM38Jut9INHhfrF4nOB3Ndpir6HO+5aHRabt7jRuLi7kQ==",
+			"type": "package",
+			"path": "runtime.win-x64.microsoft.netcore.dotnethostpolicy/2.2.8",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.TXT",
+				"THIRD-PARTY-NOTICES.TXT",
+				"runtime.win-x64.microsoft.netcore.dotnethostpolicy.2.2.8.nupkg.sha512",
+				"runtime.win-x64.microsoft.netcore.dotnethostpolicy.nuspec",
+				"runtimes/win-x64/native/hostpolicy.dll",
+				"version.txt"
+			]
+		},
+		"runtime.win-x64.Microsoft.NETCore.DotNetHostResolver/2.2.8": {
+			"sha512": "dc78seVTJPyjTSZLyL/X0Bl9h6e50s/nisE6UTH0a2angMtZ3Uw6+drAL/w/Ivk3HnH8WDAyb1nS/J3pvIVngg==",
+			"type": "package",
+			"path": "runtime.win-x64.microsoft.netcore.dotnethostresolver/2.2.8",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.TXT",
+				"THIRD-PARTY-NOTICES.TXT",
+				"runtime.win-x64.microsoft.netcore.dotnethostresolver.2.2.8.nupkg.sha512",
+				"runtime.win-x64.microsoft.netcore.dotnethostresolver.nuspec",
+				"runtimes/win-x64/native/hostfxr.dll",
+				"version.txt"
+			]
+		},
+		"SemanticVersioning/1.2.0": {
+			"sha512": "uYqkmqYk0D7pAShLoxCQsKX2aHj+D+Omf2g1NXoRADsJcFduwf1HLqiYY99vremiFobQfcsia948Gt3bG3ryFQ==",
+			"type": "package",
+			"path": "semanticversioning/1.2.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"lib/net35/SemVer.dll",
+				"lib/net45/SemVer.dll",
+				"lib/netstandard1.1/SemVer.dll",
+				"lib/netstandard2.0/SemVer.dll",
+				"semanticversioning.1.2.0.nupkg.sha512",
+				"semanticversioning.nuspec"
+			]
+		},
+		"StyleCop.Analyzers/1.0.2": {
+			"sha512": "3xD87lafnVhsSEtJKk50G7FGutvaXkFz4XrrLrxnk/DhZU42dnCGWUsvKuBv4mTS0XdIgTY88tLhxW/8Vi3Pow==",
+			"type": "package",
+			"path": "stylecop.analyzers/1.0.2",
+			"hasTools": true,
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"analyzers/dotnet/cs/StyleCop.Analyzers.CodeFixes.dll",
+				"analyzers/dotnet/cs/StyleCop.Analyzers.dll",
+				"stylecop.analyzers.1.0.2.nupkg.sha512",
+				"stylecop.analyzers.nuspec",
+				"tools/install.ps1",
+				"tools/uninstall.ps1"
+			]
+		},
+		"System.Collections/4.3.0": {
+			"sha512": "FlG6yWWvE4PdogtiUlJFLTh+Tpw9eI/T7WR/hV0pPnlLCvs1b6MsAMBRwmZ86uPfsSw6Do84H2JCLxKuB0UhOQ==",
+			"type": "package",
+			"path": "system.collections/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net45/_._",
+				"lib/portable-net45+win8+wp8+wpa81/_._",
+				"lib/win8/_._",
+				"lib/wp80/_._",
+				"lib/wpa81/_._",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net45/_._",
+				"ref/netcore50/System.Collections.dll",
+				"ref/netcore50/System.Collections.xml",
+				"ref/netcore50/de/System.Collections.xml",
+				"ref/netcore50/es/System.Collections.xml",
+				"ref/netcore50/fr/System.Collections.xml",
+				"ref/netcore50/it/System.Collections.xml",
+				"ref/netcore50/ja/System.Collections.xml",
+				"ref/netcore50/ko/System.Collections.xml",
+				"ref/netcore50/ru/System.Collections.xml",
+				"ref/netcore50/zh-hans/System.Collections.xml",
+				"ref/netcore50/zh-hant/System.Collections.xml",
+				"ref/netstandard1.0/System.Collections.dll",
+				"ref/netstandard1.0/System.Collections.xml",
+				"ref/netstandard1.0/de/System.Collections.xml",
+				"ref/netstandard1.0/es/System.Collections.xml",
+				"ref/netstandard1.0/fr/System.Collections.xml",
+				"ref/netstandard1.0/it/System.Collections.xml",
+				"ref/netstandard1.0/ja/System.Collections.xml",
+				"ref/netstandard1.0/ko/System.Collections.xml",
+				"ref/netstandard1.0/ru/System.Collections.xml",
+				"ref/netstandard1.0/zh-hans/System.Collections.xml",
+				"ref/netstandard1.0/zh-hant/System.Collections.xml",
+				"ref/netstandard1.3/System.Collections.dll",
+				"ref/netstandard1.3/System.Collections.xml",
+				"ref/netstandard1.3/de/System.Collections.xml",
+				"ref/netstandard1.3/es/System.Collections.xml",
+				"ref/netstandard1.3/fr/System.Collections.xml",
+				"ref/netstandard1.3/it/System.Collections.xml",
+				"ref/netstandard1.3/ja/System.Collections.xml",
+				"ref/netstandard1.3/ko/System.Collections.xml",
+				"ref/netstandard1.3/ru/System.Collections.xml",
+				"ref/netstandard1.3/zh-hans/System.Collections.xml",
+				"ref/netstandard1.3/zh-hant/System.Collections.xml",
+				"ref/portable-net45+win8+wp8+wpa81/_._",
+				"ref/win8/_._",
+				"ref/wp80/_._",
+				"ref/wpa81/_._",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"system.collections.4.3.0.nupkg.sha512",
+				"system.collections.nuspec"
+			]
+		},
+		"System.Composition.AttributedModel/1.4.0": {
+			"sha512": "nJpwGXzcKLPKHx1mvXbwtxU6o2tpzR529/pBe5NerXq75R5XcZHu6yMRUHcuroRor3+YlJi6l/l0klQfTYRzlQ==",
+			"type": "package",
+			"path": "system.composition.attributedmodel/1.4.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.TXT",
+				"THIRD-PARTY-NOTICES.TXT",
+				"lib/netstandard1.0/System.Composition.AttributedModel.dll",
+				"lib/netstandard2.0/System.Composition.AttributedModel.dll",
+				"lib/netstandard2.0/System.Composition.AttributedModel.xml",
+				"lib/portable-net45+win8+wp8+wpa81/System.Composition.AttributedModel.dll",
+				"system.composition.attributedmodel.1.4.0.nupkg.sha512",
+				"system.composition.attributedmodel.nuspec",
+				"useSharedDesignerContext.txt",
+				"version.txt"
+			]
+		},
+		"System.Composition.Convention/1.4.0": {
+			"sha512": "jEi0SwIWi4SilLi0i83U5kMM8A2aUIe8t8VmXvcoVHPKo0h0UeizLyIcQ6AgeJNVTmZ7uoRp5trWJDjtBeXsNA==",
+			"type": "package",
+			"path": "system.composition.convention/1.4.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.TXT",
+				"THIRD-PARTY-NOTICES.TXT",
+				"lib/netstandard1.0/System.Composition.Convention.dll",
+				"lib/netstandard2.0/System.Composition.Convention.dll",
+				"lib/netstandard2.0/System.Composition.Convention.xml",
+				"lib/portable-net45+win8+wp8+wpa81/System.Composition.Convention.dll",
+				"system.composition.convention.1.4.0.nupkg.sha512",
+				"system.composition.convention.nuspec",
+				"useSharedDesignerContext.txt",
+				"version.txt"
+			]
+		},
+		"System.Composition.Hosting/1.4.0": {
+			"sha512": "Ek5lYb00NCMzBecuV/zI4734PrrdoruiMbbR7S3joTOCSrLGND2Xt3aCp8Jg1ZvgA0SFH1Aov2/ZobLazwdd6g==",
+			"type": "package",
+			"path": "system.composition.hosting/1.4.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.TXT",
+				"THIRD-PARTY-NOTICES.TXT",
+				"lib/netstandard1.0/System.Composition.Hosting.dll",
+				"lib/netstandard2.0/System.Composition.Hosting.dll",
+				"lib/netstandard2.0/System.Composition.Hosting.xml",
+				"lib/portable-net45+win8+wp8+wpa81/System.Composition.Hosting.dll",
+				"system.composition.hosting.1.4.0.nupkg.sha512",
+				"system.composition.hosting.nuspec",
+				"useSharedDesignerContext.txt",
+				"version.txt"
+			]
+		},
+		"System.Composition.Runtime/1.4.0": {
+			"sha512": "2+RKi0a4qg7cO0w/6YS1B17MWhfSpDDUZ94ibQWhb9XM/L6MD9tcS1VgejpyPobVxsrlvh7rVtfkPPOetaKhRA==",
+			"type": "package",
+			"path": "system.composition.runtime/1.4.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.TXT",
+				"THIRD-PARTY-NOTICES.TXT",
+				"lib/netstandard1.0/System.Composition.Runtime.dll",
+				"lib/netstandard2.0/System.Composition.Runtime.dll",
+				"lib/netstandard2.0/System.Composition.Runtime.xml",
+				"lib/portable-net45+win8+wp8+wpa81/System.Composition.Runtime.dll",
+				"system.composition.runtime.1.4.0.nupkg.sha512",
+				"system.composition.runtime.nuspec",
+				"useSharedDesignerContext.txt",
+				"version.txt"
+			]
+		},
+		"System.Composition.TypedParts/1.4.0": {
+			"sha512": "azJdjOUf50JMUH+iactt0629uJg/ahoerGDnFDeBeQ1BrV6+4cpA5tiQIIZMSTpkj3fHlmdlJQBoRsJIqtNguw==",
+			"type": "package",
+			"path": "system.composition.typedparts/1.4.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.TXT",
+				"THIRD-PARTY-NOTICES.TXT",
+				"lib/netstandard1.0/System.Composition.TypedParts.dll",
+				"lib/netstandard2.0/System.Composition.TypedParts.dll",
+				"lib/netstandard2.0/System.Composition.TypedParts.xml",
+				"lib/portable-net45+win8+wp8+wpa81/System.Composition.TypedParts.dll",
+				"system.composition.typedparts.1.4.0.nupkg.sha512",
+				"system.composition.typedparts.nuspec",
+				"useSharedDesignerContext.txt",
+				"version.txt"
+			]
+		},
+		"System.Diagnostics.Debug/4.3.0": {
+			"sha512": "+JQLQwkH3DJpYPw2/IG8Adlo1XQ3z6TWHxo19/nKx1Gi2KZh56cghoyLGqiIuPpVT8hjDRjBz/MifnCFVBq5QQ==",
+			"type": "package",
+			"path": "system.diagnostics.debug/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net45/_._",
+				"lib/portable-net45+win8+wp8+wpa81/_._",
+				"lib/win8/_._",
+				"lib/wp80/_._",
+				"lib/wpa81/_._",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net45/_._",
+				"ref/netcore50/System.Diagnostics.Debug.dll",
+				"ref/netcore50/System.Diagnostics.Debug.xml",
+				"ref/netcore50/de/System.Diagnostics.Debug.xml",
+				"ref/netcore50/es/System.Diagnostics.Debug.xml",
+				"ref/netcore50/fr/System.Diagnostics.Debug.xml",
+				"ref/netcore50/it/System.Diagnostics.Debug.xml",
+				"ref/netcore50/ja/System.Diagnostics.Debug.xml",
+				"ref/netcore50/ko/System.Diagnostics.Debug.xml",
+				"ref/netcore50/ru/System.Diagnostics.Debug.xml",
+				"ref/netcore50/zh-hans/System.Diagnostics.Debug.xml",
+				"ref/netcore50/zh-hant/System.Diagnostics.Debug.xml",
+				"ref/netstandard1.0/System.Diagnostics.Debug.dll",
+				"ref/netstandard1.0/System.Diagnostics.Debug.xml",
+				"ref/netstandard1.0/de/System.Diagnostics.Debug.xml",
+				"ref/netstandard1.0/es/System.Diagnostics.Debug.xml",
+				"ref/netstandard1.0/fr/System.Diagnostics.Debug.xml",
+				"ref/netstandard1.0/it/System.Diagnostics.Debug.xml",
+				"ref/netstandard1.0/ja/System.Diagnostics.Debug.xml",
+				"ref/netstandard1.0/ko/System.Diagnostics.Debug.xml",
+				"ref/netstandard1.0/ru/System.Diagnostics.Debug.xml",
+				"ref/netstandard1.0/zh-hans/System.Diagnostics.Debug.xml",
+				"ref/netstandard1.0/zh-hant/System.Diagnostics.Debug.xml",
+				"ref/netstandard1.3/System.Diagnostics.Debug.dll",
+				"ref/netstandard1.3/System.Diagnostics.Debug.xml",
+				"ref/netstandard1.3/de/System.Diagnostics.Debug.xml",
+				"ref/netstandard1.3/es/System.Diagnostics.Debug.xml",
+				"ref/netstandard1.3/fr/System.Diagnostics.Debug.xml",
+				"ref/netstandard1.3/it/System.Diagnostics.Debug.xml",
+				"ref/netstandard1.3/ja/System.Diagnostics.Debug.xml",
+				"ref/netstandard1.3/ko/System.Diagnostics.Debug.xml",
+				"ref/netstandard1.3/ru/System.Diagnostics.Debug.xml",
+				"ref/netstandard1.3/zh-hans/System.Diagnostics.Debug.xml",
+				"ref/netstandard1.3/zh-hant/System.Diagnostics.Debug.xml",
+				"ref/portable-net45+win8+wp8+wpa81/_._",
+				"ref/win8/_._",
+				"ref/wp80/_._",
+				"ref/wpa81/_._",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"system.diagnostics.debug.4.3.0.nupkg.sha512",
+				"system.diagnostics.debug.nuspec"
+			]
+		},
+		"System.Diagnostics.Process/4.3.0": {
+			"sha512": "I2WdNpdNnPIXkCIkKUAB+63Ljk0y5aCQI+dA6G+9dEDp5rYfHe9KuFjJCNYj4FHnyqOPBsyyNAY/EGAcsAB52w==",
+			"type": "package",
+			"path": "system.diagnostics.process/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net46/System.Diagnostics.Process.dll",
+				"lib/net461/System.Diagnostics.Process.dll",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net46/System.Diagnostics.Process.dll",
+				"ref/net461/System.Diagnostics.Process.dll",
+				"ref/netstandard1.3/System.Diagnostics.Process.dll",
+				"ref/netstandard1.3/System.Diagnostics.Process.xml",
+				"ref/netstandard1.3/de/System.Diagnostics.Process.xml",
+				"ref/netstandard1.3/es/System.Diagnostics.Process.xml",
+				"ref/netstandard1.3/fr/System.Diagnostics.Process.xml",
+				"ref/netstandard1.3/it/System.Diagnostics.Process.xml",
+				"ref/netstandard1.3/ja/System.Diagnostics.Process.xml",
+				"ref/netstandard1.3/ko/System.Diagnostics.Process.xml",
+				"ref/netstandard1.3/ru/System.Diagnostics.Process.xml",
+				"ref/netstandard1.3/zh-hans/System.Diagnostics.Process.xml",
+				"ref/netstandard1.3/zh-hant/System.Diagnostics.Process.xml",
+				"ref/netstandard1.4/System.Diagnostics.Process.dll",
+				"ref/netstandard1.4/System.Diagnostics.Process.xml",
+				"ref/netstandard1.4/de/System.Diagnostics.Process.xml",
+				"ref/netstandard1.4/es/System.Diagnostics.Process.xml",
+				"ref/netstandard1.4/fr/System.Diagnostics.Process.xml",
+				"ref/netstandard1.4/it/System.Diagnostics.Process.xml",
+				"ref/netstandard1.4/ja/System.Diagnostics.Process.xml",
+				"ref/netstandard1.4/ko/System.Diagnostics.Process.xml",
+				"ref/netstandard1.4/ru/System.Diagnostics.Process.xml",
+				"ref/netstandard1.4/zh-hans/System.Diagnostics.Process.xml",
+				"ref/netstandard1.4/zh-hant/System.Diagnostics.Process.xml",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"runtimes/linux/lib/netstandard1.4/System.Diagnostics.Process.dll",
+				"runtimes/osx/lib/netstandard1.4/System.Diagnostics.Process.dll",
+				"runtimes/win/lib/net46/System.Diagnostics.Process.dll",
+				"runtimes/win/lib/net461/System.Diagnostics.Process.dll",
+				"runtimes/win/lib/netstandard1.4/System.Diagnostics.Process.dll",
+				"runtimes/win7/lib/netcore50/_._",
+				"system.diagnostics.process.4.3.0.nupkg.sha512",
+				"system.diagnostics.process.nuspec"
+			]
+		},
+		"System.Dynamic.Runtime/4.3.0": {
+			"sha512": "l5L81doUOuL76M73yPKLoZkRx4nJHVgdE8WAB4B8Pu9aKYXehBJXQCSfY++PqEiM/1HiSbj1UBaQPm6NZbrzMA==",
+			"type": "package",
+			"path": "system.dynamic.runtime/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net45/_._",
+				"lib/netcore50/System.Dynamic.Runtime.dll",
+				"lib/netstandard1.3/System.Dynamic.Runtime.dll",
+				"lib/portable-net45+win8+wp8+wpa81/_._",
+				"lib/win8/_._",
+				"lib/wp80/_._",
+				"lib/wpa81/_._",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net45/_._",
+				"ref/netcore50/System.Dynamic.Runtime.dll",
+				"ref/netcore50/System.Dynamic.Runtime.xml",
+				"ref/netcore50/de/System.Dynamic.Runtime.xml",
+				"ref/netcore50/es/System.Dynamic.Runtime.xml",
+				"ref/netcore50/fr/System.Dynamic.Runtime.xml",
+				"ref/netcore50/it/System.Dynamic.Runtime.xml",
+				"ref/netcore50/ja/System.Dynamic.Runtime.xml",
+				"ref/netcore50/ko/System.Dynamic.Runtime.xml",
+				"ref/netcore50/ru/System.Dynamic.Runtime.xml",
+				"ref/netcore50/zh-hans/System.Dynamic.Runtime.xml",
+				"ref/netcore50/zh-hant/System.Dynamic.Runtime.xml",
+				"ref/netstandard1.0/System.Dynamic.Runtime.dll",
+				"ref/netstandard1.0/System.Dynamic.Runtime.xml",
+				"ref/netstandard1.0/de/System.Dynamic.Runtime.xml",
+				"ref/netstandard1.0/es/System.Dynamic.Runtime.xml",
+				"ref/netstandard1.0/fr/System.Dynamic.Runtime.xml",
+				"ref/netstandard1.0/it/System.Dynamic.Runtime.xml",
+				"ref/netstandard1.0/ja/System.Dynamic.Runtime.xml",
+				"ref/netstandard1.0/ko/System.Dynamic.Runtime.xml",
+				"ref/netstandard1.0/ru/System.Dynamic.Runtime.xml",
+				"ref/netstandard1.0/zh-hans/System.Dynamic.Runtime.xml",
+				"ref/netstandard1.0/zh-hant/System.Dynamic.Runtime.xml",
+				"ref/netstandard1.3/System.Dynamic.Runtime.dll",
+				"ref/netstandard1.3/System.Dynamic.Runtime.xml",
+				"ref/netstandard1.3/de/System.Dynamic.Runtime.xml",
+				"ref/netstandard1.3/es/System.Dynamic.Runtime.xml",
+				"ref/netstandard1.3/fr/System.Dynamic.Runtime.xml",
+				"ref/netstandard1.3/it/System.Dynamic.Runtime.xml",
+				"ref/netstandard1.3/ja/System.Dynamic.Runtime.xml",
+				"ref/netstandard1.3/ko/System.Dynamic.Runtime.xml",
+				"ref/netstandard1.3/ru/System.Dynamic.Runtime.xml",
+				"ref/netstandard1.3/zh-hans/System.Dynamic.Runtime.xml",
+				"ref/netstandard1.3/zh-hant/System.Dynamic.Runtime.xml",
+				"ref/portable-net45+win8+wp8+wpa81/_._",
+				"ref/win8/_._",
+				"ref/wp80/_._",
+				"ref/wpa81/_._",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"runtimes/aot/lib/netcore50/System.Dynamic.Runtime.dll",
+				"system.dynamic.runtime.4.3.0.nupkg.sha512",
+				"system.dynamic.runtime.nuspec"
+			]
+		},
+		"System.Globalization/4.3.0": {
+			"sha512": "WXTmCgGfVvz26QPu0iO68Yz61JP0H2bgTTPy1c59dPiL2MtBjuNmAei2REyp/hJQ8Qzimm9vu8V42jX+XBUw8A==",
+			"type": "package",
+			"path": "system.globalization/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net45/_._",
+				"lib/portable-net45+win8+wp8+wpa81/_._",
+				"lib/win8/_._",
+				"lib/wp80/_._",
+				"lib/wpa81/_._",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net45/_._",
+				"ref/netcore50/System.Globalization.dll",
+				"ref/netcore50/System.Globalization.xml",
+				"ref/netcore50/de/System.Globalization.xml",
+				"ref/netcore50/es/System.Globalization.xml",
+				"ref/netcore50/fr/System.Globalization.xml",
+				"ref/netcore50/it/System.Globalization.xml",
+				"ref/netcore50/ja/System.Globalization.xml",
+				"ref/netcore50/ko/System.Globalization.xml",
+				"ref/netcore50/ru/System.Globalization.xml",
+				"ref/netcore50/zh-hans/System.Globalization.xml",
+				"ref/netcore50/zh-hant/System.Globalization.xml",
+				"ref/netstandard1.0/System.Globalization.dll",
+				"ref/netstandard1.0/System.Globalization.xml",
+				"ref/netstandard1.0/de/System.Globalization.xml",
+				"ref/netstandard1.0/es/System.Globalization.xml",
+				"ref/netstandard1.0/fr/System.Globalization.xml",
+				"ref/netstandard1.0/it/System.Globalization.xml",
+				"ref/netstandard1.0/ja/System.Globalization.xml",
+				"ref/netstandard1.0/ko/System.Globalization.xml",
+				"ref/netstandard1.0/ru/System.Globalization.xml",
+				"ref/netstandard1.0/zh-hans/System.Globalization.xml",
+				"ref/netstandard1.0/zh-hant/System.Globalization.xml",
+				"ref/netstandard1.3/System.Globalization.dll",
+				"ref/netstandard1.3/System.Globalization.xml",
+				"ref/netstandard1.3/de/System.Globalization.xml",
+				"ref/netstandard1.3/es/System.Globalization.xml",
+				"ref/netstandard1.3/fr/System.Globalization.xml",
+				"ref/netstandard1.3/it/System.Globalization.xml",
+				"ref/netstandard1.3/ja/System.Globalization.xml",
+				"ref/netstandard1.3/ko/System.Globalization.xml",
+				"ref/netstandard1.3/ru/System.Globalization.xml",
+				"ref/netstandard1.3/zh-hans/System.Globalization.xml",
+				"ref/netstandard1.3/zh-hant/System.Globalization.xml",
+				"ref/portable-net45+win8+wp8+wpa81/_._",
+				"ref/win8/_._",
+				"ref/wp80/_._",
+				"ref/wpa81/_._",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"system.globalization.4.3.0.nupkg.sha512",
+				"system.globalization.nuspec"
+			]
+		},
+		"System.IO/4.3.0": {
+			"sha512": "w9k/LKzv6pDgC/HUgJkNMVCC0DZZ6yaYoj0H2WNK2VOA7a0di04PDnprGU0PIVQi+3d1RIoBvp9Eom2tbKIgkA==",
+			"type": "package",
+			"path": "system.io/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net45/_._",
+				"lib/net462/System.IO.dll",
+				"lib/portable-net45+win8+wp8+wpa81/_._",
+				"lib/win8/_._",
+				"lib/wp80/_._",
+				"lib/wpa81/_._",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net45/_._",
+				"ref/net462/System.IO.dll",
+				"ref/netcore50/System.IO.dll",
+				"ref/netcore50/System.IO.xml",
+				"ref/netcore50/de/System.IO.xml",
+				"ref/netcore50/es/System.IO.xml",
+				"ref/netcore50/fr/System.IO.xml",
+				"ref/netcore50/it/System.IO.xml",
+				"ref/netcore50/ja/System.IO.xml",
+				"ref/netcore50/ko/System.IO.xml",
+				"ref/netcore50/ru/System.IO.xml",
+				"ref/netcore50/zh-hans/System.IO.xml",
+				"ref/netcore50/zh-hant/System.IO.xml",
+				"ref/netstandard1.0/System.IO.dll",
+				"ref/netstandard1.0/System.IO.xml",
+				"ref/netstandard1.0/de/System.IO.xml",
+				"ref/netstandard1.0/es/System.IO.xml",
+				"ref/netstandard1.0/fr/System.IO.xml",
+				"ref/netstandard1.0/it/System.IO.xml",
+				"ref/netstandard1.0/ja/System.IO.xml",
+				"ref/netstandard1.0/ko/System.IO.xml",
+				"ref/netstandard1.0/ru/System.IO.xml",
+				"ref/netstandard1.0/zh-hans/System.IO.xml",
+				"ref/netstandard1.0/zh-hant/System.IO.xml",
+				"ref/netstandard1.3/System.IO.dll",
+				"ref/netstandard1.3/System.IO.xml",
+				"ref/netstandard1.3/de/System.IO.xml",
+				"ref/netstandard1.3/es/System.IO.xml",
+				"ref/netstandard1.3/fr/System.IO.xml",
+				"ref/netstandard1.3/it/System.IO.xml",
+				"ref/netstandard1.3/ja/System.IO.xml",
+				"ref/netstandard1.3/ko/System.IO.xml",
+				"ref/netstandard1.3/ru/System.IO.xml",
+				"ref/netstandard1.3/zh-hans/System.IO.xml",
+				"ref/netstandard1.3/zh-hant/System.IO.xml",
+				"ref/netstandard1.5/System.IO.dll",
+				"ref/netstandard1.5/System.IO.xml",
+				"ref/netstandard1.5/de/System.IO.xml",
+				"ref/netstandard1.5/es/System.IO.xml",
+				"ref/netstandard1.5/fr/System.IO.xml",
+				"ref/netstandard1.5/it/System.IO.xml",
+				"ref/netstandard1.5/ja/System.IO.xml",
+				"ref/netstandard1.5/ko/System.IO.xml",
+				"ref/netstandard1.5/ru/System.IO.xml",
+				"ref/netstandard1.5/zh-hans/System.IO.xml",
+				"ref/netstandard1.5/zh-hant/System.IO.xml",
+				"ref/portable-net45+win8+wp8+wpa81/_._",
+				"ref/win8/_._",
+				"ref/wp80/_._",
+				"ref/wpa81/_._",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"system.io.4.3.0.nupkg.sha512",
+				"system.io.nuspec"
+			]
+		},
+		"System.IO.FileSystem/4.3.0": {
+			"sha512": "QQnhkv65V0zVq5cppNWTicGvyV9HtGEMpXJ+TQKG5K/uhaStgyDHFmq3R3Gmg1KNOqMLdrVOhsrUxGv+C3M+iw==",
+			"type": "package",
+			"path": "system.io.filesystem/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net46/System.IO.FileSystem.dll",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net46/System.IO.FileSystem.dll",
+				"ref/netstandard1.3/System.IO.FileSystem.dll",
+				"ref/netstandard1.3/System.IO.FileSystem.xml",
+				"ref/netstandard1.3/de/System.IO.FileSystem.xml",
+				"ref/netstandard1.3/es/System.IO.FileSystem.xml",
+				"ref/netstandard1.3/fr/System.IO.FileSystem.xml",
+				"ref/netstandard1.3/it/System.IO.FileSystem.xml",
+				"ref/netstandard1.3/ja/System.IO.FileSystem.xml",
+				"ref/netstandard1.3/ko/System.IO.FileSystem.xml",
+				"ref/netstandard1.3/ru/System.IO.FileSystem.xml",
+				"ref/netstandard1.3/zh-hans/System.IO.FileSystem.xml",
+				"ref/netstandard1.3/zh-hant/System.IO.FileSystem.xml",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"system.io.filesystem.4.3.0.nupkg.sha512",
+				"system.io.filesystem.nuspec"
+			]
+		},
+		"System.IO.FileSystem.Primitives/4.3.0": {
+			"sha512": "NrkZFVXkZHpfgN+2iPZWKNaACNRRXPbL0Lm2tuGIj2qVGJlNlQ/CGQ/m9iR2jUYmV98trqT5xvCzQSr3Ab8RwQ==",
+			"type": "package",
+			"path": "system.io.filesystem.primitives/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net46/System.IO.FileSystem.Primitives.dll",
+				"lib/netstandard1.3/System.IO.FileSystem.Primitives.dll",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net46/System.IO.FileSystem.Primitives.dll",
+				"ref/netstandard1.3/System.IO.FileSystem.Primitives.dll",
+				"ref/netstandard1.3/System.IO.FileSystem.Primitives.xml",
+				"ref/netstandard1.3/de/System.IO.FileSystem.Primitives.xml",
+				"ref/netstandard1.3/es/System.IO.FileSystem.Primitives.xml",
+				"ref/netstandard1.3/fr/System.IO.FileSystem.Primitives.xml",
+				"ref/netstandard1.3/it/System.IO.FileSystem.Primitives.xml",
+				"ref/netstandard1.3/ja/System.IO.FileSystem.Primitives.xml",
+				"ref/netstandard1.3/ko/System.IO.FileSystem.Primitives.xml",
+				"ref/netstandard1.3/ru/System.IO.FileSystem.Primitives.xml",
+				"ref/netstandard1.3/zh-hans/System.IO.FileSystem.Primitives.xml",
+				"ref/netstandard1.3/zh-hant/System.IO.FileSystem.Primitives.xml",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"system.io.filesystem.primitives.4.3.0.nupkg.sha512",
+				"system.io.filesystem.primitives.nuspec"
+			]
+		},
+		"System.Linq/4.3.0": {
+			"sha512": "dQA9IhOzcYRz2q90OShLjwfeemcUxsiagzikDibqY39PFxxcZvi5FMxyLG8E94V7L3OvL6Eabfl6GtKHKgj9YQ==",
+			"type": "package",
+			"path": "system.linq/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net45/_._",
+				"lib/net463/System.Linq.dll",
+				"lib/netcore50/System.Linq.dll",
+				"lib/netstandard1.6/System.Linq.dll",
+				"lib/portable-net45+win8+wp8+wpa81/_._",
+				"lib/win8/_._",
+				"lib/wp80/_._",
+				"lib/wpa81/_._",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net45/_._",
+				"ref/net463/System.Linq.dll",
+				"ref/netcore50/System.Linq.dll",
+				"ref/netcore50/System.Linq.xml",
+				"ref/netcore50/de/System.Linq.xml",
+				"ref/netcore50/es/System.Linq.xml",
+				"ref/netcore50/fr/System.Linq.xml",
+				"ref/netcore50/it/System.Linq.xml",
+				"ref/netcore50/ja/System.Linq.xml",
+				"ref/netcore50/ko/System.Linq.xml",
+				"ref/netcore50/ru/System.Linq.xml",
+				"ref/netcore50/zh-hans/System.Linq.xml",
+				"ref/netcore50/zh-hant/System.Linq.xml",
+				"ref/netstandard1.0/System.Linq.dll",
+				"ref/netstandard1.0/System.Linq.xml",
+				"ref/netstandard1.0/de/System.Linq.xml",
+				"ref/netstandard1.0/es/System.Linq.xml",
+				"ref/netstandard1.0/fr/System.Linq.xml",
+				"ref/netstandard1.0/it/System.Linq.xml",
+				"ref/netstandard1.0/ja/System.Linq.xml",
+				"ref/netstandard1.0/ko/System.Linq.xml",
+				"ref/netstandard1.0/ru/System.Linq.xml",
+				"ref/netstandard1.0/zh-hans/System.Linq.xml",
+				"ref/netstandard1.0/zh-hant/System.Linq.xml",
+				"ref/netstandard1.6/System.Linq.dll",
+				"ref/netstandard1.6/System.Linq.xml",
+				"ref/netstandard1.6/de/System.Linq.xml",
+				"ref/netstandard1.6/es/System.Linq.xml",
+				"ref/netstandard1.6/fr/System.Linq.xml",
+				"ref/netstandard1.6/it/System.Linq.xml",
+				"ref/netstandard1.6/ja/System.Linq.xml",
+				"ref/netstandard1.6/ko/System.Linq.xml",
+				"ref/netstandard1.6/ru/System.Linq.xml",
+				"ref/netstandard1.6/zh-hans/System.Linq.xml",
+				"ref/netstandard1.6/zh-hant/System.Linq.xml",
+				"ref/portable-net45+win8+wp8+wpa81/_._",
+				"ref/win8/_._",
+				"ref/wp80/_._",
+				"ref/wpa81/_._",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"system.linq.4.3.0.nupkg.sha512",
+				"system.linq.nuspec"
+			]
+		},
+		"System.Linq.Expressions/4.3.0": {
+			"sha512": "zUg8OF7g77ecmKolYZDseScBDzMZTZ2MtGnVpjQavuhHH1dPDF8dvZVgPxbMWG3vSA3j1hrUibFCgs5IraCZ2w==",
+			"type": "package",
+			"path": "system.linq.expressions/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net45/_._",
+				"lib/net463/System.Linq.Expressions.dll",
+				"lib/netcore50/System.Linq.Expressions.dll",
+				"lib/netstandard1.6/System.Linq.Expressions.dll",
+				"lib/portable-net45+win8+wp8+wpa81/_._",
+				"lib/win8/_._",
+				"lib/wp80/_._",
+				"lib/wpa81/_._",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net45/_._",
+				"ref/net463/System.Linq.Expressions.dll",
+				"ref/netcore50/System.Linq.Expressions.dll",
+				"ref/netcore50/System.Linq.Expressions.xml",
+				"ref/netcore50/de/System.Linq.Expressions.xml",
+				"ref/netcore50/es/System.Linq.Expressions.xml",
+				"ref/netcore50/fr/System.Linq.Expressions.xml",
+				"ref/netcore50/it/System.Linq.Expressions.xml",
+				"ref/netcore50/ja/System.Linq.Expressions.xml",
+				"ref/netcore50/ko/System.Linq.Expressions.xml",
+				"ref/netcore50/ru/System.Linq.Expressions.xml",
+				"ref/netcore50/zh-hans/System.Linq.Expressions.xml",
+				"ref/netcore50/zh-hant/System.Linq.Expressions.xml",
+				"ref/netstandard1.0/System.Linq.Expressions.dll",
+				"ref/netstandard1.0/System.Linq.Expressions.xml",
+				"ref/netstandard1.0/de/System.Linq.Expressions.xml",
+				"ref/netstandard1.0/es/System.Linq.Expressions.xml",
+				"ref/netstandard1.0/fr/System.Linq.Expressions.xml",
+				"ref/netstandard1.0/it/System.Linq.Expressions.xml",
+				"ref/netstandard1.0/ja/System.Linq.Expressions.xml",
+				"ref/netstandard1.0/ko/System.Linq.Expressions.xml",
+				"ref/netstandard1.0/ru/System.Linq.Expressions.xml",
+				"ref/netstandard1.0/zh-hans/System.Linq.Expressions.xml",
+				"ref/netstandard1.0/zh-hant/System.Linq.Expressions.xml",
+				"ref/netstandard1.3/System.Linq.Expressions.dll",
+				"ref/netstandard1.3/System.Linq.Expressions.xml",
+				"ref/netstandard1.3/de/System.Linq.Expressions.xml",
+				"ref/netstandard1.3/es/System.Linq.Expressions.xml",
+				"ref/netstandard1.3/fr/System.Linq.Expressions.xml",
+				"ref/netstandard1.3/it/System.Linq.Expressions.xml",
+				"ref/netstandard1.3/ja/System.Linq.Expressions.xml",
+				"ref/netstandard1.3/ko/System.Linq.Expressions.xml",
+				"ref/netstandard1.3/ru/System.Linq.Expressions.xml",
+				"ref/netstandard1.3/zh-hans/System.Linq.Expressions.xml",
+				"ref/netstandard1.3/zh-hant/System.Linq.Expressions.xml",
+				"ref/netstandard1.6/System.Linq.Expressions.dll",
+				"ref/netstandard1.6/System.Linq.Expressions.xml",
+				"ref/netstandard1.6/de/System.Linq.Expressions.xml",
+				"ref/netstandard1.6/es/System.Linq.Expressions.xml",
+				"ref/netstandard1.6/fr/System.Linq.Expressions.xml",
+				"ref/netstandard1.6/it/System.Linq.Expressions.xml",
+				"ref/netstandard1.6/ja/System.Linq.Expressions.xml",
+				"ref/netstandard1.6/ko/System.Linq.Expressions.xml",
+				"ref/netstandard1.6/ru/System.Linq.Expressions.xml",
+				"ref/netstandard1.6/zh-hans/System.Linq.Expressions.xml",
+				"ref/netstandard1.6/zh-hant/System.Linq.Expressions.xml",
+				"ref/portable-net45+win8+wp8+wpa81/_._",
+				"ref/win8/_._",
+				"ref/wp80/_._",
+				"ref/wpa81/_._",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"runtimes/aot/lib/netcore50/System.Linq.Expressions.dll",
+				"system.linq.expressions.4.3.0.nupkg.sha512",
+				"system.linq.expressions.nuspec"
+			]
+		},
+		"System.ObjectModel/4.3.0": {
+			"sha512": "QiHZaRQrXrIbx1wh6/kiKQ7Oc5yqZpSx1XckIB1KAybkhN3l/di0+P6v69XfpGw4WtPjBS7370wXrTVv5oeF5Q==",
+			"type": "package",
+			"path": "system.objectmodel/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net45/_._",
+				"lib/netcore50/System.ObjectModel.dll",
+				"lib/netstandard1.3/System.ObjectModel.dll",
+				"lib/portable-net45+win8+wp8+wpa81/_._",
+				"lib/win8/_._",
+				"lib/wp80/_._",
+				"lib/wpa81/_._",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net45/_._",
+				"ref/netcore50/System.ObjectModel.dll",
+				"ref/netcore50/System.ObjectModel.xml",
+				"ref/netcore50/de/System.ObjectModel.xml",
+				"ref/netcore50/es/System.ObjectModel.xml",
+				"ref/netcore50/fr/System.ObjectModel.xml",
+				"ref/netcore50/it/System.ObjectModel.xml",
+				"ref/netcore50/ja/System.ObjectModel.xml",
+				"ref/netcore50/ko/System.ObjectModel.xml",
+				"ref/netcore50/ru/System.ObjectModel.xml",
+				"ref/netcore50/zh-hans/System.ObjectModel.xml",
+				"ref/netcore50/zh-hant/System.ObjectModel.xml",
+				"ref/netstandard1.0/System.ObjectModel.dll",
+				"ref/netstandard1.0/System.ObjectModel.xml",
+				"ref/netstandard1.0/de/System.ObjectModel.xml",
+				"ref/netstandard1.0/es/System.ObjectModel.xml",
+				"ref/netstandard1.0/fr/System.ObjectModel.xml",
+				"ref/netstandard1.0/it/System.ObjectModel.xml",
+				"ref/netstandard1.0/ja/System.ObjectModel.xml",
+				"ref/netstandard1.0/ko/System.ObjectModel.xml",
+				"ref/netstandard1.0/ru/System.ObjectModel.xml",
+				"ref/netstandard1.0/zh-hans/System.ObjectModel.xml",
+				"ref/netstandard1.0/zh-hant/System.ObjectModel.xml",
+				"ref/netstandard1.3/System.ObjectModel.dll",
+				"ref/netstandard1.3/System.ObjectModel.xml",
+				"ref/netstandard1.3/de/System.ObjectModel.xml",
+				"ref/netstandard1.3/es/System.ObjectModel.xml",
+				"ref/netstandard1.3/fr/System.ObjectModel.xml",
+				"ref/netstandard1.3/it/System.ObjectModel.xml",
+				"ref/netstandard1.3/ja/System.ObjectModel.xml",
+				"ref/netstandard1.3/ko/System.ObjectModel.xml",
+				"ref/netstandard1.3/ru/System.ObjectModel.xml",
+				"ref/netstandard1.3/zh-hans/System.ObjectModel.xml",
+				"ref/netstandard1.3/zh-hant/System.ObjectModel.xml",
+				"ref/portable-net45+win8+wp8+wpa81/_._",
+				"ref/win8/_._",
+				"ref/wp80/_._",
+				"ref/wpa81/_._",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"system.objectmodel.4.3.0.nupkg.sha512",
+				"system.objectmodel.nuspec"
+			]
+		},
+		"System.Reactive/4.1.2": {
+			"sha512": "QRxhdvoP51UuXZbSzcIiFu3/MCSAlR8rz3G/XMcm3b+a2zOC5ropDVaZrjXAO+7VF04Aqk4MCcLEdhxTfWVlZw==",
+			"type": "package",
+			"path": "system.reactive/4.1.2",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"lib/net46/System.Reactive.dll",
+				"lib/net46/System.Reactive.xml",
+				"lib/netstandard2.0/System.Reactive.dll",
+				"lib/netstandard2.0/System.Reactive.xml",
+				"lib/uap10.0.16299/System.Reactive.dll",
+				"lib/uap10.0.16299/System.Reactive.pri",
+				"lib/uap10.0.16299/System.Reactive.xml",
+				"lib/uap10.0/System.Reactive.dll",
+				"lib/uap10.0/System.Reactive.pri",
+				"lib/uap10.0/System.Reactive.xml",
+				"system.reactive.4.1.2.nupkg.sha512",
+				"system.reactive.nuspec"
+			]
+		},
+		"System.Reflection/4.3.0": {
+			"sha512": "vVNX6iFKa2XrZUoZWE8AFDpInpdiqwt5HLrsb1YHLpj+cylnPySK1QOIeWMOG7WOCoQg341bQQ5yXmeKNUkIag==",
+			"type": "package",
+			"path": "system.reflection/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net45/_._",
+				"lib/net462/System.Reflection.dll",
+				"lib/portable-net45+win8+wp8+wpa81/_._",
+				"lib/win8/_._",
+				"lib/wp80/_._",
+				"lib/wpa81/_._",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net45/_._",
+				"ref/net462/System.Reflection.dll",
+				"ref/netcore50/System.Reflection.dll",
+				"ref/netcore50/System.Reflection.xml",
+				"ref/netcore50/de/System.Reflection.xml",
+				"ref/netcore50/es/System.Reflection.xml",
+				"ref/netcore50/fr/System.Reflection.xml",
+				"ref/netcore50/it/System.Reflection.xml",
+				"ref/netcore50/ja/System.Reflection.xml",
+				"ref/netcore50/ko/System.Reflection.xml",
+				"ref/netcore50/ru/System.Reflection.xml",
+				"ref/netcore50/zh-hans/System.Reflection.xml",
+				"ref/netcore50/zh-hant/System.Reflection.xml",
+				"ref/netstandard1.0/System.Reflection.dll",
+				"ref/netstandard1.0/System.Reflection.xml",
+				"ref/netstandard1.0/de/System.Reflection.xml",
+				"ref/netstandard1.0/es/System.Reflection.xml",
+				"ref/netstandard1.0/fr/System.Reflection.xml",
+				"ref/netstandard1.0/it/System.Reflection.xml",
+				"ref/netstandard1.0/ja/System.Reflection.xml",
+				"ref/netstandard1.0/ko/System.Reflection.xml",
+				"ref/netstandard1.0/ru/System.Reflection.xml",
+				"ref/netstandard1.0/zh-hans/System.Reflection.xml",
+				"ref/netstandard1.0/zh-hant/System.Reflection.xml",
+				"ref/netstandard1.3/System.Reflection.dll",
+				"ref/netstandard1.3/System.Reflection.xml",
+				"ref/netstandard1.3/de/System.Reflection.xml",
+				"ref/netstandard1.3/es/System.Reflection.xml",
+				"ref/netstandard1.3/fr/System.Reflection.xml",
+				"ref/netstandard1.3/it/System.Reflection.xml",
+				"ref/netstandard1.3/ja/System.Reflection.xml",
+				"ref/netstandard1.3/ko/System.Reflection.xml",
+				"ref/netstandard1.3/ru/System.Reflection.xml",
+				"ref/netstandard1.3/zh-hans/System.Reflection.xml",
+				"ref/netstandard1.3/zh-hant/System.Reflection.xml",
+				"ref/netstandard1.5/System.Reflection.dll",
+				"ref/netstandard1.5/System.Reflection.xml",
+				"ref/netstandard1.5/de/System.Reflection.xml",
+				"ref/netstandard1.5/es/System.Reflection.xml",
+				"ref/netstandard1.5/fr/System.Reflection.xml",
+				"ref/netstandard1.5/it/System.Reflection.xml",
+				"ref/netstandard1.5/ja/System.Reflection.xml",
+				"ref/netstandard1.5/ko/System.Reflection.xml",
+				"ref/netstandard1.5/ru/System.Reflection.xml",
+				"ref/netstandard1.5/zh-hans/System.Reflection.xml",
+				"ref/netstandard1.5/zh-hant/System.Reflection.xml",
+				"ref/portable-net45+win8+wp8+wpa81/_._",
+				"ref/win8/_._",
+				"ref/wp80/_._",
+				"ref/wpa81/_._",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"system.reflection.4.3.0.nupkg.sha512",
+				"system.reflection.nuspec"
+			]
+		},
+		"System.Reflection.Emit/4.3.0": {
+			"sha512": "3zIG0WX7VfgjYmX5BjmLHCvLlWFpbxgIxyJTmpcbdz7FUqQvbVSs9hfOfl97ud26pAGKglBiy6L1HfSzoSxHsQ==",
+			"type": "package",
+			"path": "system.reflection.emit/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/monotouch10/_._",
+				"lib/net45/_._",
+				"lib/netcore50/System.Reflection.Emit.dll",
+				"lib/netstandard1.3/System.Reflection.Emit.dll",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/net45/_._",
+				"ref/netstandard1.1/System.Reflection.Emit.dll",
+				"ref/netstandard1.1/System.Reflection.Emit.xml",
+				"ref/netstandard1.1/de/System.Reflection.Emit.xml",
+				"ref/netstandard1.1/es/System.Reflection.Emit.xml",
+				"ref/netstandard1.1/fr/System.Reflection.Emit.xml",
+				"ref/netstandard1.1/it/System.Reflection.Emit.xml",
+				"ref/netstandard1.1/ja/System.Reflection.Emit.xml",
+				"ref/netstandard1.1/ko/System.Reflection.Emit.xml",
+				"ref/netstandard1.1/ru/System.Reflection.Emit.xml",
+				"ref/netstandard1.1/zh-hans/System.Reflection.Emit.xml",
+				"ref/netstandard1.1/zh-hant/System.Reflection.Emit.xml",
+				"ref/xamarinmac20/_._",
+				"system.reflection.emit.4.3.0.nupkg.sha512",
+				"system.reflection.emit.nuspec"
+			]
+		},
+		"System.Reflection.Emit.ILGeneration/4.3.0": {
+			"sha512": "Mc/J1aFhbZVSc8+479qA0plqL/S8uWVjCd0Z3klzfd9bZP5mb9B9LcNVOpskC0nuQc/DwOY0Knt0D9+oNdYcmQ==",
+			"type": "package",
+			"path": "system.reflection.emit.ilgeneration/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net45/_._",
+				"lib/netcore50/System.Reflection.Emit.ILGeneration.dll",
+				"lib/netstandard1.3/System.Reflection.Emit.ILGeneration.dll",
+				"lib/portable-net45+wp8/_._",
+				"lib/wp80/_._",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net45/_._",
+				"ref/netstandard1.0/System.Reflection.Emit.ILGeneration.dll",
+				"ref/netstandard1.0/System.Reflection.Emit.ILGeneration.xml",
+				"ref/netstandard1.0/de/System.Reflection.Emit.ILGeneration.xml",
+				"ref/netstandard1.0/es/System.Reflection.Emit.ILGeneration.xml",
+				"ref/netstandard1.0/fr/System.Reflection.Emit.ILGeneration.xml",
+				"ref/netstandard1.0/it/System.Reflection.Emit.ILGeneration.xml",
+				"ref/netstandard1.0/ja/System.Reflection.Emit.ILGeneration.xml",
+				"ref/netstandard1.0/ko/System.Reflection.Emit.ILGeneration.xml",
+				"ref/netstandard1.0/ru/System.Reflection.Emit.ILGeneration.xml",
+				"ref/netstandard1.0/zh-hans/System.Reflection.Emit.ILGeneration.xml",
+				"ref/netstandard1.0/zh-hant/System.Reflection.Emit.ILGeneration.xml",
+				"ref/portable-net45+wp8/_._",
+				"ref/wp80/_._",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"runtimes/aot/lib/netcore50/_._",
+				"system.reflection.emit.ilgeneration.4.3.0.nupkg.sha512",
+				"system.reflection.emit.ilgeneration.nuspec"
+			]
+		},
+		"System.Reflection.Emit.Lightweight/4.3.0": {
+			"sha512": "Sj4SlGByuZFZJvTWFGVMKvsUC9mma++NxccuAiBsZq8muCID7YD3+INgw/d4ReK0PakK5nWDms8RYnPEljyq1Q==",
+			"type": "package",
+			"path": "system.reflection.emit.lightweight/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net45/_._",
+				"lib/netcore50/System.Reflection.Emit.Lightweight.dll",
+				"lib/netstandard1.3/System.Reflection.Emit.Lightweight.dll",
+				"lib/portable-net45+wp8/_._",
+				"lib/wp80/_._",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net45/_._",
+				"ref/netstandard1.0/System.Reflection.Emit.Lightweight.dll",
+				"ref/netstandard1.0/System.Reflection.Emit.Lightweight.xml",
+				"ref/netstandard1.0/de/System.Reflection.Emit.Lightweight.xml",
+				"ref/netstandard1.0/es/System.Reflection.Emit.Lightweight.xml",
+				"ref/netstandard1.0/fr/System.Reflection.Emit.Lightweight.xml",
+				"ref/netstandard1.0/it/System.Reflection.Emit.Lightweight.xml",
+				"ref/netstandard1.0/ja/System.Reflection.Emit.Lightweight.xml",
+				"ref/netstandard1.0/ko/System.Reflection.Emit.Lightweight.xml",
+				"ref/netstandard1.0/ru/System.Reflection.Emit.Lightweight.xml",
+				"ref/netstandard1.0/zh-hans/System.Reflection.Emit.Lightweight.xml",
+				"ref/netstandard1.0/zh-hant/System.Reflection.Emit.Lightweight.xml",
+				"ref/portable-net45+wp8/_._",
+				"ref/wp80/_._",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"runtimes/aot/lib/netcore50/_._",
+				"system.reflection.emit.lightweight.4.3.0.nupkg.sha512",
+				"system.reflection.emit.lightweight.nuspec"
+			]
+		},
+		"System.Reflection.Extensions/4.3.0": {
+			"sha512": "cZ5tI5Vya2+k3RregWqugc+zWwdxRgFSt2GF7ju/2J21fu5koK0cXDk0RtCSOteONBNmvz27SH+Cj/XL4vfXcw==",
+			"type": "package",
+			"path": "system.reflection.extensions/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net45/_._",
+				"lib/portable-net45+win8+wp8+wpa81/_._",
+				"lib/win8/_._",
+				"lib/wp80/_._",
+				"lib/wpa81/_._",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net45/_._",
+				"ref/netcore50/System.Reflection.Extensions.dll",
+				"ref/netcore50/System.Reflection.Extensions.xml",
+				"ref/netcore50/de/System.Reflection.Extensions.xml",
+				"ref/netcore50/es/System.Reflection.Extensions.xml",
+				"ref/netcore50/fr/System.Reflection.Extensions.xml",
+				"ref/netcore50/it/System.Reflection.Extensions.xml",
+				"ref/netcore50/ja/System.Reflection.Extensions.xml",
+				"ref/netcore50/ko/System.Reflection.Extensions.xml",
+				"ref/netcore50/ru/System.Reflection.Extensions.xml",
+				"ref/netcore50/zh-hans/System.Reflection.Extensions.xml",
+				"ref/netcore50/zh-hant/System.Reflection.Extensions.xml",
+				"ref/netstandard1.0/System.Reflection.Extensions.dll",
+				"ref/netstandard1.0/System.Reflection.Extensions.xml",
+				"ref/netstandard1.0/de/System.Reflection.Extensions.xml",
+				"ref/netstandard1.0/es/System.Reflection.Extensions.xml",
+				"ref/netstandard1.0/fr/System.Reflection.Extensions.xml",
+				"ref/netstandard1.0/it/System.Reflection.Extensions.xml",
+				"ref/netstandard1.0/ja/System.Reflection.Extensions.xml",
+				"ref/netstandard1.0/ko/System.Reflection.Extensions.xml",
+				"ref/netstandard1.0/ru/System.Reflection.Extensions.xml",
+				"ref/netstandard1.0/zh-hans/System.Reflection.Extensions.xml",
+				"ref/netstandard1.0/zh-hant/System.Reflection.Extensions.xml",
+				"ref/portable-net45+win8+wp8+wpa81/_._",
+				"ref/win8/_._",
+				"ref/wp80/_._",
+				"ref/wpa81/_._",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"system.reflection.extensions.4.3.0.nupkg.sha512",
+				"system.reflection.extensions.nuspec"
+			]
+		},
+		"System.Reflection.Primitives/4.3.0": {
+			"sha512": "oaBgqLXHdK81uoGe83s5r+iP+uOk5ZaNp/LxS4ity/V5gDCk6KGdXMTQruGHDl1EajJ11xf5wDMDdz9febmTrw==",
+			"type": "package",
+			"path": "system.reflection.primitives/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net45/_._",
+				"lib/portable-net45+win8+wp8+wpa81/_._",
+				"lib/win8/_._",
+				"lib/wp80/_._",
+				"lib/wpa81/_._",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net45/_._",
+				"ref/netcore50/System.Reflection.Primitives.dll",
+				"ref/netcore50/System.Reflection.Primitives.xml",
+				"ref/netcore50/de/System.Reflection.Primitives.xml",
+				"ref/netcore50/es/System.Reflection.Primitives.xml",
+				"ref/netcore50/fr/System.Reflection.Primitives.xml",
+				"ref/netcore50/it/System.Reflection.Primitives.xml",
+				"ref/netcore50/ja/System.Reflection.Primitives.xml",
+				"ref/netcore50/ko/System.Reflection.Primitives.xml",
+				"ref/netcore50/ru/System.Reflection.Primitives.xml",
+				"ref/netcore50/zh-hans/System.Reflection.Primitives.xml",
+				"ref/netcore50/zh-hant/System.Reflection.Primitives.xml",
+				"ref/netstandard1.0/System.Reflection.Primitives.dll",
+				"ref/netstandard1.0/System.Reflection.Primitives.xml",
+				"ref/netstandard1.0/de/System.Reflection.Primitives.xml",
+				"ref/netstandard1.0/es/System.Reflection.Primitives.xml",
+				"ref/netstandard1.0/fr/System.Reflection.Primitives.xml",
+				"ref/netstandard1.0/it/System.Reflection.Primitives.xml",
+				"ref/netstandard1.0/ja/System.Reflection.Primitives.xml",
+				"ref/netstandard1.0/ko/System.Reflection.Primitives.xml",
+				"ref/netstandard1.0/ru/System.Reflection.Primitives.xml",
+				"ref/netstandard1.0/zh-hans/System.Reflection.Primitives.xml",
+				"ref/netstandard1.0/zh-hant/System.Reflection.Primitives.xml",
+				"ref/portable-net45+win8+wp8+wpa81/_._",
+				"ref/win8/_._",
+				"ref/wp80/_._",
+				"ref/wpa81/_._",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"system.reflection.primitives.4.3.0.nupkg.sha512",
+				"system.reflection.primitives.nuspec"
+			]
+		},
+		"System.Reflection.TypeExtensions/4.3.0": {
+			"sha512": "2fhSLJUwruemnj/qArAMODeruJOttBpNX8v78qsJj8TrFcN0U50jfVn0mmFa/L9YxCGiEXwPrVYJ3VFtbh2v2A==",
+			"type": "package",
+			"path": "system.reflection.typeextensions/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net46/System.Reflection.TypeExtensions.dll",
+				"lib/net462/System.Reflection.TypeExtensions.dll",
+				"lib/netcore50/System.Reflection.TypeExtensions.dll",
+				"lib/netstandard1.5/System.Reflection.TypeExtensions.dll",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net46/System.Reflection.TypeExtensions.dll",
+				"ref/net462/System.Reflection.TypeExtensions.dll",
+				"ref/netstandard1.3/System.Reflection.TypeExtensions.dll",
+				"ref/netstandard1.3/System.Reflection.TypeExtensions.xml",
+				"ref/netstandard1.3/de/System.Reflection.TypeExtensions.xml",
+				"ref/netstandard1.3/es/System.Reflection.TypeExtensions.xml",
+				"ref/netstandard1.3/fr/System.Reflection.TypeExtensions.xml",
+				"ref/netstandard1.3/it/System.Reflection.TypeExtensions.xml",
+				"ref/netstandard1.3/ja/System.Reflection.TypeExtensions.xml",
+				"ref/netstandard1.3/ko/System.Reflection.TypeExtensions.xml",
+				"ref/netstandard1.3/ru/System.Reflection.TypeExtensions.xml",
+				"ref/netstandard1.3/zh-hans/System.Reflection.TypeExtensions.xml",
+				"ref/netstandard1.3/zh-hant/System.Reflection.TypeExtensions.xml",
+				"ref/netstandard1.5/System.Reflection.TypeExtensions.dll",
+				"ref/netstandard1.5/System.Reflection.TypeExtensions.xml",
+				"ref/netstandard1.5/de/System.Reflection.TypeExtensions.xml",
+				"ref/netstandard1.5/es/System.Reflection.TypeExtensions.xml",
+				"ref/netstandard1.5/fr/System.Reflection.TypeExtensions.xml",
+				"ref/netstandard1.5/it/System.Reflection.TypeExtensions.xml",
+				"ref/netstandard1.5/ja/System.Reflection.TypeExtensions.xml",
+				"ref/netstandard1.5/ko/System.Reflection.TypeExtensions.xml",
+				"ref/netstandard1.5/ru/System.Reflection.TypeExtensions.xml",
+				"ref/netstandard1.5/zh-hans/System.Reflection.TypeExtensions.xml",
+				"ref/netstandard1.5/zh-hant/System.Reflection.TypeExtensions.xml",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"runtimes/aot/lib/netcore50/System.Reflection.TypeExtensions.dll",
+				"system.reflection.typeextensions.4.3.0.nupkg.sha512",
+				"system.reflection.typeextensions.nuspec"
+			]
+		},
+		"System.Resources.ResourceManager/4.3.0": {
+			"sha512": "auIAchCOROdIxkZ9khiPLPiRK2v6IG4Ecph5gv1I9oqND+5PqWShRjOZKuTcMCRzVIR+PAc+SzeHZQ0UB2TsgA==",
+			"type": "package",
+			"path": "system.resources.resourcemanager/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net45/_._",
+				"lib/portable-net45+win8+wp8+wpa81/_._",
+				"lib/win8/_._",
+				"lib/wp80/_._",
+				"lib/wpa81/_._",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net45/_._",
+				"ref/netcore50/System.Resources.ResourceManager.dll",
+				"ref/netcore50/System.Resources.ResourceManager.xml",
+				"ref/netcore50/de/System.Resources.ResourceManager.xml",
+				"ref/netcore50/es/System.Resources.ResourceManager.xml",
+				"ref/netcore50/fr/System.Resources.ResourceManager.xml",
+				"ref/netcore50/it/System.Resources.ResourceManager.xml",
+				"ref/netcore50/ja/System.Resources.ResourceManager.xml",
+				"ref/netcore50/ko/System.Resources.ResourceManager.xml",
+				"ref/netcore50/ru/System.Resources.ResourceManager.xml",
+				"ref/netcore50/zh-hans/System.Resources.ResourceManager.xml",
+				"ref/netcore50/zh-hant/System.Resources.ResourceManager.xml",
+				"ref/netstandard1.0/System.Resources.ResourceManager.dll",
+				"ref/netstandard1.0/System.Resources.ResourceManager.xml",
+				"ref/netstandard1.0/de/System.Resources.ResourceManager.xml",
+				"ref/netstandard1.0/es/System.Resources.ResourceManager.xml",
+				"ref/netstandard1.0/fr/System.Resources.ResourceManager.xml",
+				"ref/netstandard1.0/it/System.Resources.ResourceManager.xml",
+				"ref/netstandard1.0/ja/System.Resources.ResourceManager.xml",
+				"ref/netstandard1.0/ko/System.Resources.ResourceManager.xml",
+				"ref/netstandard1.0/ru/System.Resources.ResourceManager.xml",
+				"ref/netstandard1.0/zh-hans/System.Resources.ResourceManager.xml",
+				"ref/netstandard1.0/zh-hant/System.Resources.ResourceManager.xml",
+				"ref/portable-net45+win8+wp8+wpa81/_._",
+				"ref/win8/_._",
+				"ref/wp80/_._",
+				"ref/wpa81/_._",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"system.resources.resourcemanager.4.3.0.nupkg.sha512",
+				"system.resources.resourcemanager.nuspec"
+			]
+		},
+		"System.Runtime/4.3.0": {
+			"sha512": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
+			"type": "package",
+			"path": "system.runtime/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net45/_._",
+				"lib/net462/System.Runtime.dll",
+				"lib/portable-net45+win8+wp80+wpa81/_._",
+				"lib/win8/_._",
+				"lib/wp80/_._",
+				"lib/wpa81/_._",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net45/_._",
+				"ref/net462/System.Runtime.dll",
+				"ref/netcore50/System.Runtime.dll",
+				"ref/netcore50/System.Runtime.xml",
+				"ref/netcore50/de/System.Runtime.xml",
+				"ref/netcore50/es/System.Runtime.xml",
+				"ref/netcore50/fr/System.Runtime.xml",
+				"ref/netcore50/it/System.Runtime.xml",
+				"ref/netcore50/ja/System.Runtime.xml",
+				"ref/netcore50/ko/System.Runtime.xml",
+				"ref/netcore50/ru/System.Runtime.xml",
+				"ref/netcore50/zh-hans/System.Runtime.xml",
+				"ref/netcore50/zh-hant/System.Runtime.xml",
+				"ref/netstandard1.0/System.Runtime.dll",
+				"ref/netstandard1.0/System.Runtime.xml",
+				"ref/netstandard1.0/de/System.Runtime.xml",
+				"ref/netstandard1.0/es/System.Runtime.xml",
+				"ref/netstandard1.0/fr/System.Runtime.xml",
+				"ref/netstandard1.0/it/System.Runtime.xml",
+				"ref/netstandard1.0/ja/System.Runtime.xml",
+				"ref/netstandard1.0/ko/System.Runtime.xml",
+				"ref/netstandard1.0/ru/System.Runtime.xml",
+				"ref/netstandard1.0/zh-hans/System.Runtime.xml",
+				"ref/netstandard1.0/zh-hant/System.Runtime.xml",
+				"ref/netstandard1.2/System.Runtime.dll",
+				"ref/netstandard1.2/System.Runtime.xml",
+				"ref/netstandard1.2/de/System.Runtime.xml",
+				"ref/netstandard1.2/es/System.Runtime.xml",
+				"ref/netstandard1.2/fr/System.Runtime.xml",
+				"ref/netstandard1.2/it/System.Runtime.xml",
+				"ref/netstandard1.2/ja/System.Runtime.xml",
+				"ref/netstandard1.2/ko/System.Runtime.xml",
+				"ref/netstandard1.2/ru/System.Runtime.xml",
+				"ref/netstandard1.2/zh-hans/System.Runtime.xml",
+				"ref/netstandard1.2/zh-hant/System.Runtime.xml",
+				"ref/netstandard1.3/System.Runtime.dll",
+				"ref/netstandard1.3/System.Runtime.xml",
+				"ref/netstandard1.3/de/System.Runtime.xml",
+				"ref/netstandard1.3/es/System.Runtime.xml",
+				"ref/netstandard1.3/fr/System.Runtime.xml",
+				"ref/netstandard1.3/it/System.Runtime.xml",
+				"ref/netstandard1.3/ja/System.Runtime.xml",
+				"ref/netstandard1.3/ko/System.Runtime.xml",
+				"ref/netstandard1.3/ru/System.Runtime.xml",
+				"ref/netstandard1.3/zh-hans/System.Runtime.xml",
+				"ref/netstandard1.3/zh-hant/System.Runtime.xml",
+				"ref/netstandard1.5/System.Runtime.dll",
+				"ref/netstandard1.5/System.Runtime.xml",
+				"ref/netstandard1.5/de/System.Runtime.xml",
+				"ref/netstandard1.5/es/System.Runtime.xml",
+				"ref/netstandard1.5/fr/System.Runtime.xml",
+				"ref/netstandard1.5/it/System.Runtime.xml",
+				"ref/netstandard1.5/ja/System.Runtime.xml",
+				"ref/netstandard1.5/ko/System.Runtime.xml",
+				"ref/netstandard1.5/ru/System.Runtime.xml",
+				"ref/netstandard1.5/zh-hans/System.Runtime.xml",
+				"ref/netstandard1.5/zh-hant/System.Runtime.xml",
+				"ref/portable-net45+win8+wp80+wpa81/_._",
+				"ref/win8/_._",
+				"ref/wp80/_._",
+				"ref/wpa81/_._",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"system.runtime.4.3.0.nupkg.sha512",
+				"system.runtime.nuspec"
+			]
+		},
+		"System.Runtime.Extensions/4.3.0": {
+			"sha512": "yt+AqvxHbHImbFtN/bLLdm494IYMVqBCIGu8yk6p4Vh7j+c5y1MQz1zg1Pl3lrKZxb1VUxZs4rNxSX2tBMMd5w==",
+			"type": "package",
+			"path": "system.runtime.extensions/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net45/_._",
+				"lib/net462/System.Runtime.Extensions.dll",
+				"lib/portable-net45+win8+wp8+wpa81/_._",
+				"lib/win8/_._",
+				"lib/wp80/_._",
+				"lib/wpa81/_._",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net45/_._",
+				"ref/net462/System.Runtime.Extensions.dll",
+				"ref/netcore50/System.Runtime.Extensions.dll",
+				"ref/netcore50/System.Runtime.Extensions.xml",
+				"ref/netcore50/de/System.Runtime.Extensions.xml",
+				"ref/netcore50/es/System.Runtime.Extensions.xml",
+				"ref/netcore50/fr/System.Runtime.Extensions.xml",
+				"ref/netcore50/it/System.Runtime.Extensions.xml",
+				"ref/netcore50/ja/System.Runtime.Extensions.xml",
+				"ref/netcore50/ko/System.Runtime.Extensions.xml",
+				"ref/netcore50/ru/System.Runtime.Extensions.xml",
+				"ref/netcore50/zh-hans/System.Runtime.Extensions.xml",
+				"ref/netcore50/zh-hant/System.Runtime.Extensions.xml",
+				"ref/netstandard1.0/System.Runtime.Extensions.dll",
+				"ref/netstandard1.0/System.Runtime.Extensions.xml",
+				"ref/netstandard1.0/de/System.Runtime.Extensions.xml",
+				"ref/netstandard1.0/es/System.Runtime.Extensions.xml",
+				"ref/netstandard1.0/fr/System.Runtime.Extensions.xml",
+				"ref/netstandard1.0/it/System.Runtime.Extensions.xml",
+				"ref/netstandard1.0/ja/System.Runtime.Extensions.xml",
+				"ref/netstandard1.0/ko/System.Runtime.Extensions.xml",
+				"ref/netstandard1.0/ru/System.Runtime.Extensions.xml",
+				"ref/netstandard1.0/zh-hans/System.Runtime.Extensions.xml",
+				"ref/netstandard1.0/zh-hant/System.Runtime.Extensions.xml",
+				"ref/netstandard1.3/System.Runtime.Extensions.dll",
+				"ref/netstandard1.3/System.Runtime.Extensions.xml",
+				"ref/netstandard1.3/de/System.Runtime.Extensions.xml",
+				"ref/netstandard1.3/es/System.Runtime.Extensions.xml",
+				"ref/netstandard1.3/fr/System.Runtime.Extensions.xml",
+				"ref/netstandard1.3/it/System.Runtime.Extensions.xml",
+				"ref/netstandard1.3/ja/System.Runtime.Extensions.xml",
+				"ref/netstandard1.3/ko/System.Runtime.Extensions.xml",
+				"ref/netstandard1.3/ru/System.Runtime.Extensions.xml",
+				"ref/netstandard1.3/zh-hans/System.Runtime.Extensions.xml",
+				"ref/netstandard1.3/zh-hant/System.Runtime.Extensions.xml",
+				"ref/netstandard1.5/System.Runtime.Extensions.dll",
+				"ref/netstandard1.5/System.Runtime.Extensions.xml",
+				"ref/netstandard1.5/de/System.Runtime.Extensions.xml",
+				"ref/netstandard1.5/es/System.Runtime.Extensions.xml",
+				"ref/netstandard1.5/fr/System.Runtime.Extensions.xml",
+				"ref/netstandard1.5/it/System.Runtime.Extensions.xml",
+				"ref/netstandard1.5/ja/System.Runtime.Extensions.xml",
+				"ref/netstandard1.5/ko/System.Runtime.Extensions.xml",
+				"ref/netstandard1.5/ru/System.Runtime.Extensions.xml",
+				"ref/netstandard1.5/zh-hans/System.Runtime.Extensions.xml",
+				"ref/netstandard1.5/zh-hant/System.Runtime.Extensions.xml",
+				"ref/portable-net45+win8+wp8+wpa81/_._",
+				"ref/win8/_._",
+				"ref/wp80/_._",
+				"ref/wpa81/_._",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"system.runtime.extensions.4.3.0.nupkg.sha512",
+				"system.runtime.extensions.nuspec"
+			]
+		},
+		"System.Runtime.Handles/4.3.0": {
+			"sha512": "V3MooCI0PHMoaTiuHWyDsSTL0nM2CZaIrSCorymuyn1PUoQhyBeAV2CC75oNIut3jt5b5yXpDZfIHkP0xWfRhw==",
+			"type": "package",
+			"path": "system.runtime.handles/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net46/_._",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net46/_._",
+				"ref/netstandard1.3/System.Runtime.Handles.dll",
+				"ref/netstandard1.3/System.Runtime.Handles.xml",
+				"ref/netstandard1.3/de/System.Runtime.Handles.xml",
+				"ref/netstandard1.3/es/System.Runtime.Handles.xml",
+				"ref/netstandard1.3/fr/System.Runtime.Handles.xml",
+				"ref/netstandard1.3/it/System.Runtime.Handles.xml",
+				"ref/netstandard1.3/ja/System.Runtime.Handles.xml",
+				"ref/netstandard1.3/ko/System.Runtime.Handles.xml",
+				"ref/netstandard1.3/ru/System.Runtime.Handles.xml",
+				"ref/netstandard1.3/zh-hans/System.Runtime.Handles.xml",
+				"ref/netstandard1.3/zh-hant/System.Runtime.Handles.xml",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"system.runtime.handles.4.3.0.nupkg.sha512",
+				"system.runtime.handles.nuspec"
+			]
+		},
+		"System.Runtime.InteropServices/4.3.0": {
+			"sha512": "wI+FumZaz/hXeWfTCgfm0gCI6mchKPbnXO+/GMiLj5KrQpIKe/vmTfFRNAcJdnsBCxxWfGD5QfWzOe0vXpndYQ==",
+			"type": "package",
+			"path": "system.runtime.interopservices/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net45/_._",
+				"lib/net462/System.Runtime.InteropServices.dll",
+				"lib/net463/System.Runtime.InteropServices.dll",
+				"lib/portable-net45+win8+wpa81/_._",
+				"lib/win8/_._",
+				"lib/wpa81/_._",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net45/_._",
+				"ref/net462/System.Runtime.InteropServices.dll",
+				"ref/net463/System.Runtime.InteropServices.dll",
+				"ref/netcore50/System.Runtime.InteropServices.dll",
+				"ref/netcore50/System.Runtime.InteropServices.xml",
+				"ref/netcore50/de/System.Runtime.InteropServices.xml",
+				"ref/netcore50/es/System.Runtime.InteropServices.xml",
+				"ref/netcore50/fr/System.Runtime.InteropServices.xml",
+				"ref/netcore50/it/System.Runtime.InteropServices.xml",
+				"ref/netcore50/ja/System.Runtime.InteropServices.xml",
+				"ref/netcore50/ko/System.Runtime.InteropServices.xml",
+				"ref/netcore50/ru/System.Runtime.InteropServices.xml",
+				"ref/netcore50/zh-hans/System.Runtime.InteropServices.xml",
+				"ref/netcore50/zh-hant/System.Runtime.InteropServices.xml",
+				"ref/netcoreapp1.1/System.Runtime.InteropServices.dll",
+				"ref/netstandard1.1/System.Runtime.InteropServices.dll",
+				"ref/netstandard1.1/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.1/de/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.1/es/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.1/fr/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.1/it/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.1/ja/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.1/ko/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.1/ru/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.1/zh-hans/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.1/zh-hant/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.2/System.Runtime.InteropServices.dll",
+				"ref/netstandard1.2/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.2/de/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.2/es/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.2/fr/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.2/it/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.2/ja/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.2/ko/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.2/ru/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.2/zh-hans/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.2/zh-hant/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.3/System.Runtime.InteropServices.dll",
+				"ref/netstandard1.3/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.3/de/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.3/es/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.3/fr/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.3/it/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.3/ja/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.3/ko/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.3/ru/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.3/zh-hans/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.3/zh-hant/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.5/System.Runtime.InteropServices.dll",
+				"ref/netstandard1.5/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.5/de/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.5/es/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.5/fr/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.5/it/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.5/ja/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.5/ko/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.5/ru/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.5/zh-hans/System.Runtime.InteropServices.xml",
+				"ref/netstandard1.5/zh-hant/System.Runtime.InteropServices.xml",
+				"ref/portable-net45+win8+wpa81/_._",
+				"ref/win8/_._",
+				"ref/wpa81/_._",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"system.runtime.interopservices.4.3.0.nupkg.sha512",
+				"system.runtime.interopservices.nuspec"
+			]
+		},
+		"System.Runtime.InteropServices.WindowsRuntime/4.3.0": {
+			"sha512": "J4GUi3xZQLUBasNwZnjrffN8i5wpHrBtZoLG+OhRyGo/+YunMRWWtwoMDlUAIdmX0uRfpHIBDSV6zyr3yf00TA==",
+			"type": "package",
+			"path": "system.runtime.interopservices.windowsruntime/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net45/_._",
+				"lib/netcore50/System.Runtime.InteropServices.WindowsRuntime.dll",
+				"lib/netstandard1.3/System.Runtime.InteropServices.WindowsRuntime.dll",
+				"lib/portable-net45+win8+wp8+wpa81/_._",
+				"lib/win8/_._",
+				"lib/wp80/_._",
+				"lib/wpa81/_._",
+				"lib/xamarinios1/_._",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net45/_._",
+				"ref/netcore50/System.Runtime.InteropServices.WindowsRuntime.dll",
+				"ref/netcore50/System.Runtime.InteropServices.WindowsRuntime.xml",
+				"ref/netcore50/de/System.Runtime.InteropServices.WindowsRuntime.xml",
+				"ref/netcore50/es/System.Runtime.InteropServices.WindowsRuntime.xml",
+				"ref/netcore50/fr/System.Runtime.InteropServices.WindowsRuntime.xml",
+				"ref/netcore50/it/System.Runtime.InteropServices.WindowsRuntime.xml",
+				"ref/netcore50/ja/System.Runtime.InteropServices.WindowsRuntime.xml",
+				"ref/netcore50/ko/System.Runtime.InteropServices.WindowsRuntime.xml",
+				"ref/netcore50/ru/System.Runtime.InteropServices.WindowsRuntime.xml",
+				"ref/netcore50/zh-hans/System.Runtime.InteropServices.WindowsRuntime.xml",
+				"ref/netcore50/zh-hant/System.Runtime.InteropServices.WindowsRuntime.xml",
+				"ref/netstandard1.0/System.Runtime.InteropServices.WindowsRuntime.dll",
+				"ref/netstandard1.0/System.Runtime.InteropServices.WindowsRuntime.xml",
+				"ref/netstandard1.0/de/System.Runtime.InteropServices.WindowsRuntime.xml",
+				"ref/netstandard1.0/es/System.Runtime.InteropServices.WindowsRuntime.xml",
+				"ref/netstandard1.0/fr/System.Runtime.InteropServices.WindowsRuntime.xml",
+				"ref/netstandard1.0/it/System.Runtime.InteropServices.WindowsRuntime.xml",
+				"ref/netstandard1.0/ja/System.Runtime.InteropServices.WindowsRuntime.xml",
+				"ref/netstandard1.0/ko/System.Runtime.InteropServices.WindowsRuntime.xml",
+				"ref/netstandard1.0/ru/System.Runtime.InteropServices.WindowsRuntime.xml",
+				"ref/netstandard1.0/zh-hans/System.Runtime.InteropServices.WindowsRuntime.xml",
+				"ref/netstandard1.0/zh-hant/System.Runtime.InteropServices.WindowsRuntime.xml",
+				"ref/portable-net45+win8+wp8+wpa81/_._",
+				"ref/win8/_._",
+				"ref/wp80/_._",
+				"ref/wpa81/_._",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"runtimes/aot/lib/netcore50/System.Runtime.InteropServices.WindowsRuntime.dll",
+				"system.runtime.interopservices.windowsruntime.4.3.0.nupkg.sha512",
+				"system.runtime.interopservices.windowsruntime.nuspec"
+			]
+		},
+		"System.Runtime.Loader/4.3.0": {
+			"sha512": "DHMaRn8D8YCK2GG2pw+UzNxn/OHVfaWx7OTLBD/hPegHZZgcZh3H6seWegrC4BYwsfuGrywIuT+MQs+rPqRLTQ==",
+			"type": "package",
+			"path": "system.runtime.loader/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net462/_._",
+				"lib/netstandard1.5/System.Runtime.Loader.dll",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/netstandard1.5/System.Runtime.Loader.dll",
+				"ref/netstandard1.5/System.Runtime.Loader.xml",
+				"ref/netstandard1.5/de/System.Runtime.Loader.xml",
+				"ref/netstandard1.5/es/System.Runtime.Loader.xml",
+				"ref/netstandard1.5/fr/System.Runtime.Loader.xml",
+				"ref/netstandard1.5/it/System.Runtime.Loader.xml",
+				"ref/netstandard1.5/ja/System.Runtime.Loader.xml",
+				"ref/netstandard1.5/ko/System.Runtime.Loader.xml",
+				"ref/netstandard1.5/ru/System.Runtime.Loader.xml",
+				"ref/netstandard1.5/zh-hans/System.Runtime.Loader.xml",
+				"ref/netstandard1.5/zh-hant/System.Runtime.Loader.xml",
+				"system.runtime.loader.4.3.0.nupkg.sha512",
+				"system.runtime.loader.nuspec"
+			]
+		},
+		"System.Security.Cryptography.Primitives/4.3.0": {
+			"sha512": "KPbXYG4gvrm+AzckXHktg7DYC5j4xsvdmva4oJLdir3uFZjoYY2NG2nBKdL33St1C1tVF5rsKSAILKqeGqjY2g==",
+			"type": "package",
+			"path": "system.security.cryptography.primitives/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net46/System.Security.Cryptography.Primitives.dll",
+				"lib/netstandard1.3/System.Security.Cryptography.Primitives.dll",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net46/System.Security.Cryptography.Primitives.dll",
+				"ref/netstandard1.3/System.Security.Cryptography.Primitives.dll",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"system.security.cryptography.primitives.4.3.0.nupkg.sha512",
+				"system.security.cryptography.primitives.nuspec"
+			]
+		},
+		"System.Security.Cryptography.ProtectedData/4.3.0": {
+			"sha512": "qBUHUk7IqrPHY96THHTa1akCxw0GsNFpsk3XFHbi0A0tMUDBpQprtY1Tbl6yaS1x4c96ilcXU8PocYtmSmkaQQ==",
+			"type": "package",
+			"path": "system.security.cryptography.protecteddata/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net46/System.Security.Cryptography.ProtectedData.dll",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net46/System.Security.Cryptography.ProtectedData.dll",
+				"ref/netstandard1.3/System.Security.Cryptography.ProtectedData.dll",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"runtimes/unix/lib/netstandard1.3/System.Security.Cryptography.ProtectedData.dll",
+				"runtimes/win/lib/net46/System.Security.Cryptography.ProtectedData.dll",
+				"runtimes/win/lib/netstandard1.3/System.Security.Cryptography.ProtectedData.dll",
+				"system.security.cryptography.protecteddata.4.3.0.nupkg.sha512",
+				"system.security.cryptography.protecteddata.nuspec"
+			]
+		},
+		"System.Text.Encoding/4.3.0": {
+			"sha512": "A/CSPPY+HAH7x7IYKU7KGIRHWwHcDi+Ai9ERC30fpCbUY1SpzFapRPPB5xYep9GWG/TnJD9/prAwvsdyrTHDog==",
+			"type": "package",
+			"path": "system.text.encoding/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net45/_._",
+				"lib/portable-net45+win8+wp8+wpa81/_._",
+				"lib/win8/_._",
+				"lib/wp80/_._",
+				"lib/wpa81/_._",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net45/_._",
+				"ref/netcore50/System.Text.Encoding.dll",
+				"ref/netcore50/System.Text.Encoding.xml",
+				"ref/netcore50/de/System.Text.Encoding.xml",
+				"ref/netcore50/es/System.Text.Encoding.xml",
+				"ref/netcore50/fr/System.Text.Encoding.xml",
+				"ref/netcore50/it/System.Text.Encoding.xml",
+				"ref/netcore50/ja/System.Text.Encoding.xml",
+				"ref/netcore50/ko/System.Text.Encoding.xml",
+				"ref/netcore50/ru/System.Text.Encoding.xml",
+				"ref/netcore50/zh-hans/System.Text.Encoding.xml",
+				"ref/netcore50/zh-hant/System.Text.Encoding.xml",
+				"ref/netstandard1.0/System.Text.Encoding.dll",
+				"ref/netstandard1.0/System.Text.Encoding.xml",
+				"ref/netstandard1.0/de/System.Text.Encoding.xml",
+				"ref/netstandard1.0/es/System.Text.Encoding.xml",
+				"ref/netstandard1.0/fr/System.Text.Encoding.xml",
+				"ref/netstandard1.0/it/System.Text.Encoding.xml",
+				"ref/netstandard1.0/ja/System.Text.Encoding.xml",
+				"ref/netstandard1.0/ko/System.Text.Encoding.xml",
+				"ref/netstandard1.0/ru/System.Text.Encoding.xml",
+				"ref/netstandard1.0/zh-hans/System.Text.Encoding.xml",
+				"ref/netstandard1.0/zh-hant/System.Text.Encoding.xml",
+				"ref/netstandard1.3/System.Text.Encoding.dll",
+				"ref/netstandard1.3/System.Text.Encoding.xml",
+				"ref/netstandard1.3/de/System.Text.Encoding.xml",
+				"ref/netstandard1.3/es/System.Text.Encoding.xml",
+				"ref/netstandard1.3/fr/System.Text.Encoding.xml",
+				"ref/netstandard1.3/it/System.Text.Encoding.xml",
+				"ref/netstandard1.3/ja/System.Text.Encoding.xml",
+				"ref/netstandard1.3/ko/System.Text.Encoding.xml",
+				"ref/netstandard1.3/ru/System.Text.Encoding.xml",
+				"ref/netstandard1.3/zh-hans/System.Text.Encoding.xml",
+				"ref/netstandard1.3/zh-hant/System.Text.Encoding.xml",
+				"ref/portable-net45+win8+wp8+wpa81/_._",
+				"ref/win8/_._",
+				"ref/wp80/_._",
+				"ref/wpa81/_._",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"system.text.encoding.4.3.0.nupkg.sha512",
+				"system.text.encoding.nuspec"
+			]
+		},
+		"System.Text.Encoding.Extensions/4.3.0": {
+			"sha512": "/Kfn26qRhqTegFFNLc6o2hM3EtDzyh8Kf94yik6DFVyGGmVilGQj0CK6I2xbMVT+PMHeF3V6njeQPc8lfuBicg==",
+			"type": "package",
+			"path": "system.text.encoding.extensions/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net45/_._",
+				"lib/portable-net45+win8+wp8+wpa81/_._",
+				"lib/win8/_._",
+				"lib/wp80/_._",
+				"lib/wpa81/_._",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net45/_._",
+				"ref/netcore50/System.Text.Encoding.Extensions.dll",
+				"ref/netcore50/System.Text.Encoding.Extensions.xml",
+				"ref/netcore50/de/System.Text.Encoding.Extensions.xml",
+				"ref/netcore50/es/System.Text.Encoding.Extensions.xml",
+				"ref/netcore50/fr/System.Text.Encoding.Extensions.xml",
+				"ref/netcore50/it/System.Text.Encoding.Extensions.xml",
+				"ref/netcore50/ja/System.Text.Encoding.Extensions.xml",
+				"ref/netcore50/ko/System.Text.Encoding.Extensions.xml",
+				"ref/netcore50/ru/System.Text.Encoding.Extensions.xml",
+				"ref/netcore50/zh-hans/System.Text.Encoding.Extensions.xml",
+				"ref/netcore50/zh-hant/System.Text.Encoding.Extensions.xml",
+				"ref/netstandard1.0/System.Text.Encoding.Extensions.dll",
+				"ref/netstandard1.0/System.Text.Encoding.Extensions.xml",
+				"ref/netstandard1.0/de/System.Text.Encoding.Extensions.xml",
+				"ref/netstandard1.0/es/System.Text.Encoding.Extensions.xml",
+				"ref/netstandard1.0/fr/System.Text.Encoding.Extensions.xml",
+				"ref/netstandard1.0/it/System.Text.Encoding.Extensions.xml",
+				"ref/netstandard1.0/ja/System.Text.Encoding.Extensions.xml",
+				"ref/netstandard1.0/ko/System.Text.Encoding.Extensions.xml",
+				"ref/netstandard1.0/ru/System.Text.Encoding.Extensions.xml",
+				"ref/netstandard1.0/zh-hans/System.Text.Encoding.Extensions.xml",
+				"ref/netstandard1.0/zh-hant/System.Text.Encoding.Extensions.xml",
+				"ref/netstandard1.3/System.Text.Encoding.Extensions.dll",
+				"ref/netstandard1.3/System.Text.Encoding.Extensions.xml",
+				"ref/netstandard1.3/de/System.Text.Encoding.Extensions.xml",
+				"ref/netstandard1.3/es/System.Text.Encoding.Extensions.xml",
+				"ref/netstandard1.3/fr/System.Text.Encoding.Extensions.xml",
+				"ref/netstandard1.3/it/System.Text.Encoding.Extensions.xml",
+				"ref/netstandard1.3/ja/System.Text.Encoding.Extensions.xml",
+				"ref/netstandard1.3/ko/System.Text.Encoding.Extensions.xml",
+				"ref/netstandard1.3/ru/System.Text.Encoding.Extensions.xml",
+				"ref/netstandard1.3/zh-hans/System.Text.Encoding.Extensions.xml",
+				"ref/netstandard1.3/zh-hant/System.Text.Encoding.Extensions.xml",
+				"ref/portable-net45+win8+wp8+wpa81/_._",
+				"ref/win8/_._",
+				"ref/wp80/_._",
+				"ref/wpa81/_._",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"system.text.encoding.extensions.4.3.0.nupkg.sha512",
+				"system.text.encoding.extensions.nuspec"
+			]
+		},
+		"System.Threading/4.3.0": {
+			"sha512": "TXL0xLMyMsjF+GVjnlZGQDJ+ht94pa51Sdu49b/rtiRQqjnCr5wU26qDL1Um5Xkygy97TiWdZviBksmrK0Sjlw==",
+			"type": "package",
+			"path": "system.threading/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net45/_._",
+				"lib/netcore50/System.Threading.dll",
+				"lib/netstandard1.3/System.Threading.dll",
+				"lib/portable-net45+win8+wp8+wpa81/_._",
+				"lib/win8/_._",
+				"lib/wp80/_._",
+				"lib/wpa81/_._",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net45/_._",
+				"ref/netcore50/System.Threading.dll",
+				"ref/netcore50/System.Threading.xml",
+				"ref/netcore50/de/System.Threading.xml",
+				"ref/netcore50/es/System.Threading.xml",
+				"ref/netcore50/fr/System.Threading.xml",
+				"ref/netcore50/it/System.Threading.xml",
+				"ref/netcore50/ja/System.Threading.xml",
+				"ref/netcore50/ko/System.Threading.xml",
+				"ref/netcore50/ru/System.Threading.xml",
+				"ref/netcore50/zh-hans/System.Threading.xml",
+				"ref/netcore50/zh-hant/System.Threading.xml",
+				"ref/netstandard1.0/System.Threading.dll",
+				"ref/netstandard1.0/System.Threading.xml",
+				"ref/netstandard1.0/de/System.Threading.xml",
+				"ref/netstandard1.0/es/System.Threading.xml",
+				"ref/netstandard1.0/fr/System.Threading.xml",
+				"ref/netstandard1.0/it/System.Threading.xml",
+				"ref/netstandard1.0/ja/System.Threading.xml",
+				"ref/netstandard1.0/ko/System.Threading.xml",
+				"ref/netstandard1.0/ru/System.Threading.xml",
+				"ref/netstandard1.0/zh-hans/System.Threading.xml",
+				"ref/netstandard1.0/zh-hant/System.Threading.xml",
+				"ref/netstandard1.3/System.Threading.dll",
+				"ref/netstandard1.3/System.Threading.xml",
+				"ref/netstandard1.3/de/System.Threading.xml",
+				"ref/netstandard1.3/es/System.Threading.xml",
+				"ref/netstandard1.3/fr/System.Threading.xml",
+				"ref/netstandard1.3/it/System.Threading.xml",
+				"ref/netstandard1.3/ja/System.Threading.xml",
+				"ref/netstandard1.3/ko/System.Threading.xml",
+				"ref/netstandard1.3/ru/System.Threading.xml",
+				"ref/netstandard1.3/zh-hans/System.Threading.xml",
+				"ref/netstandard1.3/zh-hant/System.Threading.xml",
+				"ref/portable-net45+win8+wp8+wpa81/_._",
+				"ref/win8/_._",
+				"ref/wp80/_._",
+				"ref/wpa81/_._",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"runtimes/aot/lib/netcore50/System.Threading.dll",
+				"system.threading.4.3.0.nupkg.sha512",
+				"system.threading.nuspec"
+			]
+		},
+		"System.Threading.Tasks/4.3.0": {
+			"sha512": "hMoUsp8EfrVWub6+ZRT9EXmi3C8E/ZX4dpayEXKlygNneCnRZTNiWACsICU5Y5MY84W3NLNEu2nhop2nX/fT0A==",
+			"type": "package",
+			"path": "system.threading.tasks/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net45/_._",
+				"lib/portable-net45+win8+wp8+wpa81/_._",
+				"lib/win8/_._",
+				"lib/wp80/_._",
+				"lib/wpa81/_._",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net45/_._",
+				"ref/netcore50/System.Threading.Tasks.dll",
+				"ref/netcore50/System.Threading.Tasks.xml",
+				"ref/netcore50/de/System.Threading.Tasks.xml",
+				"ref/netcore50/es/System.Threading.Tasks.xml",
+				"ref/netcore50/fr/System.Threading.Tasks.xml",
+				"ref/netcore50/it/System.Threading.Tasks.xml",
+				"ref/netcore50/ja/System.Threading.Tasks.xml",
+				"ref/netcore50/ko/System.Threading.Tasks.xml",
+				"ref/netcore50/ru/System.Threading.Tasks.xml",
+				"ref/netcore50/zh-hans/System.Threading.Tasks.xml",
+				"ref/netcore50/zh-hant/System.Threading.Tasks.xml",
+				"ref/netstandard1.0/System.Threading.Tasks.dll",
+				"ref/netstandard1.0/System.Threading.Tasks.xml",
+				"ref/netstandard1.0/de/System.Threading.Tasks.xml",
+				"ref/netstandard1.0/es/System.Threading.Tasks.xml",
+				"ref/netstandard1.0/fr/System.Threading.Tasks.xml",
+				"ref/netstandard1.0/it/System.Threading.Tasks.xml",
+				"ref/netstandard1.0/ja/System.Threading.Tasks.xml",
+				"ref/netstandard1.0/ko/System.Threading.Tasks.xml",
+				"ref/netstandard1.0/ru/System.Threading.Tasks.xml",
+				"ref/netstandard1.0/zh-hans/System.Threading.Tasks.xml",
+				"ref/netstandard1.0/zh-hant/System.Threading.Tasks.xml",
+				"ref/netstandard1.3/System.Threading.Tasks.dll",
+				"ref/netstandard1.3/System.Threading.Tasks.xml",
+				"ref/netstandard1.3/de/System.Threading.Tasks.xml",
+				"ref/netstandard1.3/es/System.Threading.Tasks.xml",
+				"ref/netstandard1.3/fr/System.Threading.Tasks.xml",
+				"ref/netstandard1.3/it/System.Threading.Tasks.xml",
+				"ref/netstandard1.3/ja/System.Threading.Tasks.xml",
+				"ref/netstandard1.3/ko/System.Threading.Tasks.xml",
+				"ref/netstandard1.3/ru/System.Threading.Tasks.xml",
+				"ref/netstandard1.3/zh-hans/System.Threading.Tasks.xml",
+				"ref/netstandard1.3/zh-hant/System.Threading.Tasks.xml",
+				"ref/portable-net45+win8+wp8+wpa81/_._",
+				"ref/win8/_._",
+				"ref/wp80/_._",
+				"ref/wpa81/_._",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"system.threading.tasks.4.3.0.nupkg.sha512",
+				"system.threading.tasks.nuspec"
+			]
+		},
+		"System.Threading.Tasks.Dataflow/4.9.0": {
+			"sha512": "dTS+3D/GtG2/Pvc3E5YzVvAa7aQJgLDlZDIzukMOJjYudVOQOUXEU68y6Zi3Nn/jqIeB5kOCwrGbQFAKHVzXEQ==",
+			"type": "package",
+			"path": "system.threading.tasks.dataflow/4.9.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.TXT",
+				"THIRD-PARTY-NOTICES.TXT",
+				"lib/netstandard1.0/System.Threading.Tasks.Dataflow.dll",
+				"lib/netstandard1.0/System.Threading.Tasks.Dataflow.xml",
+				"lib/netstandard1.1/System.Threading.Tasks.Dataflow.dll",
+				"lib/netstandard1.1/System.Threading.Tasks.Dataflow.xml",
+				"lib/netstandard2.0/System.Threading.Tasks.Dataflow.dll",
+				"lib/netstandard2.0/System.Threading.Tasks.Dataflow.xml",
+				"lib/portable-net45+win8+wpa81/System.Threading.Tasks.Dataflow.dll",
+				"lib/portable-net45+win8+wpa81/System.Threading.Tasks.Dataflow.xml",
+				"paket-installmodel.cache",
+				"system.threading.tasks.dataflow.4.9.0.nupkg.sha512",
+				"system.threading.tasks.dataflow.nuspec",
+				"useSharedDesignerContext.txt",
+				"version.txt"
+			]
+		},
+		"System.Threading.Tasks.Extensions/4.5.1": {
+			"sha512": "WSKUTtLhPR8gllzIWO2x6l4lmAIfbyMAiTlyXAis4QBDonXK4b4S6F8zGARX4/P8wH3DH+sLdhamCiHn+fTU1A==",
+			"type": "package",
+			"path": "system.threading.tasks.extensions/4.5.1",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"LICENSE.TXT",
+				"THIRD-PARTY-NOTICES.TXT",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/netcoreapp2.1/_._",
+				"lib/netstandard1.0/System.Threading.Tasks.Extensions.dll",
+				"lib/netstandard1.0/System.Threading.Tasks.Extensions.xml",
+				"lib/netstandard2.0/System.Threading.Tasks.Extensions.dll",
+				"lib/netstandard2.0/System.Threading.Tasks.Extensions.xml",
+				"lib/portable-net45+win8+wp8+wpa81/System.Threading.Tasks.Extensions.dll",
+				"lib/portable-net45+win8+wp8+wpa81/System.Threading.Tasks.Extensions.xml",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/netcoreapp2.1/_._",
+				"ref/netstandard1.0/System.Threading.Tasks.Extensions.dll",
+				"ref/netstandard1.0/System.Threading.Tasks.Extensions.xml",
+				"ref/netstandard2.0/System.Threading.Tasks.Extensions.dll",
+				"ref/netstandard2.0/System.Threading.Tasks.Extensions.xml",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"system.threading.tasks.extensions.4.5.1.nupkg.sha512",
+				"system.threading.tasks.extensions.nuspec",
+				"useSharedDesignerContext.txt",
+				"version.txt"
+			]
+		},
+		"System.Threading.Thread/4.3.0": {
+			"sha512": "p9W93yjgtQL+YruKrMfhysDbGHxCw6B+3ZI8xqEZmYoqYXVTEnkuBdv5iFSLfD1FVknC8VoSSnteKmuGCPH6Yg==",
+			"type": "package",
+			"path": "system.threading.thread/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net46/System.Threading.Thread.dll",
+				"lib/netcore50/_._",
+				"lib/netstandard1.3/System.Threading.Thread.dll",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net46/System.Threading.Thread.dll",
+				"ref/netstandard1.3/System.Threading.Thread.dll",
+				"ref/netstandard1.3/System.Threading.Thread.xml",
+				"ref/netstandard1.3/de/System.Threading.Thread.xml",
+				"ref/netstandard1.3/es/System.Threading.Thread.xml",
+				"ref/netstandard1.3/fr/System.Threading.Thread.xml",
+				"ref/netstandard1.3/it/System.Threading.Thread.xml",
+				"ref/netstandard1.3/ja/System.Threading.Thread.xml",
+				"ref/netstandard1.3/ko/System.Threading.Thread.xml",
+				"ref/netstandard1.3/ru/System.Threading.Thread.xml",
+				"ref/netstandard1.3/zh-hans/System.Threading.Thread.xml",
+				"ref/netstandard1.3/zh-hant/System.Threading.Thread.xml",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"system.threading.thread.4.3.0.nupkg.sha512",
+				"system.threading.thread.nuspec"
+			]
+		},
+		"System.Threading.ThreadPool/4.3.0": {
+			"sha512": "DlJ/+1Fj+RgS7IKXJWgXIVT02dDxTMRuAuNx74EetSuQ6/1hb011GkX8ulvc6xkL9KEzQ6kNMy//pfxCt4ux4Q==",
+			"type": "package",
+			"path": "system.threading.threadpool/4.3.0",
+			"files": [
+				".nupkg.metadata",
+				"ThirdPartyNotices.txt",
+				"dotnet_library_license.txt",
+				"lib/MonoAndroid10/_._",
+				"lib/MonoTouch10/_._",
+				"lib/net46/System.Threading.ThreadPool.dll",
+				"lib/netcore50/_._",
+				"lib/netstandard1.3/System.Threading.ThreadPool.dll",
+				"lib/xamarinios10/_._",
+				"lib/xamarinmac20/_._",
+				"lib/xamarintvos10/_._",
+				"lib/xamarinwatchos10/_._",
+				"paket-installmodel.cache",
+				"ref/MonoAndroid10/_._",
+				"ref/MonoTouch10/_._",
+				"ref/net46/System.Threading.ThreadPool.dll",
+				"ref/netstandard1.3/System.Threading.ThreadPool.dll",
+				"ref/netstandard1.3/System.Threading.ThreadPool.xml",
+				"ref/netstandard1.3/de/System.Threading.ThreadPool.xml",
+				"ref/netstandard1.3/es/System.Threading.ThreadPool.xml",
+				"ref/netstandard1.3/fr/System.Threading.ThreadPool.xml",
+				"ref/netstandard1.3/it/System.Threading.ThreadPool.xml",
+				"ref/netstandard1.3/ja/System.Threading.ThreadPool.xml",
+				"ref/netstandard1.3/ko/System.Threading.ThreadPool.xml",
+				"ref/netstandard1.3/ru/System.Threading.ThreadPool.xml",
+				"ref/netstandard1.3/zh-hans/System.Threading.ThreadPool.xml",
+				"ref/netstandard1.3/zh-hant/System.Threading.ThreadPool.xml",
+				"ref/xamarinios10/_._",
+				"ref/xamarinmac20/_._",
+				"ref/xamarintvos10/_._",
+				"ref/xamarinwatchos10/_._",
+				"system.threading.threadpool.4.3.0.nupkg.sha512",
+				"system.threading.threadpool.nuspec"
+			]
+		},
+		"YamlDotNet/5.3.0": {
+			"sha512": "w9oY3x4qZ9PR5fj5P9NIxXFyx47ZTHy30J7JKkYwDABh8quAs5H66Rv+9F5ib9je9lEdm2vrBRlLPght9erRQg==",
+			"type": "package",
+			"path": "yamldotnet/5.3.0",
+			"files": [
+				".nupkg.metadata",
+				".signature.p7s",
+				"lib/net20/YamlDotNet.dll",
+				"lib/net20/YamlDotNet.xml",
+				"lib/net35/YamlDotNet.dll",
+				"lib/net35/YamlDotNet.xml",
+				"lib/net45/YamlDotNet.dll",
+				"lib/net45/YamlDotNet.xml",
+				"lib/netstandard1.3/YamlDotNet.dll",
+				"lib/netstandard1.3/YamlDotNet.xml",
+				"yamldotnet.5.3.0.nupkg.sha512",
+				"yamldotnet.nuspec"
+			]
+		},
+		"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common/1.0.0": {
+			"type": "project",
+			"path": "../Common/MS.VS.Services.Governance.ComponentDetection.Common.csproj",
+			"msbuildProject": "../Common/MS.VS.Services.Governance.ComponentDetection.Common.csproj"
+		},
+		"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts/1.0.0": {
+			"type": "project",
+			"path": "../Contracts/MS.VS.Services.Governance.ComponentDetection.Contracts.csproj",
+			"msbuildProject": "../Contracts/MS.VS.Services.Governance.ComponentDetection.Contracts.csproj"
+		},
+		"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Detectors/1.0.0": {
+			"type": "project",
+			"path": "../Detectors/MS.VS.Services.Governance.ComponentDetection.Detectors.csproj",
+			"msbuildProject": "../Detectors/MS.VS.Services.Governance.ComponentDetection.Detectors.csproj"
+		},
+		"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Orchestrator/1.0.0": {
+			"type": "project",
+			"path": "../Orchestrator/MS.VS.Services.Governance.ComponentDetection.Orchestrator.csproj",
+			"msbuildProject": "../Orchestrator/MS.VS.Services.Governance.ComponentDetection.Orchestrator.csproj"
+		}
+	},
+	"projectFileDependencyGroups": {
+		".NETCoreApp,Version=v2.2": [
+			"Microsoft.NETCore.App >= 2.2.8",
+			"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common >= 1.0.0",
+			"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts >= 1.0.0",
+			"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Detectors >= 1.0.0",
+			"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Orchestrator >= 1.0.0",
+			"MinVer >= 2.5.0",
+			"StyleCop.Analyzers >= 1.0.2",
+			"coverlet.msbuild >= 2.5.1"
+		]
+	},
+	"packageFolders": {
+		"C:\\Users\\brphelps\\.nuget\\packages\\": {},
+		"C:\\Program Files\\dotnet\\sdk\\NuGetFallbackFolder": {}
+	},
+	"project": {
+		"version": "1.0.0",
+		"restore": {
+			"projectUniqueName": "D:\\Source\\componentdetection-bcde\\src\\Loader\\MS.VS.Services.Governance.ComponentDetection.Loader.csproj",
+			"projectName": "Microsoft.VisualStudio.Services.Governance.ComponentDetection.Loader",
+			"projectPath": "D:\\Source\\componentdetection-bcde\\src\\Loader\\MS.VS.Services.Governance.ComponentDetection.Loader.csproj",
+			"packagesPath": "C:\\Users\\brphelps\\.nuget\\packages\\",
+			"outputPath": "D:\\Source\\componentdetection-bcde\\src\\Loader\\obj\\",
+			"projectStyle": "PackageReference",
+			"fallbackFolders": [
+				"C:\\Program Files\\dotnet\\sdk\\NuGetFallbackFolder"
+			],
+			"configFilePaths": [
+				"C:\\Users\\brphelps\\AppData\\Roaming\\NuGet\\NuGet.Config",
+				"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
+			],
+			"originalTargetFrameworks": [
+				"netcoreapp2.2"
+			],
+			"sources": {
+				"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
+				"C:\\temp\\AI": {}
+			},
+			"frameworks": {
+				"netcoreapp2.2": {
+					"projectReferences": {
+						"D:\\Source\\componentdetection-bcde\\src\\Common\\MS.VS.Services.Governance.ComponentDetection.Common.csproj": {
+							"projectPath": "D:\\Source\\componentdetection-bcde\\src\\Common\\MS.VS.Services.Governance.ComponentDetection.Common.csproj"
+						},
+						"D:\\Source\\componentdetection-bcde\\src\\Contracts\\MS.VS.Services.Governance.ComponentDetection.Contracts.csproj": {
+							"projectPath": "D:\\Source\\componentdetection-bcde\\src\\Contracts\\MS.VS.Services.Governance.ComponentDetection.Contracts.csproj"
+						},
+						"D:\\Source\\componentdetection-bcde\\src\\Detectors\\MS.VS.Services.Governance.ComponentDetection.Detectors.csproj": {
+							"projectPath": "D:\\Source\\componentdetection-bcde\\src\\Detectors\\MS.VS.Services.Governance.ComponentDetection.Detectors.csproj"
+						},
+						"D:\\Source\\componentdetection-bcde\\src\\Orchestrator\\MS.VS.Services.Governance.ComponentDetection.Orchestrator.csproj": {
+							"projectPath": "D:\\Source\\componentdetection-bcde\\src\\Orchestrator\\MS.VS.Services.Governance.ComponentDetection.Orchestrator.csproj",
+							"privateAssets": "all"
+						}
+					}
+				}
+			},
+			"warningProperties": {
+				"noWarn": [
+					"NU1608"
+				],
+				"warnAsError": [
+					"NU1605"
+				]
+			}
+		},
+		"frameworks": {
+			"netcoreapp2.2": {
+				"dependencies": {
+					"Microsoft.NETCore.App": {
+						"suppressParent": "All",
+						"target": "Package",
+						"version": "[2.2.8, )",
+						"autoReferenced": true
+					},
+					"MinVer": {
+						"include": "Build, Analyzers",
+						"suppressParent": "All",
+						"target": "Package",
+						"version": "[2.5.0, )"
+					},
+					"StyleCop.Analyzers": {
+						"include": "Build, Analyzers",
+						"suppressParent": "All",
+						"target": "Package",
+						"version": "[1.0.2, )"
+					},
+					"coverlet.msbuild": {
+						"include": "Build, Analyzers",
+						"suppressParent": "All",
+						"target": "Package",
+						"version": "[2.5.1, )"
+					}
+				},
+				"imports": [
+					"net461",
+					"net462",
+					"net47",
+					"net471",
+					"net472",
+					"net48"
+				],
+				"assetTargetFallback": true,
+				"warn": true,
+				"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\3.1.201\\RuntimeIdentifierGraph.json"
+			}
+		},
+		"runtimes": {
+			"linux-x64": {
+				"#import": []
+			},
+			"osx-x64": {
+				"#import": []
+			},
+			"win-x64": {
+				"#import": []
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/Resources/project_assets_2_2_additional.json b/test/Microsoft.ComponentDetection.Detectors.Tests/Resources/project_assets_2_2_additional.json
new file mode 100644
index 000000000..8de9bf309
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/Resources/project_assets_2_2_additional.json
@@ -0,0 +1,4294 @@
+{
+	"version": 3,
+	"targets": {
+	  ".NETCoreApp,Version=v2.2": {
+		"coverlet.msbuild/2.5.1": {
+		  "type": "package",
+		  "build": {
+			"build/netstandard2.0/coverlet.msbuild.props": {},
+			"build/netstandard2.0/coverlet.msbuild.targets": {}
+		  }
+		},
+		"DotNet.Glob/2.1.1": {
+		  "type": "package",
+		  "dependencies": {
+			"NETStandard.Library": "1.6.1"
+		  },
+		  "compile": {
+			"lib/netstandard1.1/DotNet.Glob.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard1.1/DotNet.Glob.dll": {}
+		  }
+		},
+		"Microsoft.AspNet.WebApi.Client/5.2.7": {
+		  "type": "package",
+		  "dependencies": {
+			"Newtonsoft.Json": "10.0.1",
+			"Newtonsoft.Json.Bson": "1.0.1"
+		  },
+		  "compile": {
+			"lib/netstandard2.0/_._": {}
+		  },
+		  "runtime": {
+			"lib/netstandard2.0/System.Net.Http.Formatting.dll": {}
+		  }
+		},
+		"Microsoft.NETCore.App/2.2.8": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.DotNetHostPolicy": "2.2.8",
+			"Microsoft.NETCore.Platforms": "2.2.4",
+			"Microsoft.NETCore.Targets": "2.0.0",
+			"NETStandard.Library": "2.0.3"
+		  },
+		  "compile": {
+			"ref/netcoreapp2.2/Microsoft.CSharp.dll": {},
+			"ref/netcoreapp2.2/Microsoft.VisualBasic.dll": {},
+			"ref/netcoreapp2.2/Microsoft.Win32.Primitives.dll": {},
+			"ref/netcoreapp2.2/System.AppContext.dll": {},
+			"ref/netcoreapp2.2/System.Buffers.dll": {},
+			"ref/netcoreapp2.2/System.Collections.Concurrent.dll": {},
+			"ref/netcoreapp2.2/System.Collections.Immutable.dll": {},
+			"ref/netcoreapp2.2/System.Collections.NonGeneric.dll": {},
+			"ref/netcoreapp2.2/System.Collections.Specialized.dll": {},
+			"ref/netcoreapp2.2/System.Collections.dll": {},
+			"ref/netcoreapp2.2/System.ComponentModel.Annotations.dll": {},
+			"ref/netcoreapp2.2/System.ComponentModel.DataAnnotations.dll": {},
+			"ref/netcoreapp2.2/System.ComponentModel.EventBasedAsync.dll": {},
+			"ref/netcoreapp2.2/System.ComponentModel.Primitives.dll": {},
+			"ref/netcoreapp2.2/System.ComponentModel.TypeConverter.dll": {},
+			"ref/netcoreapp2.2/System.ComponentModel.dll": {},
+			"ref/netcoreapp2.2/System.Configuration.dll": {},
+			"ref/netcoreapp2.2/System.Console.dll": {},
+			"ref/netcoreapp2.2/System.Core.dll": {},
+			"ref/netcoreapp2.2/System.Data.Common.dll": {},
+			"ref/netcoreapp2.2/System.Data.dll": {},
+			"ref/netcoreapp2.2/System.Diagnostics.Contracts.dll": {},
+			"ref/netcoreapp2.2/System.Diagnostics.Debug.dll": {},
+			"ref/netcoreapp2.2/System.Diagnostics.DiagnosticSource.dll": {},
+			"ref/netcoreapp2.2/System.Diagnostics.FileVersionInfo.dll": {},
+			"ref/netcoreapp2.2/System.Diagnostics.Process.dll": {},
+			"ref/netcoreapp2.2/System.Diagnostics.StackTrace.dll": {},
+			"ref/netcoreapp2.2/System.Diagnostics.TextWriterTraceListener.dll": {},
+			"ref/netcoreapp2.2/System.Diagnostics.Tools.dll": {},
+			"ref/netcoreapp2.2/System.Diagnostics.TraceSource.dll": {},
+			"ref/netcoreapp2.2/System.Diagnostics.Tracing.dll": {},
+			"ref/netcoreapp2.2/System.Drawing.Primitives.dll": {},
+			"ref/netcoreapp2.2/System.Drawing.dll": {},
+			"ref/netcoreapp2.2/System.Dynamic.Runtime.dll": {},
+			"ref/netcoreapp2.2/System.Globalization.Calendars.dll": {},
+			"ref/netcoreapp2.2/System.Globalization.Extensions.dll": {},
+			"ref/netcoreapp2.2/System.Globalization.dll": {},
+			"ref/netcoreapp2.2/System.IO.Compression.Brotli.dll": {},
+			"ref/netcoreapp2.2/System.IO.Compression.FileSystem.dll": {},
+			"ref/netcoreapp2.2/System.IO.Compression.ZipFile.dll": {},
+			"ref/netcoreapp2.2/System.IO.Compression.dll": {},
+			"ref/netcoreapp2.2/System.IO.FileSystem.DriveInfo.dll": {},
+			"ref/netcoreapp2.2/System.IO.FileSystem.Primitives.dll": {},
+			"ref/netcoreapp2.2/System.IO.FileSystem.Watcher.dll": {},
+			"ref/netcoreapp2.2/System.IO.FileSystem.dll": {},
+			"ref/netcoreapp2.2/System.IO.IsolatedStorage.dll": {},
+			"ref/netcoreapp2.2/System.IO.MemoryMappedFiles.dll": {},
+			"ref/netcoreapp2.2/System.IO.Pipes.dll": {},
+			"ref/netcoreapp2.2/System.IO.UnmanagedMemoryStream.dll": {},
+			"ref/netcoreapp2.2/System.IO.dll": {},
+			"ref/netcoreapp2.2/System.Linq.Expressions.dll": {},
+			"ref/netcoreapp2.2/System.Linq.Parallel.dll": {},
+			"ref/netcoreapp2.2/System.Linq.Queryable.dll": {},
+			"ref/netcoreapp2.2/System.Linq.dll": {},
+			"ref/netcoreapp2.2/System.Memory.dll": {},
+			"ref/netcoreapp2.2/System.Net.Http.dll": {},
+			"ref/netcoreapp2.2/System.Net.HttpListener.dll": {},
+			"ref/netcoreapp2.2/System.Net.Mail.dll": {},
+			"ref/netcoreapp2.2/System.Net.NameResolution.dll": {},
+			"ref/netcoreapp2.2/System.Net.NetworkInformation.dll": {},
+			"ref/netcoreapp2.2/System.Net.Ping.dll": {},
+			"ref/netcoreapp2.2/System.Net.Primitives.dll": {},
+			"ref/netcoreapp2.2/System.Net.Requests.dll": {},
+			"ref/netcoreapp2.2/System.Net.Security.dll": {},
+			"ref/netcoreapp2.2/System.Net.ServicePoint.dll": {},
+			"ref/netcoreapp2.2/System.Net.Sockets.dll": {},
+			"ref/netcoreapp2.2/System.Net.WebClient.dll": {},
+			"ref/netcoreapp2.2/System.Net.WebHeaderCollection.dll": {},
+			"ref/netcoreapp2.2/System.Net.WebProxy.dll": {},
+			"ref/netcoreapp2.2/System.Net.WebSockets.Client.dll": {},
+			"ref/netcoreapp2.2/System.Net.WebSockets.dll": {},
+			"ref/netcoreapp2.2/System.Net.dll": {},
+			"ref/netcoreapp2.2/System.Numerics.Vectors.dll": {},
+			"ref/netcoreapp2.2/System.Numerics.dll": {},
+			"ref/netcoreapp2.2/System.ObjectModel.dll": {},
+			"ref/netcoreapp2.2/System.Reflection.DispatchProxy.dll": {},
+			"ref/netcoreapp2.2/System.Reflection.Emit.ILGeneration.dll": {},
+			"ref/netcoreapp2.2/System.Reflection.Emit.Lightweight.dll": {},
+			"ref/netcoreapp2.2/System.Reflection.Emit.dll": {},
+			"ref/netcoreapp2.2/System.Reflection.Extensions.dll": {},
+			"ref/netcoreapp2.2/System.Reflection.Metadata.dll": {},
+			"ref/netcoreapp2.2/System.Reflection.Primitives.dll": {},
+			"ref/netcoreapp2.2/System.Reflection.TypeExtensions.dll": {},
+			"ref/netcoreapp2.2/System.Reflection.dll": {},
+			"ref/netcoreapp2.2/System.Resources.Reader.dll": {},
+			"ref/netcoreapp2.2/System.Resources.ResourceManager.dll": {},
+			"ref/netcoreapp2.2/System.Resources.Writer.dll": {},
+			"ref/netcoreapp2.2/System.Runtime.CompilerServices.VisualC.dll": {},
+			"ref/netcoreapp2.2/System.Runtime.Extensions.dll": {},
+			"ref/netcoreapp2.2/System.Runtime.Handles.dll": {},
+			"ref/netcoreapp2.2/System.Runtime.InteropServices.RuntimeInformation.dll": {},
+			"ref/netcoreapp2.2/System.Runtime.InteropServices.WindowsRuntime.dll": {},
+			"ref/netcoreapp2.2/System.Runtime.InteropServices.dll": {},
+			"ref/netcoreapp2.2/System.Runtime.Loader.dll": {},
+			"ref/netcoreapp2.2/System.Runtime.Numerics.dll": {},
+			"ref/netcoreapp2.2/System.Runtime.Serialization.Formatters.dll": {},
+			"ref/netcoreapp2.2/System.Runtime.Serialization.Json.dll": {},
+			"ref/netcoreapp2.2/System.Runtime.Serialization.Primitives.dll": {},
+			"ref/netcoreapp2.2/System.Runtime.Serialization.Xml.dll": {},
+			"ref/netcoreapp2.2/System.Runtime.Serialization.dll": {},
+			"ref/netcoreapp2.2/System.Runtime.dll": {},
+			"ref/netcoreapp2.2/System.Security.Claims.dll": {},
+			"ref/netcoreapp2.2/System.Security.Cryptography.Algorithms.dll": {},
+			"ref/netcoreapp2.2/System.Security.Cryptography.Csp.dll": {},
+			"ref/netcoreapp2.2/System.Security.Cryptography.Encoding.dll": {},
+			"ref/netcoreapp2.2/System.Security.Cryptography.Primitives.dll": {},
+			"ref/netcoreapp2.2/System.Security.Cryptography.X509Certificates.dll": {},
+			"ref/netcoreapp2.2/System.Security.Principal.dll": {},
+			"ref/netcoreapp2.2/System.Security.SecureString.dll": {},
+			"ref/netcoreapp2.2/System.Security.dll": {},
+			"ref/netcoreapp2.2/System.ServiceModel.Web.dll": {},
+			"ref/netcoreapp2.2/System.ServiceProcess.dll": {},
+			"ref/netcoreapp2.2/System.Text.Encoding.Extensions.dll": {},
+			"ref/netcoreapp2.2/System.Text.Encoding.dll": {},
+			"ref/netcoreapp2.2/System.Text.RegularExpressions.dll": {},
+			"ref/netcoreapp2.2/System.Threading.Overlapped.dll": {},
+			"ref/netcoreapp2.2/System.Threading.Tasks.Dataflow.dll": {},
+			"ref/netcoreapp2.2/System.Threading.Tasks.Extensions.dll": {},
+			"ref/netcoreapp2.2/System.Threading.Tasks.Parallel.dll": {},
+			"ref/netcoreapp2.2/System.Threading.Tasks.dll": {},
+			"ref/netcoreapp2.2/System.Threading.Thread.dll": {},
+			"ref/netcoreapp2.2/System.Threading.ThreadPool.dll": {},
+			"ref/netcoreapp2.2/System.Threading.Timer.dll": {},
+			"ref/netcoreapp2.2/System.Threading.dll": {},
+			"ref/netcoreapp2.2/System.Transactions.Local.dll": {},
+			"ref/netcoreapp2.2/System.Transactions.dll": {},
+			"ref/netcoreapp2.2/System.ValueTuple.dll": {},
+			"ref/netcoreapp2.2/System.Web.HttpUtility.dll": {},
+			"ref/netcoreapp2.2/System.Web.dll": {},
+			"ref/netcoreapp2.2/System.Windows.dll": {},
+			"ref/netcoreapp2.2/System.Xml.Linq.dll": {},
+			"ref/netcoreapp2.2/System.Xml.ReaderWriter.dll": {},
+			"ref/netcoreapp2.2/System.Xml.Serialization.dll": {},
+			"ref/netcoreapp2.2/System.Xml.XDocument.dll": {},
+			"ref/netcoreapp2.2/System.Xml.XPath.XDocument.dll": {},
+			"ref/netcoreapp2.2/System.Xml.XPath.dll": {},
+			"ref/netcoreapp2.2/System.Xml.XmlDocument.dll": {},
+			"ref/netcoreapp2.2/System.Xml.XmlSerializer.dll": {},
+			"ref/netcoreapp2.2/System.Xml.dll": {},
+			"ref/netcoreapp2.2/System.dll": {},
+			"ref/netcoreapp2.2/WindowsBase.dll": {},
+			"ref/netcoreapp2.2/mscorlib.dll": {},
+			"ref/netcoreapp2.2/netstandard.dll": {}
+		  },
+		  "build": {
+			"build/netcoreapp2.2/Microsoft.NETCore.App.props": {},
+			"build/netcoreapp2.2/Microsoft.NETCore.App.targets": {}
+		  }
+		},
+		"Microsoft.NETCore.DotNetAppHost/2.2.8": {
+		  "type": "package"
+		},
+		"Microsoft.NETCore.DotNetHostPolicy/2.2.8": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.DotNetHostResolver": "2.2.8"
+		  }
+		},
+		"Microsoft.NETCore.DotNetHostResolver/2.2.8": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.DotNetAppHost": "2.2.8"
+		  }
+		},
+		"Microsoft.NETCore.Platforms/2.2.4": {
+		  "type": "package",
+		  "compile": {
+			"lib/netstandard1.0/_._": {}
+		  },
+		  "runtime": {
+			"lib/netstandard1.0/_._": {}
+		  }
+		},
+		"Microsoft.NETCore.Targets/2.0.0": {
+		  "type": "package",
+		  "compile": {
+			"lib/netstandard1.0/_._": {}
+		  },
+		  "runtime": {
+			"lib/netstandard1.0/_._": {}
+		  }
+		},
+		"Microsoft.Win32.Primitives/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.Platforms": "1.1.0",
+			"Microsoft.NETCore.Targets": "1.1.0",
+			"System.Runtime": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.3/_._": {}
+		  }
+		},
+		"Microsoft.Win32.Registry/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.Platforms": "1.1.0",
+			"System.Collections": "4.3.0",
+			"System.Globalization": "4.3.0",
+			"System.Resources.ResourceManager": "4.3.0",
+			"System.Runtime": "4.3.0",
+			"System.Runtime.Extensions": "4.3.0",
+			"System.Runtime.Handles": "4.3.0",
+			"System.Runtime.InteropServices": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.3/_._": {}
+		  },
+		  "runtimeTargets": {
+			"runtimes/unix/lib/netstandard1.3/Microsoft.Win32.Registry.dll": {
+			  "assetType": "runtime",
+			  "rid": "unix"
+			},
+			"runtimes/win/lib/netstandard1.3/Microsoft.Win32.Registry.dll": {
+			  "assetType": "runtime",
+			  "rid": "win"
+			}
+		  }
+		},
+		"MinVer/2.5.0": {
+		  "type": "package",
+		  "build": {
+			"build/MinVer.targets": {}
+		  },
+		  "buildMultiTargeting": {
+			"buildCrossTargeting/MinVer.targets": {}
+		  }
+		},
+		"NETStandard.Library/2.0.3": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.Platforms": "1.1.0"
+		  },
+		  "compile": {
+			"lib/netstandard1.0/_._": {}
+		  },
+		  "runtime": {
+			"lib/netstandard1.0/_._": {}
+		  },
+		  "build": {
+			"build/netstandard2.0/NETStandard.Library.targets": {}
+		  }
+		},
+		"Nett/0.10.0": {
+		  "type": "package",
+		  "compile": {
+			"lib/netstandard2.0/Nett.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard2.0/Nett.dll": {}
+		  }
+		},
+		"Newtonsoft.Json/12.0.3": {
+		  "type": "package",
+		  "compile": {
+			"lib/netstandard2.0/Newtonsoft.Json.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard2.0/Newtonsoft.Json.dll": {}
+		  }
+		},
+		"Newtonsoft.Json.Bson/1.0.1": {
+		  "type": "package",
+		  "dependencies": {
+			"NETStandard.Library": "1.6.1",
+			"Newtonsoft.Json": "10.0.1"
+		  },
+		  "compile": {
+			"lib/netstandard1.3/_._": {}
+		  },
+		  "runtime": {
+			"lib/netstandard1.3/Newtonsoft.Json.Bson.dll": {}
+		  }
+		},
+		"NuGet.Common/5.6.0": {
+		  "type": "package",
+		  "dependencies": {
+			"NuGet.Frameworks": "5.6.0",
+			"System.Diagnostics.Process": "4.3.0",
+			"System.Threading.Thread": "4.3.0"
+		  },
+		  "compile": {
+			"lib/netstandard2.0/NuGet.Common.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard2.0/NuGet.Common.dll": {}
+		  }
+		},
+		"NuGet.Configuration/5.6.0": {
+		  "type": "package",
+		  "dependencies": {
+			"NuGet.Common": "5.6.0",
+			"System.Security.Cryptography.ProtectedData": "4.3.0"
+		  },
+		  "compile": {
+			"lib/netstandard2.0/NuGet.Configuration.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard2.0/NuGet.Configuration.dll": {}
+		  }
+		},
+		"NuGet.DependencyResolver.Core/5.6.0": {
+		  "type": "package",
+		  "dependencies": {
+			"NuGet.LibraryModel": "5.6.0",
+			"NuGet.Protocol": "5.6.0"
+		  },
+		  "compile": {
+			"lib/netstandard2.0/NuGet.DependencyResolver.Core.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard2.0/NuGet.DependencyResolver.Core.dll": {}
+		  }
+		},
+		"NuGet.Frameworks/5.6.0": {
+		  "type": "package",
+		  "compile": {
+			"lib/netstandard2.0/NuGet.Frameworks.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard2.0/NuGet.Frameworks.dll": {}
+		  }
+		},
+		"NuGet.LibraryModel/5.6.0": {
+		  "type": "package",
+		  "dependencies": {
+			"NuGet.Common": "5.6.0",
+			"NuGet.Versioning": "5.6.0"
+		  },
+		  "compile": {
+			"lib/netstandard2.0/NuGet.LibraryModel.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard2.0/NuGet.LibraryModel.dll": {}
+		  }
+		},
+		"NuGet.Packaging/5.6.0": {
+		  "type": "package",
+		  "dependencies": {
+			"Newtonsoft.Json": "9.0.1",
+			"NuGet.Configuration": "5.6.0",
+			"NuGet.Versioning": "5.6.0",
+			"System.Dynamic.Runtime": "4.3.0"
+		  },
+		  "compile": {
+			"lib/netstandard2.0/NuGet.Packaging.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard2.0/NuGet.Packaging.dll": {}
+		  }
+		},
+		"NuGet.ProjectModel/5.6.0": {
+		  "type": "package",
+		  "dependencies": {
+			"NuGet.DependencyResolver.Core": "5.6.0",
+			"System.Dynamic.Runtime": "4.3.0",
+			"System.Threading.Thread": "4.3.0"
+		  },
+		  "compile": {
+			"lib/netstandard2.0/NuGet.ProjectModel.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard2.0/NuGet.ProjectModel.dll": {}
+		  }
+		},
+		"NuGet.Protocol/5.6.0": {
+		  "type": "package",
+		  "dependencies": {
+			"NuGet.Packaging": "5.6.0",
+			"System.Dynamic.Runtime": "4.3.0"
+		  },
+		  "compile": {
+			"lib/netstandard2.0/NuGet.Protocol.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard2.0/NuGet.Protocol.dll": {}
+		  }
+		},
+		"NuGet.Versioning/5.6.0": {
+		  "type": "package",
+		  "compile": {
+			"lib/netstandard2.0/NuGet.Versioning.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard2.0/NuGet.Versioning.dll": {}
+		  }
+		},
+		"Polly/7.0.3": {
+		  "type": "package",
+		  "compile": {
+			"lib/netstandard2.0/Polly.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard2.0/Polly.dll": {}
+		  }
+		},
+		"runtime.native.System/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.Platforms": "1.1.0",
+			"Microsoft.NETCore.Targets": "1.1.0"
+		  },
+		  "compile": {
+			"lib/netstandard1.0/_._": {}
+		  },
+		  "runtime": {
+			"lib/netstandard1.0/_._": {}
+		  }
+		},
+		"SemanticVersioning/1.2.0": {
+		  "type": "package",
+		  "compile": {
+			"lib/netstandard2.0/SemVer.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard2.0/SemVer.dll": {}
+		  }
+		},
+		"StyleCop.Analyzers/1.0.2": {
+		  "type": "package"
+		},
+		"System.Collections/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.Platforms": "1.1.0",
+			"Microsoft.NETCore.Targets": "1.1.0",
+			"System.Runtime": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.3/_._": {}
+		  }
+		},
+		"System.Composition.AttributedModel/1.4.0": {
+		  "type": "package",
+		  "compile": {
+			"lib/netstandard2.0/System.Composition.AttributedModel.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard2.0/System.Composition.AttributedModel.dll": {}
+		  }
+		},
+		"System.Composition.Convention/1.4.0": {
+		  "type": "package",
+		  "dependencies": {
+			"System.Composition.AttributedModel": "1.4.0"
+		  },
+		  "compile": {
+			"lib/netstandard2.0/System.Composition.Convention.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard2.0/System.Composition.Convention.dll": {}
+		  }
+		},
+		"System.Composition.Hosting/1.4.0": {
+		  "type": "package",
+		  "dependencies": {
+			"System.Composition.Runtime": "1.4.0"
+		  },
+		  "compile": {
+			"lib/netstandard2.0/System.Composition.Hosting.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard2.0/System.Composition.Hosting.dll": {}
+		  }
+		},
+		"System.Composition.Runtime/1.4.0": {
+		  "type": "package",
+		  "compile": {
+			"lib/netstandard2.0/System.Composition.Runtime.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard2.0/System.Composition.Runtime.dll": {}
+		  }
+		},
+		"System.Composition.TypedParts/1.4.0": {
+		  "type": "package",
+		  "dependencies": {
+			"System.Composition.AttributedModel": "1.4.0",
+			"System.Composition.Hosting": "1.4.0",
+			"System.Composition.Runtime": "1.4.0"
+		  },
+		  "compile": {
+			"lib/netstandard2.0/System.Composition.TypedParts.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard2.0/System.Composition.TypedParts.dll": {}
+		  }
+		},
+		"System.Diagnostics.Debug/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.Platforms": "1.1.0",
+			"Microsoft.NETCore.Targets": "1.1.0",
+			"System.Runtime": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.3/_._": {}
+		  }
+		},
+		"System.Diagnostics.Process/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.Platforms": "1.1.0",
+			"Microsoft.Win32.Primitives": "4.3.0",
+			"Microsoft.Win32.Registry": "4.3.0",
+			"System.Collections": "4.3.0",
+			"System.Diagnostics.Debug": "4.3.0",
+			"System.Globalization": "4.3.0",
+			"System.IO": "4.3.0",
+			"System.IO.FileSystem": "4.3.0",
+			"System.IO.FileSystem.Primitives": "4.3.0",
+			"System.Resources.ResourceManager": "4.3.0",
+			"System.Runtime": "4.3.0",
+			"System.Runtime.Extensions": "4.3.0",
+			"System.Runtime.Handles": "4.3.0",
+			"System.Runtime.InteropServices": "4.3.0",
+			"System.Text.Encoding": "4.3.0",
+			"System.Text.Encoding.Extensions": "4.3.0",
+			"System.Threading": "4.3.0",
+			"System.Threading.Tasks": "4.3.0",
+			"System.Threading.Thread": "4.3.0",
+			"System.Threading.ThreadPool": "4.3.0",
+			"runtime.native.System": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.4/System.Diagnostics.Process.dll": {}
+		  },
+		  "runtimeTargets": {
+			"runtimes/linux/lib/netstandard1.4/System.Diagnostics.Process.dll": {
+			  "assetType": "runtime",
+			  "rid": "linux"
+			},
+			"runtimes/osx/lib/netstandard1.4/System.Diagnostics.Process.dll": {
+			  "assetType": "runtime",
+			  "rid": "osx"
+			},
+			"runtimes/win/lib/netstandard1.4/System.Diagnostics.Process.dll": {
+			  "assetType": "runtime",
+			  "rid": "win"
+			}
+		  }
+		},
+		"System.Dynamic.Runtime/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"System.Collections": "4.3.0",
+			"System.Diagnostics.Debug": "4.3.0",
+			"System.Linq": "4.3.0",
+			"System.Linq.Expressions": "4.3.0",
+			"System.ObjectModel": "4.3.0",
+			"System.Reflection": "4.3.0",
+			"System.Reflection.Emit": "4.3.0",
+			"System.Reflection.Emit.ILGeneration": "4.3.0",
+			"System.Reflection.Primitives": "4.3.0",
+			"System.Reflection.TypeExtensions": "4.3.0",
+			"System.Resources.ResourceManager": "4.3.0",
+			"System.Runtime": "4.3.0",
+			"System.Runtime.Extensions": "4.3.0",
+			"System.Threading": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.3/System.Dynamic.Runtime.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard1.3/System.Dynamic.Runtime.dll": {}
+		  }
+		},
+		"System.Globalization/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.Platforms": "1.1.0",
+			"Microsoft.NETCore.Targets": "1.1.0",
+			"System.Runtime": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.3/_._": {}
+		  }
+		},
+		"System.IO/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.Platforms": "1.1.0",
+			"Microsoft.NETCore.Targets": "1.1.0",
+			"System.Runtime": "4.3.0",
+			"System.Text.Encoding": "4.3.0",
+			"System.Threading.Tasks": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.5/System.IO.dll": {}
+		  }
+		},
+		"System.IO.FileSystem/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.Platforms": "1.1.0",
+			"Microsoft.NETCore.Targets": "1.1.0",
+			"System.IO": "4.3.0",
+			"System.IO.FileSystem.Primitives": "4.3.0",
+			"System.Runtime": "4.3.0",
+			"System.Runtime.Handles": "4.3.0",
+			"System.Text.Encoding": "4.3.0",
+			"System.Threading.Tasks": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.3/_._": {}
+		  }
+		},
+		"System.IO.FileSystem.Primitives/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"System.Runtime": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.3/_._": {}
+		  },
+		  "runtime": {
+			"lib/netstandard1.3/System.IO.FileSystem.Primitives.dll": {}
+		  }
+		},
+		"System.Linq/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"System.Collections": "4.3.0",
+			"System.Diagnostics.Debug": "4.3.0",
+			"System.Resources.ResourceManager": "4.3.0",
+			"System.Runtime": "4.3.0",
+			"System.Runtime.Extensions": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.6/_._": {}
+		  },
+		  "runtime": {
+			"lib/netstandard1.6/System.Linq.dll": {}
+		  }
+		},
+		"System.Linq.Expressions/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"System.Collections": "4.3.0",
+			"System.Diagnostics.Debug": "4.3.0",
+			"System.Globalization": "4.3.0",
+			"System.IO": "4.3.0",
+			"System.Linq": "4.3.0",
+			"System.ObjectModel": "4.3.0",
+			"System.Reflection": "4.3.0",
+			"System.Reflection.Emit": "4.3.0",
+			"System.Reflection.Emit.ILGeneration": "4.3.0",
+			"System.Reflection.Emit.Lightweight": "4.3.0",
+			"System.Reflection.Extensions": "4.3.0",
+			"System.Reflection.Primitives": "4.3.0",
+			"System.Reflection.TypeExtensions": "4.3.0",
+			"System.Resources.ResourceManager": "4.3.0",
+			"System.Runtime": "4.3.0",
+			"System.Runtime.Extensions": "4.3.0",
+			"System.Threading": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.6/System.Linq.Expressions.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard1.6/System.Linq.Expressions.dll": {}
+		  }
+		},
+		"System.ObjectModel/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"System.Collections": "4.3.0",
+			"System.Diagnostics.Debug": "4.3.0",
+			"System.Resources.ResourceManager": "4.3.0",
+			"System.Runtime": "4.3.0",
+			"System.Threading": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.3/System.ObjectModel.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard1.3/System.ObjectModel.dll": {}
+		  }
+		},
+		"System.Reactive/4.1.2": {
+		  "type": "package",
+		  "dependencies": {
+			"System.Runtime.InteropServices.WindowsRuntime": "4.3.0",
+			"System.Threading.Tasks.Extensions": "4.5.1"
+		  },
+		  "compile": {
+			"lib/netstandard2.0/System.Reactive.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard2.0/System.Reactive.dll": {}
+		  }
+		},
+		"System.Reflection/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.Platforms": "1.1.0",
+			"Microsoft.NETCore.Targets": "1.1.0",
+			"System.IO": "4.3.0",
+			"System.Reflection.Primitives": "4.3.0",
+			"System.Runtime": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.5/System.Reflection.dll": {}
+		  }
+		},
+		"System.Reflection.Emit/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"System.IO": "4.3.0",
+			"System.Reflection": "4.3.0",
+			"System.Reflection.Emit.ILGeneration": "4.3.0",
+			"System.Reflection.Primitives": "4.3.0",
+			"System.Runtime": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.1/_._": {}
+		  },
+		  "runtime": {
+			"lib/netstandard1.3/System.Reflection.Emit.dll": {}
+		  }
+		},
+		"System.Reflection.Emit.ILGeneration/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"System.Reflection": "4.3.0",
+			"System.Reflection.Primitives": "4.3.0",
+			"System.Runtime": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.0/_._": {}
+		  },
+		  "runtime": {
+			"lib/netstandard1.3/System.Reflection.Emit.ILGeneration.dll": {}
+		  }
+		},
+		"System.Reflection.Emit.Lightweight/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"System.Reflection": "4.3.0",
+			"System.Reflection.Emit.ILGeneration": "4.3.0",
+			"System.Reflection.Primitives": "4.3.0",
+			"System.Runtime": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.0/_._": {}
+		  },
+		  "runtime": {
+			"lib/netstandard1.3/System.Reflection.Emit.Lightweight.dll": {}
+		  }
+		},
+		"System.Reflection.Extensions/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.Platforms": "1.1.0",
+			"Microsoft.NETCore.Targets": "1.1.0",
+			"System.Reflection": "4.3.0",
+			"System.Runtime": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.0/_._": {}
+		  }
+		},
+		"System.Reflection.Primitives/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.Platforms": "1.1.0",
+			"Microsoft.NETCore.Targets": "1.1.0",
+			"System.Runtime": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.0/System.Reflection.Primitives.dll": {}
+		  }
+		},
+		"System.Reflection.TypeExtensions/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"System.Reflection": "4.3.0",
+			"System.Runtime": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.5/_._": {}
+		  },
+		  "runtime": {
+			"lib/netstandard1.5/System.Reflection.TypeExtensions.dll": {}
+		  }
+		},
+		"System.Resources.ResourceManager/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.Platforms": "1.1.0",
+			"Microsoft.NETCore.Targets": "1.1.0",
+			"System.Globalization": "4.3.0",
+			"System.Reflection": "4.3.0",
+			"System.Runtime": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.0/_._": {}
+		  }
+		},
+		"System.Runtime/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.Platforms": "1.1.0",
+			"Microsoft.NETCore.Targets": "1.1.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.5/System.Runtime.dll": {}
+		  }
+		},
+		"System.Runtime.Extensions/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.Platforms": "1.1.0",
+			"Microsoft.NETCore.Targets": "1.1.0",
+			"System.Runtime": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.5/_._": {}
+		  }
+		},
+		"System.Runtime.Handles/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.Platforms": "1.1.0",
+			"Microsoft.NETCore.Targets": "1.1.0",
+			"System.Runtime": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.3/System.Runtime.Handles.dll": {}
+		  }
+		},
+		"System.Runtime.InteropServices/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.Platforms": "1.1.0",
+			"Microsoft.NETCore.Targets": "1.1.0",
+			"System.Reflection": "4.3.0",
+			"System.Reflection.Primitives": "4.3.0",
+			"System.Runtime": "4.3.0",
+			"System.Runtime.Handles": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netcoreapp1.1/_._": {}
+		  }
+		},
+		"System.Runtime.InteropServices.WindowsRuntime/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"System.Runtime": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.0/System.Runtime.InteropServices.WindowsRuntime.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard1.3/System.Runtime.InteropServices.WindowsRuntime.dll": {}
+		  }
+		},
+		"System.Security.Cryptography.Primitives/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"System.Diagnostics.Debug": "4.3.0",
+			"System.Globalization": "4.3.0",
+			"System.IO": "4.3.0",
+			"System.Resources.ResourceManager": "4.3.0",
+			"System.Runtime": "4.3.0",
+			"System.Threading": "4.3.0",
+			"System.Threading.Tasks": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.3/_._": {}
+		  },
+		  "runtime": {
+			"lib/netstandard1.3/System.Security.Cryptography.Primitives.dll": {}
+		  }
+		},
+		"System.Security.Cryptography.ProtectedData/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.Platforms": "1.1.0",
+			"System.Resources.ResourceManager": "4.3.0",
+			"System.Runtime": "4.3.0",
+			"System.Runtime.InteropServices": "4.3.0",
+			"System.Security.Cryptography.Primitives": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.3/System.Security.Cryptography.ProtectedData.dll": {}
+		  },
+		  "runtimeTargets": {
+			"runtimes/unix/lib/netstandard1.3/System.Security.Cryptography.ProtectedData.dll": {
+			  "assetType": "runtime",
+			  "rid": "unix"
+			},
+			"runtimes/win/lib/netstandard1.3/System.Security.Cryptography.ProtectedData.dll": {
+			  "assetType": "runtime",
+			  "rid": "win"
+			}
+		  }
+		},
+		"System.Text.Encoding/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.Platforms": "1.1.0",
+			"Microsoft.NETCore.Targets": "1.1.0",
+			"System.Runtime": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.3/System.Text.Encoding.dll": {}
+		  }
+		},
+		"System.Text.Encoding.Extensions/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.Platforms": "1.1.0",
+			"Microsoft.NETCore.Targets": "1.1.0",
+			"System.Runtime": "4.3.0",
+			"System.Text.Encoding": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.3/_._": {}
+		  }
+		},
+		"System.Threading/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"System.Runtime": "4.3.0",
+			"System.Threading.Tasks": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.3/_._": {}
+		  },
+		  "runtime": {
+			"lib/netstandard1.3/System.Threading.dll": {}
+		  }
+		},
+		"System.Threading.Tasks/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"Microsoft.NETCore.Platforms": "1.1.0",
+			"Microsoft.NETCore.Targets": "1.1.0",
+			"System.Runtime": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.3/System.Threading.Tasks.dll": {}
+		  }
+		},
+		"System.Threading.Tasks.Dataflow/4.9.0": {
+		  "type": "package",
+		  "compile": {
+			"lib/netstandard2.0/System.Threading.Tasks.Dataflow.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard2.0/System.Threading.Tasks.Dataflow.dll": {}
+		  }
+		},
+		"System.Threading.Tasks.Extensions/4.5.1": {
+		  "type": "package",
+		  "compile": {
+			"ref/netcoreapp2.1/_._": {}
+		  },
+		  "runtime": {
+			"lib/netcoreapp2.1/_._": {}
+		  }
+		},
+		"System.Threading.Thread/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"System.Runtime": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.3/System.Threading.Thread.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard1.3/System.Threading.Thread.dll": {}
+		  }
+		},
+		"System.Threading.ThreadPool/4.3.0": {
+		  "type": "package",
+		  "dependencies": {
+			"System.Runtime": "4.3.0",
+			"System.Runtime.Handles": "4.3.0"
+		  },
+		  "compile": {
+			"ref/netstandard1.3/_._": {}
+		  },
+		  "runtime": {
+			"lib/netstandard1.3/System.Threading.ThreadPool.dll": {}
+		  }
+		},
+		"YamlDotNet/5.3.0": {
+		  "type": "package",
+		  "compile": {
+			"lib/netstandard1.3/YamlDotNet.dll": {}
+		  },
+		  "runtime": {
+			"lib/netstandard1.3/YamlDotNet.dll": {}
+		  }
+		},
+		"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common/1.0.0": {
+		  "type": "project",
+		  "framework": ".NETCoreApp,Version=v2.2",
+		  "dependencies": {
+			"Microsoft.AspNet.WebApi.Client": "5.2.7",
+			"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts": "1.0.0",
+			"Newtonsoft.Json": "12.0.3",
+			"System.Composition.AttributedModel": "1.4.0",
+			"System.Composition.Convention": "1.4.0",
+			"System.Composition.Hosting": "1.4.0",
+			"System.Composition.Runtime": "1.4.0",
+			"System.Composition.TypedParts": "1.4.0",
+			"System.Reactive": "4.1.2"
+		  },
+		  "compile": {
+			"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common.dll": {}
+		  },
+		  "runtime": {
+			"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common.dll": {}
+		  }
+		},
+		"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts/1.0.0": {
+		  "type": "project",
+		  "framework": ".NETCoreApp,Version=v2.2",
+		  "dependencies": {
+			"Newtonsoft.Json": "12.0.3",
+			"System.Composition.AttributedModel": "1.4.0",
+			"System.Reactive": "4.1.2"
+		  },
+		  "compile": {
+			"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts.dll": {}
+		  },
+		  "runtime": {
+			"bin/placeholder/Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts.dll": {}
+		  }
+		}
+	  }
+	},
+	"libraries": {
+	  "coverlet.msbuild/2.5.1": {
+		"sha512": "+2+jvqh6pMWEDUhx3gkxo8zqxAPiac/2Ky8KpvuujJfM9JPne6eUiscnvt4hnKet2EKX+PpEiW4NQYdMMMzhqQ==",
+		"type": "package",
+		"path": "coverlet.msbuild/2.5.1",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "build/netstandard2.0/ConsoleTables.dll",
+		  "build/netstandard2.0/Microsoft.Build.Framework.dll",
+		  "build/netstandard2.0/Microsoft.Build.Utilities.Core.dll",
+		  "build/netstandard2.0/Microsoft.Extensions.FileSystemGlobbing.dll",
+		  "build/netstandard2.0/Mono.Cecil.Mdb.dll",
+		  "build/netstandard2.0/Mono.Cecil.Pdb.dll",
+		  "build/netstandard2.0/Mono.Cecil.Rocks.dll",
+		  "build/netstandard2.0/Mono.Cecil.dll",
+		  "build/netstandard2.0/Newtonsoft.Json.dll",
+		  "build/netstandard2.0/coverlet.core.dll",
+		  "build/netstandard2.0/coverlet.msbuild.props",
+		  "build/netstandard2.0/coverlet.msbuild.targets",
+		  "build/netstandard2.0/coverlet.msbuild.tasks.deps.json",
+		  "build/netstandard2.0/coverlet.msbuild.tasks.dll",
+		  "build/netstandard2.0/coverlet.template.dll",
+		  "build/netstandard2.0/runtimes/unix/lib/netstandard1.3/System.Text.Encoding.CodePages.dll",
+		  "build/netstandard2.0/runtimes/win/lib/netstandard1.3/System.Text.Encoding.CodePages.dll",
+		  "coverlet.msbuild.2.5.1.nupkg.sha512",
+		  "coverlet.msbuild.nuspec"
+		]
+	  },
+	  "DotNet.Glob/2.1.1": {
+		"sha512": "bi9K+JHUZ0fZ1QqUI5MM1FY5WNMxIE4DSTBMMC5iFS/oxqcbcA5rwuYAsKwI/UHQOfyOLc/j5K24SN46h4Z+xQ==",
+		"type": "package",
+		"path": "dotnet.glob/2.1.1",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "dotnet.glob.2.1.1.nupkg.sha512",
+		  "dotnet.glob.nuspec",
+		  "lib/net40/DotNet.Glob.dll",
+		  "lib/net45/DotNet.Glob.dll",
+		  "lib/net46/DotNet.Glob.dll",
+		  "lib/netstandard1.1/DotNet.Glob.dll"
+		]
+	  },
+	  "Microsoft.AspNet.WebApi.Client/5.2.7": {
+		"sha512": "/76fAHknzvFqbznS6Uj2sOyE9rJB3PltY+f53TH8dX9RiGhk02EhuFCWljSj5nnqKaTsmma8DFR50OGyQ4yJ1g==",
+		"type": "package",
+		"path": "microsoft.aspnet.webapi.client/5.2.7",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "lib/net45/System.Net.Http.Formatting.dll",
+		  "lib/net45/System.Net.Http.Formatting.xml",
+		  "lib/netstandard2.0/System.Net.Http.Formatting.dll",
+		  "lib/netstandard2.0/System.Net.Http.Formatting.xml",
+		  "lib/portable-wp8+netcore45+net45+wp81+wpa81/System.Net.Http.Formatting.dll",
+		  "lib/portable-wp8+netcore45+net45+wp81+wpa81/System.Net.Http.Formatting.xml",
+		  "microsoft.aspnet.webapi.client.5.2.7.nupkg.sha512",
+		  "microsoft.aspnet.webapi.client.nuspec"
+		]
+	  },
+	  "Microsoft.NETCore.App/2.2.8": {
+		"sha512": "GOxlvyc8hFrnhDjYlm25JJ7PwoyeoOpZzcg6ZgF8n8l6VxezNupRkkTeA2ek1WsspN0CdAoA8e7iDVNU84/F+Q==",
+		"type": "package",
+		"path": "microsoft.netcore.app/2.2.8",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "LICENSE.TXT",
+		  "Microsoft.NETCore.App.versions.txt",
+		  "THIRD-PARTY-NOTICES.TXT",
+		  "build/netcoreapp2.2/Microsoft.NETCore.App.PlatformManifest.txt",
+		  "build/netcoreapp2.2/Microsoft.NETCore.App.props",
+		  "build/netcoreapp2.2/Microsoft.NETCore.App.targets",
+		  "microsoft.netcore.app.2.2.8.nupkg.sha512",
+		  "microsoft.netcore.app.nuspec",
+		  "ref/netcoreapp2.2/Microsoft.CSharp.dll",
+		  "ref/netcoreapp2.2/Microsoft.CSharp.xml",
+		  "ref/netcoreapp2.2/Microsoft.VisualBasic.dll",
+		  "ref/netcoreapp2.2/Microsoft.VisualBasic.xml",
+		  "ref/netcoreapp2.2/Microsoft.Win32.Primitives.dll",
+		  "ref/netcoreapp2.2/Microsoft.Win32.Primitives.xml",
+		  "ref/netcoreapp2.2/System.AppContext.dll",
+		  "ref/netcoreapp2.2/System.Buffers.dll",
+		  "ref/netcoreapp2.2/System.Buffers.xml",
+		  "ref/netcoreapp2.2/System.Collections.Concurrent.dll",
+		  "ref/netcoreapp2.2/System.Collections.Concurrent.xml",
+		  "ref/netcoreapp2.2/System.Collections.Immutable.dll",
+		  "ref/netcoreapp2.2/System.Collections.Immutable.xml",
+		  "ref/netcoreapp2.2/System.Collections.NonGeneric.dll",
+		  "ref/netcoreapp2.2/System.Collections.NonGeneric.xml",
+		  "ref/netcoreapp2.2/System.Collections.Specialized.dll",
+		  "ref/netcoreapp2.2/System.Collections.Specialized.xml",
+		  "ref/netcoreapp2.2/System.Collections.dll",
+		  "ref/netcoreapp2.2/System.Collections.xml",
+		  "ref/netcoreapp2.2/System.ComponentModel.Annotations.dll",
+		  "ref/netcoreapp2.2/System.ComponentModel.Annotations.xml",
+		  "ref/netcoreapp2.2/System.ComponentModel.DataAnnotations.dll",
+		  "ref/netcoreapp2.2/System.ComponentModel.EventBasedAsync.dll",
+		  "ref/netcoreapp2.2/System.ComponentModel.EventBasedAsync.xml",
+		  "ref/netcoreapp2.2/System.ComponentModel.Primitives.dll",
+		  "ref/netcoreapp2.2/System.ComponentModel.Primitives.xml",
+		  "ref/netcoreapp2.2/System.ComponentModel.TypeConverter.dll",
+		  "ref/netcoreapp2.2/System.ComponentModel.TypeConverter.xml",
+		  "ref/netcoreapp2.2/System.ComponentModel.dll",
+		  "ref/netcoreapp2.2/System.ComponentModel.xml",
+		  "ref/netcoreapp2.2/System.Configuration.dll",
+		  "ref/netcoreapp2.2/System.Console.dll",
+		  "ref/netcoreapp2.2/System.Console.xml",
+		  "ref/netcoreapp2.2/System.Core.dll",
+		  "ref/netcoreapp2.2/System.Data.Common.dll",
+		  "ref/netcoreapp2.2/System.Data.Common.xml",
+		  "ref/netcoreapp2.2/System.Data.dll",
+		  "ref/netcoreapp2.2/System.Diagnostics.Contracts.dll",
+		  "ref/netcoreapp2.2/System.Diagnostics.Contracts.xml",
+		  "ref/netcoreapp2.2/System.Diagnostics.Debug.dll",
+		  "ref/netcoreapp2.2/System.Diagnostics.Debug.xml",
+		  "ref/netcoreapp2.2/System.Diagnostics.DiagnosticSource.dll",
+		  "ref/netcoreapp2.2/System.Diagnostics.DiagnosticSource.xml",
+		  "ref/netcoreapp2.2/System.Diagnostics.FileVersionInfo.dll",
+		  "ref/netcoreapp2.2/System.Diagnostics.FileVersionInfo.xml",
+		  "ref/netcoreapp2.2/System.Diagnostics.Process.dll",
+		  "ref/netcoreapp2.2/System.Diagnostics.Process.xml",
+		  "ref/netcoreapp2.2/System.Diagnostics.StackTrace.dll",
+		  "ref/netcoreapp2.2/System.Diagnostics.StackTrace.xml",
+		  "ref/netcoreapp2.2/System.Diagnostics.TextWriterTraceListener.dll",
+		  "ref/netcoreapp2.2/System.Diagnostics.TextWriterTraceListener.xml",
+		  "ref/netcoreapp2.2/System.Diagnostics.Tools.dll",
+		  "ref/netcoreapp2.2/System.Diagnostics.Tools.xml",
+		  "ref/netcoreapp2.2/System.Diagnostics.TraceSource.dll",
+		  "ref/netcoreapp2.2/System.Diagnostics.TraceSource.xml",
+		  "ref/netcoreapp2.2/System.Diagnostics.Tracing.dll",
+		  "ref/netcoreapp2.2/System.Diagnostics.Tracing.xml",
+		  "ref/netcoreapp2.2/System.Drawing.Primitives.dll",
+		  "ref/netcoreapp2.2/System.Drawing.Primitives.xml",
+		  "ref/netcoreapp2.2/System.Drawing.dll",
+		  "ref/netcoreapp2.2/System.Dynamic.Runtime.dll",
+		  "ref/netcoreapp2.2/System.Globalization.Calendars.dll",
+		  "ref/netcoreapp2.2/System.Globalization.Extensions.dll",
+		  "ref/netcoreapp2.2/System.Globalization.dll",
+		  "ref/netcoreapp2.2/System.IO.Compression.Brotli.dll",
+		  "ref/netcoreapp2.2/System.IO.Compression.FileSystem.dll",
+		  "ref/netcoreapp2.2/System.IO.Compression.ZipFile.dll",
+		  "ref/netcoreapp2.2/System.IO.Compression.ZipFile.xml",
+		  "ref/netcoreapp2.2/System.IO.Compression.dll",
+		  "ref/netcoreapp2.2/System.IO.Compression.xml",
+		  "ref/netcoreapp2.2/System.IO.FileSystem.DriveInfo.dll",
+		  "ref/netcoreapp2.2/System.IO.FileSystem.DriveInfo.xml",
+		  "ref/netcoreapp2.2/System.IO.FileSystem.Primitives.dll",
+		  "ref/netcoreapp2.2/System.IO.FileSystem.Watcher.dll",
+		  "ref/netcoreapp2.2/System.IO.FileSystem.Watcher.xml",
+		  "ref/netcoreapp2.2/System.IO.FileSystem.dll",
+		  "ref/netcoreapp2.2/System.IO.FileSystem.xml",
+		  "ref/netcoreapp2.2/System.IO.IsolatedStorage.dll",
+		  "ref/netcoreapp2.2/System.IO.IsolatedStorage.xml",
+		  "ref/netcoreapp2.2/System.IO.MemoryMappedFiles.dll",
+		  "ref/netcoreapp2.2/System.IO.MemoryMappedFiles.xml",
+		  "ref/netcoreapp2.2/System.IO.Pipes.dll",
+		  "ref/netcoreapp2.2/System.IO.Pipes.xml",
+		  "ref/netcoreapp2.2/System.IO.UnmanagedMemoryStream.dll",
+		  "ref/netcoreapp2.2/System.IO.dll",
+		  "ref/netcoreapp2.2/System.Linq.Expressions.dll",
+		  "ref/netcoreapp2.2/System.Linq.Expressions.xml",
+		  "ref/netcoreapp2.2/System.Linq.Parallel.dll",
+		  "ref/netcoreapp2.2/System.Linq.Parallel.xml",
+		  "ref/netcoreapp2.2/System.Linq.Queryable.dll",
+		  "ref/netcoreapp2.2/System.Linq.Queryable.xml",
+		  "ref/netcoreapp2.2/System.Linq.dll",
+		  "ref/netcoreapp2.2/System.Linq.xml",
+		  "ref/netcoreapp2.2/System.Memory.dll",
+		  "ref/netcoreapp2.2/System.Memory.xml",
+		  "ref/netcoreapp2.2/System.Net.Http.dll",
+		  "ref/netcoreapp2.2/System.Net.Http.xml",
+		  "ref/netcoreapp2.2/System.Net.HttpListener.dll",
+		  "ref/netcoreapp2.2/System.Net.HttpListener.xml",
+		  "ref/netcoreapp2.2/System.Net.Mail.dll",
+		  "ref/netcoreapp2.2/System.Net.Mail.xml",
+		  "ref/netcoreapp2.2/System.Net.NameResolution.dll",
+		  "ref/netcoreapp2.2/System.Net.NameResolution.xml",
+		  "ref/netcoreapp2.2/System.Net.NetworkInformation.dll",
+		  "ref/netcoreapp2.2/System.Net.NetworkInformation.xml",
+		  "ref/netcoreapp2.2/System.Net.Ping.dll",
+		  "ref/netcoreapp2.2/System.Net.Ping.xml",
+		  "ref/netcoreapp2.2/System.Net.Primitives.dll",
+		  "ref/netcoreapp2.2/System.Net.Primitives.xml",
+		  "ref/netcoreapp2.2/System.Net.Requests.dll",
+		  "ref/netcoreapp2.2/System.Net.Requests.xml",
+		  "ref/netcoreapp2.2/System.Net.Security.dll",
+		  "ref/netcoreapp2.2/System.Net.Security.xml",
+		  "ref/netcoreapp2.2/System.Net.ServicePoint.dll",
+		  "ref/netcoreapp2.2/System.Net.ServicePoint.xml",
+		  "ref/netcoreapp2.2/System.Net.Sockets.dll",
+		  "ref/netcoreapp2.2/System.Net.Sockets.xml",
+		  "ref/netcoreapp2.2/System.Net.WebClient.dll",
+		  "ref/netcoreapp2.2/System.Net.WebClient.xml",
+		  "ref/netcoreapp2.2/System.Net.WebHeaderCollection.dll",
+		  "ref/netcoreapp2.2/System.Net.WebHeaderCollection.xml",
+		  "ref/netcoreapp2.2/System.Net.WebProxy.dll",
+		  "ref/netcoreapp2.2/System.Net.WebProxy.xml",
+		  "ref/netcoreapp2.2/System.Net.WebSockets.Client.dll",
+		  "ref/netcoreapp2.2/System.Net.WebSockets.Client.xml",
+		  "ref/netcoreapp2.2/System.Net.WebSockets.dll",
+		  "ref/netcoreapp2.2/System.Net.WebSockets.xml",
+		  "ref/netcoreapp2.2/System.Net.dll",
+		  "ref/netcoreapp2.2/System.Numerics.Vectors.dll",
+		  "ref/netcoreapp2.2/System.Numerics.Vectors.xml",
+		  "ref/netcoreapp2.2/System.Numerics.dll",
+		  "ref/netcoreapp2.2/System.ObjectModel.dll",
+		  "ref/netcoreapp2.2/System.ObjectModel.xml",
+		  "ref/netcoreapp2.2/System.Reflection.DispatchProxy.dll",
+		  "ref/netcoreapp2.2/System.Reflection.DispatchProxy.xml",
+		  "ref/netcoreapp2.2/System.Reflection.Emit.ILGeneration.dll",
+		  "ref/netcoreapp2.2/System.Reflection.Emit.ILGeneration.xml",
+		  "ref/netcoreapp2.2/System.Reflection.Emit.Lightweight.dll",
+		  "ref/netcoreapp2.2/System.Reflection.Emit.Lightweight.xml",
+		  "ref/netcoreapp2.2/System.Reflection.Emit.dll",
+		  "ref/netcoreapp2.2/System.Reflection.Emit.xml",
+		  "ref/netcoreapp2.2/System.Reflection.Extensions.dll",
+		  "ref/netcoreapp2.2/System.Reflection.Metadata.dll",
+		  "ref/netcoreapp2.2/System.Reflection.Metadata.xml",
+		  "ref/netcoreapp2.2/System.Reflection.Primitives.dll",
+		  "ref/netcoreapp2.2/System.Reflection.Primitives.xml",
+		  "ref/netcoreapp2.2/System.Reflection.TypeExtensions.dll",
+		  "ref/netcoreapp2.2/System.Reflection.TypeExtensions.xml",
+		  "ref/netcoreapp2.2/System.Reflection.dll",
+		  "ref/netcoreapp2.2/System.Resources.Reader.dll",
+		  "ref/netcoreapp2.2/System.Resources.ResourceManager.dll",
+		  "ref/netcoreapp2.2/System.Resources.ResourceManager.xml",
+		  "ref/netcoreapp2.2/System.Resources.Writer.dll",
+		  "ref/netcoreapp2.2/System.Resources.Writer.xml",
+		  "ref/netcoreapp2.2/System.Runtime.CompilerServices.VisualC.dll",
+		  "ref/netcoreapp2.2/System.Runtime.CompilerServices.VisualC.xml",
+		  "ref/netcoreapp2.2/System.Runtime.Extensions.dll",
+		  "ref/netcoreapp2.2/System.Runtime.Extensions.xml",
+		  "ref/netcoreapp2.2/System.Runtime.Handles.dll",
+		  "ref/netcoreapp2.2/System.Runtime.InteropServices.RuntimeInformation.dll",
+		  "ref/netcoreapp2.2/System.Runtime.InteropServices.RuntimeInformation.xml",
+		  "ref/netcoreapp2.2/System.Runtime.InteropServices.WindowsRuntime.dll",
+		  "ref/netcoreapp2.2/System.Runtime.InteropServices.WindowsRuntime.xml",
+		  "ref/netcoreapp2.2/System.Runtime.InteropServices.dll",
+		  "ref/netcoreapp2.2/System.Runtime.InteropServices.xml",
+		  "ref/netcoreapp2.2/System.Runtime.Loader.dll",
+		  "ref/netcoreapp2.2/System.Runtime.Loader.xml",
+		  "ref/netcoreapp2.2/System.Runtime.Numerics.dll",
+		  "ref/netcoreapp2.2/System.Runtime.Numerics.xml",
+		  "ref/netcoreapp2.2/System.Runtime.Serialization.Formatters.dll",
+		  "ref/netcoreapp2.2/System.Runtime.Serialization.Formatters.xml",
+		  "ref/netcoreapp2.2/System.Runtime.Serialization.Json.dll",
+		  "ref/netcoreapp2.2/System.Runtime.Serialization.Json.xml",
+		  "ref/netcoreapp2.2/System.Runtime.Serialization.Primitives.dll",
+		  "ref/netcoreapp2.2/System.Runtime.Serialization.Primitives.xml",
+		  "ref/netcoreapp2.2/System.Runtime.Serialization.Xml.dll",
+		  "ref/netcoreapp2.2/System.Runtime.Serialization.Xml.xml",
+		  "ref/netcoreapp2.2/System.Runtime.Serialization.dll",
+		  "ref/netcoreapp2.2/System.Runtime.dll",
+		  "ref/netcoreapp2.2/System.Runtime.xml",
+		  "ref/netcoreapp2.2/System.Security.Claims.dll",
+		  "ref/netcoreapp2.2/System.Security.Claims.xml",
+		  "ref/netcoreapp2.2/System.Security.Cryptography.Algorithms.dll",
+		  "ref/netcoreapp2.2/System.Security.Cryptography.Algorithms.xml",
+		  "ref/netcoreapp2.2/System.Security.Cryptography.Csp.dll",
+		  "ref/netcoreapp2.2/System.Security.Cryptography.Csp.xml",
+		  "ref/netcoreapp2.2/System.Security.Cryptography.Encoding.dll",
+		  "ref/netcoreapp2.2/System.Security.Cryptography.Encoding.xml",
+		  "ref/netcoreapp2.2/System.Security.Cryptography.Primitives.dll",
+		  "ref/netcoreapp2.2/System.Security.Cryptography.Primitives.xml",
+		  "ref/netcoreapp2.2/System.Security.Cryptography.X509Certificates.dll",
+		  "ref/netcoreapp2.2/System.Security.Cryptography.X509Certificates.xml",
+		  "ref/netcoreapp2.2/System.Security.Principal.dll",
+		  "ref/netcoreapp2.2/System.Security.Principal.xml",
+		  "ref/netcoreapp2.2/System.Security.SecureString.dll",
+		  "ref/netcoreapp2.2/System.Security.dll",
+		  "ref/netcoreapp2.2/System.ServiceModel.Web.dll",
+		  "ref/netcoreapp2.2/System.ServiceProcess.dll",
+		  "ref/netcoreapp2.2/System.Text.Encoding.Extensions.dll",
+		  "ref/netcoreapp2.2/System.Text.Encoding.Extensions.xml",
+		  "ref/netcoreapp2.2/System.Text.Encoding.dll",
+		  "ref/netcoreapp2.2/System.Text.RegularExpressions.dll",
+		  "ref/netcoreapp2.2/System.Text.RegularExpressions.xml",
+		  "ref/netcoreapp2.2/System.Threading.Overlapped.dll",
+		  "ref/netcoreapp2.2/System.Threading.Overlapped.xml",
+		  "ref/netcoreapp2.2/System.Threading.Tasks.Dataflow.dll",
+		  "ref/netcoreapp2.2/System.Threading.Tasks.Dataflow.xml",
+		  "ref/netcoreapp2.2/System.Threading.Tasks.Extensions.dll",
+		  "ref/netcoreapp2.2/System.Threading.Tasks.Extensions.xml",
+		  "ref/netcoreapp2.2/System.Threading.Tasks.Parallel.dll",
+		  "ref/netcoreapp2.2/System.Threading.Tasks.Parallel.xml",
+		  "ref/netcoreapp2.2/System.Threading.Tasks.dll",
+		  "ref/netcoreapp2.2/System.Threading.Tasks.xml",
+		  "ref/netcoreapp2.2/System.Threading.Thread.dll",
+		  "ref/netcoreapp2.2/System.Threading.Thread.xml",
+		  "ref/netcoreapp2.2/System.Threading.ThreadPool.dll",
+		  "ref/netcoreapp2.2/System.Threading.ThreadPool.xml",
+		  "ref/netcoreapp2.2/System.Threading.Timer.dll",
+		  "ref/netcoreapp2.2/System.Threading.Timer.xml",
+		  "ref/netcoreapp2.2/System.Threading.dll",
+		  "ref/netcoreapp2.2/System.Threading.xml",
+		  "ref/netcoreapp2.2/System.Transactions.Local.dll",
+		  "ref/netcoreapp2.2/System.Transactions.Local.xml",
+		  "ref/netcoreapp2.2/System.Transactions.dll",
+		  "ref/netcoreapp2.2/System.ValueTuple.dll",
+		  "ref/netcoreapp2.2/System.Web.HttpUtility.dll",
+		  "ref/netcoreapp2.2/System.Web.HttpUtility.xml",
+		  "ref/netcoreapp2.2/System.Web.dll",
+		  "ref/netcoreapp2.2/System.Windows.dll",
+		  "ref/netcoreapp2.2/System.Xml.Linq.dll",
+		  "ref/netcoreapp2.2/System.Xml.ReaderWriter.dll",
+		  "ref/netcoreapp2.2/System.Xml.ReaderWriter.xml",
+		  "ref/netcoreapp2.2/System.Xml.Serialization.dll",
+		  "ref/netcoreapp2.2/System.Xml.XDocument.dll",
+		  "ref/netcoreapp2.2/System.Xml.XDocument.xml",
+		  "ref/netcoreapp2.2/System.Xml.XPath.XDocument.dll",
+		  "ref/netcoreapp2.2/System.Xml.XPath.XDocument.xml",
+		  "ref/netcoreapp2.2/System.Xml.XPath.dll",
+		  "ref/netcoreapp2.2/System.Xml.XPath.xml",
+		  "ref/netcoreapp2.2/System.Xml.XmlDocument.dll",
+		  "ref/netcoreapp2.2/System.Xml.XmlSerializer.dll",
+		  "ref/netcoreapp2.2/System.Xml.XmlSerializer.xml",
+		  "ref/netcoreapp2.2/System.Xml.dll",
+		  "ref/netcoreapp2.2/System.dll",
+		  "ref/netcoreapp2.2/WindowsBase.dll",
+		  "ref/netcoreapp2.2/mscorlib.dll",
+		  "ref/netcoreapp2.2/netstandard.dll",
+		  "runtime.json"
+		]
+	  },
+	  "Microsoft.NETCore.DotNetAppHost/2.2.8": {
+		"sha512": "Lh1F6z41levvtfC3KuuiQe9ppWKRP1oIB42vP1QNQE4uumo95h+LpjPDeysX1DlTjCzG0BVGSUEpCW5fHkni7w==",
+		"type": "package",
+		"path": "microsoft.netcore.dotnetapphost/2.2.8",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "LICENSE.TXT",
+		  "THIRD-PARTY-NOTICES.TXT",
+		  "microsoft.netcore.dotnetapphost.2.2.8.nupkg.sha512",
+		  "microsoft.netcore.dotnetapphost.nuspec",
+		  "runtime.json"
+		]
+	  },
+	  "Microsoft.NETCore.DotNetHostPolicy/2.2.8": {
+		"sha512": "rOHr0Dk87vaiq9d1hMpXETB4IKq1jIiPQlVKNUjRGilK/cjOcadhsk+1MsrJ/GnM3eovhy8zW2PGkN8pYEolnw==",
+		"type": "package",
+		"path": "microsoft.netcore.dotnethostpolicy/2.2.8",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "LICENSE.TXT",
+		  "THIRD-PARTY-NOTICES.TXT",
+		  "microsoft.netcore.dotnethostpolicy.2.2.8.nupkg.sha512",
+		  "microsoft.netcore.dotnethostpolicy.nuspec",
+		  "runtime.json"
+		]
+	  },
+	  "Microsoft.NETCore.DotNetHostResolver/2.2.8": {
+		"sha512": "culLr+x2GvUkXVGi4ULZ7jmWJEhuAMyS7iTWBlkWnqbKtYJ36ZlgHbw/6qTm82790gJemEFeo9RehDwfRXfJzA==",
+		"type": "package",
+		"path": "microsoft.netcore.dotnethostresolver/2.2.8",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "LICENSE.TXT",
+		  "THIRD-PARTY-NOTICES.TXT",
+		  "microsoft.netcore.dotnethostresolver.2.2.8.nupkg.sha512",
+		  "microsoft.netcore.dotnethostresolver.nuspec",
+		  "runtime.json"
+		]
+	  },
+	  "Microsoft.NETCore.Platforms/2.2.4": {
+		"sha512": "ZeCe9PRhMpKzVWrNgTvWpLjJigppErzN663lJOqAzcx0xjXpcAMpIImFI46IE1gze18VWw6bbfo7JDkcaRWuOg==",
+		"type": "package",
+		"path": "microsoft.netcore.platforms/2.2.4",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "LICENSE.TXT",
+		  "THIRD-PARTY-NOTICES.TXT",
+		  "lib/netstandard1.0/_._",
+		  "microsoft.netcore.platforms.2.2.4.nupkg.sha512",
+		  "microsoft.netcore.platforms.nuspec",
+		  "runtime.json",
+		  "useSharedDesignerContext.txt",
+		  "version.txt"
+		]
+	  },
+	  "Microsoft.NETCore.Targets/2.0.0": {
+		"sha512": "odP/tJj1z6GylFpNo7pMtbd/xQgTC3Ex2If63dRTL38bBNMwsBnJ+RceUIyHdRBC0oik/3NehYT+oECwBhIM3Q==",
+		"type": "package",
+		"path": "microsoft.netcore.targets/2.0.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "LICENSE.TXT",
+		  "THIRD-PARTY-NOTICES.TXT",
+		  "lib/netstandard1.0/_._",
+		  "microsoft.netcore.targets.2.0.0.nupkg.sha512",
+		  "microsoft.netcore.targets.nuspec",
+		  "runtime.json",
+		  "useSharedDesignerContext.txt",
+		  "version.txt"
+		]
+	  },
+	  "Microsoft.Win32.Primitives/4.3.0": {
+		"sha512": "9l/mZyHnvWCxqqBotSz/biGAJVQYrFoe2PGrhuXMNFeDy3RpB9XrRDWmiHQavhwud4fBAvi86QpRk3TLdzDYWg==",
+		"type": "package",
+		"path": "microsoft.win32.primitives/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net46/Microsoft.Win32.Primitives.dll",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "microsoft.win32.primitives.4.3.0.nupkg.sha512",
+		  "microsoft.win32.primitives.nuspec",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net46/Microsoft.Win32.Primitives.dll",
+		  "ref/netstandard1.3/Microsoft.Win32.Primitives.dll",
+		  "ref/netstandard1.3/Microsoft.Win32.Primitives.xml",
+		  "ref/netstandard1.3/de/Microsoft.Win32.Primitives.xml",
+		  "ref/netstandard1.3/es/Microsoft.Win32.Primitives.xml",
+		  "ref/netstandard1.3/fr/Microsoft.Win32.Primitives.xml",
+		  "ref/netstandard1.3/it/Microsoft.Win32.Primitives.xml",
+		  "ref/netstandard1.3/ja/Microsoft.Win32.Primitives.xml",
+		  "ref/netstandard1.3/ko/Microsoft.Win32.Primitives.xml",
+		  "ref/netstandard1.3/ru/Microsoft.Win32.Primitives.xml",
+		  "ref/netstandard1.3/zh-hans/Microsoft.Win32.Primitives.xml",
+		  "ref/netstandard1.3/zh-hant/Microsoft.Win32.Primitives.xml",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._"
+		]
+	  },
+	  "Microsoft.Win32.Registry/4.3.0": {
+		"sha512": "4f0ZyVP4QvGOroa0I7UTwgNXWxUDkaiRf3fpSXLG7rcQnUSh8UFrIRtg8WqDRwhsBnMbKN/2WoiWVGuf/w7VSw==",
+		"type": "package",
+		"path": "microsoft.win32.registry/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/net46/Microsoft.Win32.Registry.dll",
+		  "microsoft.win32.registry.4.3.0.nupkg.sha512",
+		  "microsoft.win32.registry.nuspec",
+		  "ref/net46/Microsoft.Win32.Registry.dll",
+		  "ref/netstandard1.3/Microsoft.Win32.Registry.dll",
+		  "ref/netstandard1.3/Microsoft.Win32.Registry.xml",
+		  "ref/netstandard1.3/de/Microsoft.Win32.Registry.xml",
+		  "ref/netstandard1.3/es/Microsoft.Win32.Registry.xml",
+		  "ref/netstandard1.3/fr/Microsoft.Win32.Registry.xml",
+		  "ref/netstandard1.3/it/Microsoft.Win32.Registry.xml",
+		  "ref/netstandard1.3/ja/Microsoft.Win32.Registry.xml",
+		  "ref/netstandard1.3/ko/Microsoft.Win32.Registry.xml",
+		  "ref/netstandard1.3/ru/Microsoft.Win32.Registry.xml",
+		  "ref/netstandard1.3/zh-hans/Microsoft.Win32.Registry.xml",
+		  "ref/netstandard1.3/zh-hant/Microsoft.Win32.Registry.xml",
+		  "runtimes/unix/lib/netstandard1.3/Microsoft.Win32.Registry.dll",
+		  "runtimes/win/lib/net46/Microsoft.Win32.Registry.dll",
+		  "runtimes/win/lib/netcore50/_._",
+		  "runtimes/win/lib/netstandard1.3/Microsoft.Win32.Registry.dll"
+		]
+	  },
+	  "MinVer/2.5.0": {
+		"sha512": "+vgY+COxnu93nZEVYScloRuboNRIYkElokxTdtKLt6isr/f6GllPt0oLfrHj7fzxgj7SC5xMZg5c2qvd6qyHDQ==",
+		"type": "package",
+		"path": "minver/2.5.0",
+		"hasTools": true,
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "build/MinVer.targets",
+		  "buildMultiTargeting/MinVer.targets",
+		  "minver.2.5.0.nupkg.sha512",
+		  "minver.nuspec",
+		  "minver.png",
+		  "minver/McMaster.Extensions.CommandLineUtils.dll",
+		  "minver/MinVer.Lib.dll",
+		  "minver/MinVer.Lib.pdb",
+		  "minver/MinVer.deps.json",
+		  "minver/MinVer.dll",
+		  "minver/MinVer.pdb",
+		  "minver/MinVer.runtimeconfig.json"
+		]
+	  },
+	  "NETStandard.Library/2.0.3": {
+		"sha512": "VmImnWywqGJMIIbkn2cAPHn+dZ7FR8LivkZ9BNMUZeQxBQ7e8S6ElMV51dyOA4tZjNycXlqvF7ALO4IbYdAi6w==",
+		"type": "package",
+		"path": "netstandard.library/2.0.3",
+		"files": [
+		  ".nupkg.metadata",
+		  "LICENSE.TXT",
+		  "THIRD-PARTY-NOTICES.TXT",
+		  "build/netstandard2.0/NETStandard.Library.targets",
+		  "build/netstandard2.0/ref/Microsoft.Win32.Primitives.dll",
+		  "build/netstandard2.0/ref/System.AppContext.dll",
+		  "build/netstandard2.0/ref/System.Collections.Concurrent.dll",
+		  "build/netstandard2.0/ref/System.Collections.NonGeneric.dll",
+		  "build/netstandard2.0/ref/System.Collections.Specialized.dll",
+		  "build/netstandard2.0/ref/System.Collections.dll",
+		  "build/netstandard2.0/ref/System.ComponentModel.Composition.dll",
+		  "build/netstandard2.0/ref/System.ComponentModel.EventBasedAsync.dll",
+		  "build/netstandard2.0/ref/System.ComponentModel.Primitives.dll",
+		  "build/netstandard2.0/ref/System.ComponentModel.TypeConverter.dll",
+		  "build/netstandard2.0/ref/System.ComponentModel.dll",
+		  "build/netstandard2.0/ref/System.Console.dll",
+		  "build/netstandard2.0/ref/System.Core.dll",
+		  "build/netstandard2.0/ref/System.Data.Common.dll",
+		  "build/netstandard2.0/ref/System.Data.dll",
+		  "build/netstandard2.0/ref/System.Diagnostics.Contracts.dll",
+		  "build/netstandard2.0/ref/System.Diagnostics.Debug.dll",
+		  "build/netstandard2.0/ref/System.Diagnostics.FileVersionInfo.dll",
+		  "build/netstandard2.0/ref/System.Diagnostics.Process.dll",
+		  "build/netstandard2.0/ref/System.Diagnostics.StackTrace.dll",
+		  "build/netstandard2.0/ref/System.Diagnostics.TextWriterTraceListener.dll",
+		  "build/netstandard2.0/ref/System.Diagnostics.Tools.dll",
+		  "build/netstandard2.0/ref/System.Diagnostics.TraceSource.dll",
+		  "build/netstandard2.0/ref/System.Diagnostics.Tracing.dll",
+		  "build/netstandard2.0/ref/System.Drawing.Primitives.dll",
+		  "build/netstandard2.0/ref/System.Drawing.dll",
+		  "build/netstandard2.0/ref/System.Dynamic.Runtime.dll",
+		  "build/netstandard2.0/ref/System.Globalization.Calendars.dll",
+		  "build/netstandard2.0/ref/System.Globalization.Extensions.dll",
+		  "build/netstandard2.0/ref/System.Globalization.dll",
+		  "build/netstandard2.0/ref/System.IO.Compression.FileSystem.dll",
+		  "build/netstandard2.0/ref/System.IO.Compression.ZipFile.dll",
+		  "build/netstandard2.0/ref/System.IO.Compression.dll",
+		  "build/netstandard2.0/ref/System.IO.FileSystem.DriveInfo.dll",
+		  "build/netstandard2.0/ref/System.IO.FileSystem.Primitives.dll",
+		  "build/netstandard2.0/ref/System.IO.FileSystem.Watcher.dll",
+		  "build/netstandard2.0/ref/System.IO.FileSystem.dll",
+		  "build/netstandard2.0/ref/System.IO.IsolatedStorage.dll",
+		  "build/netstandard2.0/ref/System.IO.MemoryMappedFiles.dll",
+		  "build/netstandard2.0/ref/System.IO.Pipes.dll",
+		  "build/netstandard2.0/ref/System.IO.UnmanagedMemoryStream.dll",
+		  "build/netstandard2.0/ref/System.IO.dll",
+		  "build/netstandard2.0/ref/System.Linq.Expressions.dll",
+		  "build/netstandard2.0/ref/System.Linq.Parallel.dll",
+		  "build/netstandard2.0/ref/System.Linq.Queryable.dll",
+		  "build/netstandard2.0/ref/System.Linq.dll",
+		  "build/netstandard2.0/ref/System.Net.Http.dll",
+		  "build/netstandard2.0/ref/System.Net.NameResolution.dll",
+		  "build/netstandard2.0/ref/System.Net.NetworkInformation.dll",
+		  "build/netstandard2.0/ref/System.Net.Ping.dll",
+		  "build/netstandard2.0/ref/System.Net.Primitives.dll",
+		  "build/netstandard2.0/ref/System.Net.Requests.dll",
+		  "build/netstandard2.0/ref/System.Net.Security.dll",
+		  "build/netstandard2.0/ref/System.Net.Sockets.dll",
+		  "build/netstandard2.0/ref/System.Net.WebHeaderCollection.dll",
+		  "build/netstandard2.0/ref/System.Net.WebSockets.Client.dll",
+		  "build/netstandard2.0/ref/System.Net.WebSockets.dll",
+		  "build/netstandard2.0/ref/System.Net.dll",
+		  "build/netstandard2.0/ref/System.Numerics.dll",
+		  "build/netstandard2.0/ref/System.ObjectModel.dll",
+		  "build/netstandard2.0/ref/System.Reflection.Extensions.dll",
+		  "build/netstandard2.0/ref/System.Reflection.Primitives.dll",
+		  "build/netstandard2.0/ref/System.Reflection.dll",
+		  "build/netstandard2.0/ref/System.Resources.Reader.dll",
+		  "build/netstandard2.0/ref/System.Resources.ResourceManager.dll",
+		  "build/netstandard2.0/ref/System.Resources.Writer.dll",
+		  "build/netstandard2.0/ref/System.Runtime.CompilerServices.VisualC.dll",
+		  "build/netstandard2.0/ref/System.Runtime.Extensions.dll",
+		  "build/netstandard2.0/ref/System.Runtime.Handles.dll",
+		  "build/netstandard2.0/ref/System.Runtime.InteropServices.RuntimeInformation.dll",
+		  "build/netstandard2.0/ref/System.Runtime.InteropServices.dll",
+		  "build/netstandard2.0/ref/System.Runtime.Numerics.dll",
+		  "build/netstandard2.0/ref/System.Runtime.Serialization.Formatters.dll",
+		  "build/netstandard2.0/ref/System.Runtime.Serialization.Json.dll",
+		  "build/netstandard2.0/ref/System.Runtime.Serialization.Primitives.dll",
+		  "build/netstandard2.0/ref/System.Runtime.Serialization.Xml.dll",
+		  "build/netstandard2.0/ref/System.Runtime.Serialization.dll",
+		  "build/netstandard2.0/ref/System.Runtime.dll",
+		  "build/netstandard2.0/ref/System.Security.Claims.dll",
+		  "build/netstandard2.0/ref/System.Security.Cryptography.Algorithms.dll",
+		  "build/netstandard2.0/ref/System.Security.Cryptography.Csp.dll",
+		  "build/netstandard2.0/ref/System.Security.Cryptography.Encoding.dll",
+		  "build/netstandard2.0/ref/System.Security.Cryptography.Primitives.dll",
+		  "build/netstandard2.0/ref/System.Security.Cryptography.X509Certificates.dll",
+		  "build/netstandard2.0/ref/System.Security.Principal.dll",
+		  "build/netstandard2.0/ref/System.Security.SecureString.dll",
+		  "build/netstandard2.0/ref/System.ServiceModel.Web.dll",
+		  "build/netstandard2.0/ref/System.Text.Encoding.Extensions.dll",
+		  "build/netstandard2.0/ref/System.Text.Encoding.dll",
+		  "build/netstandard2.0/ref/System.Text.RegularExpressions.dll",
+		  "build/netstandard2.0/ref/System.Threading.Overlapped.dll",
+		  "build/netstandard2.0/ref/System.Threading.Tasks.Parallel.dll",
+		  "build/netstandard2.0/ref/System.Threading.Tasks.dll",
+		  "build/netstandard2.0/ref/System.Threading.Thread.dll",
+		  "build/netstandard2.0/ref/System.Threading.ThreadPool.dll",
+		  "build/netstandard2.0/ref/System.Threading.Timer.dll",
+		  "build/netstandard2.0/ref/System.Threading.dll",
+		  "build/netstandard2.0/ref/System.Transactions.dll",
+		  "build/netstandard2.0/ref/System.ValueTuple.dll",
+		  "build/netstandard2.0/ref/System.Web.dll",
+		  "build/netstandard2.0/ref/System.Windows.dll",
+		  "build/netstandard2.0/ref/System.Xml.Linq.dll",
+		  "build/netstandard2.0/ref/System.Xml.ReaderWriter.dll",
+		  "build/netstandard2.0/ref/System.Xml.Serialization.dll",
+		  "build/netstandard2.0/ref/System.Xml.XDocument.dll",
+		  "build/netstandard2.0/ref/System.Xml.XPath.XDocument.dll",
+		  "build/netstandard2.0/ref/System.Xml.XPath.dll",
+		  "build/netstandard2.0/ref/System.Xml.XmlDocument.dll",
+		  "build/netstandard2.0/ref/System.Xml.XmlSerializer.dll",
+		  "build/netstandard2.0/ref/System.Xml.dll",
+		  "build/netstandard2.0/ref/System.dll",
+		  "build/netstandard2.0/ref/mscorlib.dll",
+		  "build/netstandard2.0/ref/netstandard.dll",
+		  "build/netstandard2.0/ref/netstandard.xml",
+		  "lib/netstandard1.0/_._",
+		  "netstandard.library.2.0.3.nupkg.sha512",
+		  "netstandard.library.nuspec",
+		  "paket-installmodel.cache"
+		]
+	  },
+	  "Nett/0.10.0": {
+		"sha512": "qR4/qq4EZdMkVHn3YLjIZLo6kELbBbZ3v+lNfF7rNAX8ZEU7/2Phj/5sXsgv42H9NAvId3W6YLL45rrHr4xrrw==",
+		"type": "package",
+		"path": "nett/0.10.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "lib/Net40/Nett.dll",
+		  "lib/Net40/Nett.xml",
+		  "lib/netstandard2.0/Nett.dll",
+		  "lib/netstandard2.0/Nett.xml",
+		  "nett.0.10.0.nupkg.sha512",
+		  "nett.nuspec"
+		]
+	  },
+	  "Newtonsoft.Json/12.0.3": {
+		"sha512": "6mgjfnRB4jKMlzHSl+VD+oUc1IebOZabkbyWj2RiTgWwYPPuaK1H97G1sHqGwPlS5npiF5Q0OrxN1wni2n5QWg==",
+		"type": "package",
+		"path": "newtonsoft.json/12.0.3",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "LICENSE.md",
+		  "lib/net20/Newtonsoft.Json.dll",
+		  "lib/net20/Newtonsoft.Json.xml",
+		  "lib/net35/Newtonsoft.Json.dll",
+		  "lib/net35/Newtonsoft.Json.xml",
+		  "lib/net40/Newtonsoft.Json.dll",
+		  "lib/net40/Newtonsoft.Json.xml",
+		  "lib/net45/Newtonsoft.Json.dll",
+		  "lib/net45/Newtonsoft.Json.xml",
+		  "lib/netstandard1.0/Newtonsoft.Json.dll",
+		  "lib/netstandard1.0/Newtonsoft.Json.xml",
+		  "lib/netstandard1.3/Newtonsoft.Json.dll",
+		  "lib/netstandard1.3/Newtonsoft.Json.xml",
+		  "lib/netstandard2.0/Newtonsoft.Json.dll",
+		  "lib/netstandard2.0/Newtonsoft.Json.xml",
+		  "lib/portable-net40+sl5+win8+wp8+wpa81/Newtonsoft.Json.dll",
+		  "lib/portable-net40+sl5+win8+wp8+wpa81/Newtonsoft.Json.xml",
+		  "lib/portable-net45+win8+wp8+wpa81/Newtonsoft.Json.dll",
+		  "lib/portable-net45+win8+wp8+wpa81/Newtonsoft.Json.xml",
+		  "newtonsoft.json.12.0.3.nupkg.sha512",
+		  "newtonsoft.json.nuspec",
+		  "packageIcon.png",
+		  "paket-installmodel.cache"
+		]
+	  },
+	  "Newtonsoft.Json.Bson/1.0.1": {
+		"sha512": "5PYT/IqQ+UK31AmZiSS102R6EsTo+LGTSI8bp7WAUqDKaF4wHXD8U9u4WxTI1vc64tYi++8p3dk3WWNqPFgldw==",
+		"type": "package",
+		"path": "newtonsoft.json.bson/1.0.1",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "lib/net45/Newtonsoft.Json.Bson.dll",
+		  "lib/net45/Newtonsoft.Json.Bson.xml",
+		  "lib/netstandard1.3/Newtonsoft.Json.Bson.dll",
+		  "lib/netstandard1.3/Newtonsoft.Json.Bson.xml",
+		  "newtonsoft.json.bson.1.0.1.nupkg.sha512",
+		  "newtonsoft.json.bson.nuspec"
+		]
+	  },
+	  "NuGet.Common/5.6.0": {
+		"sha512": "N6EvbGxGOvDgf92osTE7o9GKeLuzRodLo6iiG/TGcUvW9sBt9oKWzB5C16MaubIKBZ8Z4J4ri8gTGUM3mEwPkg==",
+		"type": "package",
+		"path": "nuget.common/5.6.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "lib/net472/NuGet.Common.dll",
+		  "lib/net472/NuGet.Common.xml",
+		  "lib/netstandard2.0/NuGet.Common.dll",
+		  "lib/netstandard2.0/NuGet.Common.xml",
+		  "nuget.common.5.6.0.nupkg.sha512",
+		  "nuget.common.nuspec"
+		]
+	  },
+	  "NuGet.Configuration/5.6.0": {
+		"sha512": "l4ePqw6Tml1SEhxYv8EWg/h0AiNnNi7dOnUU/8ovZ9e0e7z25AJRGt8aBwzIzntgEkRzL0KnwMuXy5oTM+wszg==",
+		"type": "package",
+		"path": "nuget.configuration/5.6.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "lib/net472/NuGet.Configuration.dll",
+		  "lib/net472/NuGet.Configuration.xml",
+		  "lib/netstandard2.0/NuGet.Configuration.dll",
+		  "lib/netstandard2.0/NuGet.Configuration.xml",
+		  "nuget.configuration.5.6.0.nupkg.sha512",
+		  "nuget.configuration.nuspec"
+		]
+	  },
+	  "NuGet.DependencyResolver.Core/5.6.0": {
+		"sha512": "7+m4dXvMn+pJ/GUsl4DvuEU3G+CvhiQLlHPT4VtBiQGeHF0PQtxYW9YhLaLZYE/p/dJ6ipvR2E5X5wzncQvIiA==",
+		"type": "package",
+		"path": "nuget.dependencyresolver.core/5.6.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "lib/net472/NuGet.DependencyResolver.Core.dll",
+		  "lib/net472/NuGet.DependencyResolver.Core.xml",
+		  "lib/netstandard2.0/NuGet.DependencyResolver.Core.dll",
+		  "lib/netstandard2.0/NuGet.DependencyResolver.Core.xml",
+		  "nuget.dependencyresolver.core.5.6.0.nupkg.sha512",
+		  "nuget.dependencyresolver.core.nuspec"
+		]
+	  },
+	  "NuGet.Frameworks/5.6.0": {
+		"sha512": "QKl4ieKQnDnNybLxk5y7TbIHAiYLVXxapCV7AmIr8gVk2wzPwETerlNwks1jCiknfjRPxtAf6XKNln+Q6G4RPA==",
+		"type": "package",
+		"path": "nuget.frameworks/5.6.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "lib/net40/NuGet.Frameworks.dll",
+		  "lib/net40/NuGet.Frameworks.xml",
+		  "lib/net472/NuGet.Frameworks.dll",
+		  "lib/net472/NuGet.Frameworks.xml",
+		  "lib/netstandard2.0/NuGet.Frameworks.dll",
+		  "lib/netstandard2.0/NuGet.Frameworks.xml",
+		  "nuget.frameworks.5.6.0.nupkg.sha512",
+		  "nuget.frameworks.nuspec"
+		]
+	  },
+	  "NuGet.LibraryModel/5.6.0": {
+		"sha512": "DlGXw0LHwq0DZpQGi6jEk/aSrhyYoBlO3ZGA+tVLtL6rnrs/BWFQQ05OhtANsDNObp0ikjVN29/dn4DPCn2J3w==",
+		"type": "package",
+		"path": "nuget.librarymodel/5.6.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "lib/net472/NuGet.LibraryModel.dll",
+		  "lib/net472/NuGet.LibraryModel.xml",
+		  "lib/netstandard2.0/NuGet.LibraryModel.dll",
+		  "lib/netstandard2.0/NuGet.LibraryModel.xml",
+		  "nuget.librarymodel.5.6.0.nupkg.sha512",
+		  "nuget.librarymodel.nuspec"
+		]
+	  },
+	  "NuGet.Packaging/5.6.0": {
+		"sha512": "av0vDlf071JdpP5bdX2wN+sEYFp/TkLGGArpl3mn5w7pHp7eeLYGWYpYRmRIABSkA15gxMB+P1QH1LNch5Kqhg==",
+		"type": "package",
+		"path": "nuget.packaging/5.6.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "lib/net472/NuGet.Packaging.dll",
+		  "lib/net472/NuGet.Packaging.xml",
+		  "lib/netstandard2.0/NuGet.Packaging.dll",
+		  "lib/netstandard2.0/NuGet.Packaging.xml",
+		  "nuget.packaging.5.6.0.nupkg.sha512",
+		  "nuget.packaging.nuspec"
+		]
+	  },
+	  "NuGet.ProjectModel/5.6.0": {
+		"sha512": "gdFVa2p7lM+Nj1QvWY2+9Ww81aoaNm8Lcvq8fwJ9H1JpdJEs+tWUa+v7KDgJgRe3GzgrYn20R9Vsd34BBctqOg==",
+		"type": "package",
+		"path": "nuget.projectmodel/5.6.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "lib/net472/NuGet.ProjectModel.dll",
+		  "lib/net472/NuGet.ProjectModel.xml",
+		  "lib/netstandard2.0/NuGet.ProjectModel.dll",
+		  "lib/netstandard2.0/NuGet.ProjectModel.xml",
+		  "nuget.projectmodel.5.6.0.nupkg.sha512",
+		  "nuget.projectmodel.nuspec"
+		]
+	  },
+	  "NuGet.Protocol/5.6.0": {
+		"sha512": "95ekDA23ypvzf138qhFkSQAkjykTikB1dKpRLBBD8Ugm9Cfib2VEWZQ3QCMwg/xdEDpoVN9/GtF+J3JYV0eBaA==",
+		"type": "package",
+		"path": "nuget.protocol/5.6.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "lib/net472/NuGet.Protocol.dll",
+		  "lib/net472/NuGet.Protocol.xml",
+		  "lib/netstandard2.0/NuGet.Protocol.dll",
+		  "lib/netstandard2.0/NuGet.Protocol.xml",
+		  "nuget.protocol.5.6.0.nupkg.sha512",
+		  "nuget.protocol.nuspec"
+		]
+	  },
+	  "NuGet.Versioning/5.6.0": {
+		"sha512": "UIUh1X1XXSOJwc18a7ssTmCAyN7sDXOTfNJN9HfJnWX8NStzQeZq/6RHfSHYMxUjECk6n5alCQjUA51C36R46A==",
+		"type": "package",
+		"path": "nuget.versioning/5.6.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "lib/net472/NuGet.Versioning.dll",
+		  "lib/net472/NuGet.Versioning.xml",
+		  "lib/netstandard2.0/NuGet.Versioning.dll",
+		  "lib/netstandard2.0/NuGet.Versioning.xml",
+		  "nuget.versioning.5.6.0.nupkg.sha512",
+		  "nuget.versioning.nuspec"
+		]
+	  },
+	  "Polly/7.0.3": {
+		"sha512": "wSHZLI31uJls8XCd2/HH7iMxmvTNGZeq4Zxya6A+0bruoKL4HYtkrF5PyUc38bYXhIR85xAL0nQc/L7BAdELrw==",
+		"type": "package",
+		"path": "polly/7.0.3",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "lib/netstandard1.1/Polly.deps.json",
+		  "lib/netstandard1.1/Polly.dll",
+		  "lib/netstandard1.1/Polly.pdb",
+		  "lib/netstandard1.1/Polly.xml",
+		  "lib/netstandard2.0/Polly.deps.json",
+		  "lib/netstandard2.0/Polly.dll",
+		  "lib/netstandard2.0/Polly.pdb",
+		  "lib/netstandard2.0/Polly.xml",
+		  "polly.7.0.3.nupkg.sha512",
+		  "polly.nuspec"
+		]
+	  },
+	  "runtime.native.System/4.3.0": {
+		"sha512": "wNJH9fbFmdLp9iJeXzGQhr2GxkJbon+CCWoaNho4MNbroklzBka6u7r2k6Jw9IYHWVFy4/gCqAwu8AUE4s+RBw==",
+		"type": "package",
+		"path": "runtime.native.system/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/netstandard1.0/_._",
+		  "runtime.native.system.4.3.0.nupkg.sha512",
+		  "runtime.native.system.nuspec"
+		]
+	  },
+	  "SemanticVersioning/1.2.0": {
+		"sha512": "uYqkmqYk0D7pAShLoxCQsKX2aHj+D+Omf2g1NXoRADsJcFduwf1HLqiYY99vremiFobQfcsia948Gt3bG3ryFQ==",
+		"type": "package",
+		"path": "semanticversioning/1.2.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "lib/net35/SemVer.dll",
+		  "lib/net45/SemVer.dll",
+		  "lib/netstandard1.1/SemVer.dll",
+		  "lib/netstandard2.0/SemVer.dll",
+		  "semanticversioning.1.2.0.nupkg.sha512",
+		  "semanticversioning.nuspec"
+		]
+	  },
+	  "StyleCop.Analyzers/1.0.2": {
+		"sha512": "3xD87lafnVhsSEtJKk50G7FGutvaXkFz4XrrLrxnk/DhZU42dnCGWUsvKuBv4mTS0XdIgTY88tLhxW/8Vi3Pow==",
+		"type": "package",
+		"path": "stylecop.analyzers/1.0.2",
+		"hasTools": true,
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "analyzers/dotnet/cs/StyleCop.Analyzers.CodeFixes.dll",
+		  "analyzers/dotnet/cs/StyleCop.Analyzers.dll",
+		  "stylecop.analyzers.1.0.2.nupkg.sha512",
+		  "stylecop.analyzers.nuspec",
+		  "tools/install.ps1",
+		  "tools/uninstall.ps1"
+		]
+	  },
+	  "System.Collections/4.3.0": {
+		"sha512": "FlG6yWWvE4PdogtiUlJFLTh+Tpw9eI/T7WR/hV0pPnlLCvs1b6MsAMBRwmZ86uPfsSw6Do84H2JCLxKuB0UhOQ==",
+		"type": "package",
+		"path": "system.collections/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net45/_._",
+		  "lib/portable-net45+win8+wp8+wpa81/_._",
+		  "lib/win8/_._",
+		  "lib/wp80/_._",
+		  "lib/wpa81/_._",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net45/_._",
+		  "ref/netcore50/System.Collections.dll",
+		  "ref/netcore50/System.Collections.xml",
+		  "ref/netcore50/de/System.Collections.xml",
+		  "ref/netcore50/es/System.Collections.xml",
+		  "ref/netcore50/fr/System.Collections.xml",
+		  "ref/netcore50/it/System.Collections.xml",
+		  "ref/netcore50/ja/System.Collections.xml",
+		  "ref/netcore50/ko/System.Collections.xml",
+		  "ref/netcore50/ru/System.Collections.xml",
+		  "ref/netcore50/zh-hans/System.Collections.xml",
+		  "ref/netcore50/zh-hant/System.Collections.xml",
+		  "ref/netstandard1.0/System.Collections.dll",
+		  "ref/netstandard1.0/System.Collections.xml",
+		  "ref/netstandard1.0/de/System.Collections.xml",
+		  "ref/netstandard1.0/es/System.Collections.xml",
+		  "ref/netstandard1.0/fr/System.Collections.xml",
+		  "ref/netstandard1.0/it/System.Collections.xml",
+		  "ref/netstandard1.0/ja/System.Collections.xml",
+		  "ref/netstandard1.0/ko/System.Collections.xml",
+		  "ref/netstandard1.0/ru/System.Collections.xml",
+		  "ref/netstandard1.0/zh-hans/System.Collections.xml",
+		  "ref/netstandard1.0/zh-hant/System.Collections.xml",
+		  "ref/netstandard1.3/System.Collections.dll",
+		  "ref/netstandard1.3/System.Collections.xml",
+		  "ref/netstandard1.3/de/System.Collections.xml",
+		  "ref/netstandard1.3/es/System.Collections.xml",
+		  "ref/netstandard1.3/fr/System.Collections.xml",
+		  "ref/netstandard1.3/it/System.Collections.xml",
+		  "ref/netstandard1.3/ja/System.Collections.xml",
+		  "ref/netstandard1.3/ko/System.Collections.xml",
+		  "ref/netstandard1.3/ru/System.Collections.xml",
+		  "ref/netstandard1.3/zh-hans/System.Collections.xml",
+		  "ref/netstandard1.3/zh-hant/System.Collections.xml",
+		  "ref/portable-net45+win8+wp8+wpa81/_._",
+		  "ref/win8/_._",
+		  "ref/wp80/_._",
+		  "ref/wpa81/_._",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "system.collections.4.3.0.nupkg.sha512",
+		  "system.collections.nuspec"
+		]
+	  },
+	  "System.Composition.AttributedModel/1.4.0": {
+		"sha512": "nJpwGXzcKLPKHx1mvXbwtxU6o2tpzR529/pBe5NerXq75R5XcZHu6yMRUHcuroRor3+YlJi6l/l0klQfTYRzlQ==",
+		"type": "package",
+		"path": "system.composition.attributedmodel/1.4.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "LICENSE.TXT",
+		  "THIRD-PARTY-NOTICES.TXT",
+		  "lib/netstandard1.0/System.Composition.AttributedModel.dll",
+		  "lib/netstandard2.0/System.Composition.AttributedModel.dll",
+		  "lib/netstandard2.0/System.Composition.AttributedModel.xml",
+		  "lib/portable-net45+win8+wp8+wpa81/System.Composition.AttributedModel.dll",
+		  "system.composition.attributedmodel.1.4.0.nupkg.sha512",
+		  "system.composition.attributedmodel.nuspec",
+		  "useSharedDesignerContext.txt",
+		  "version.txt"
+		]
+	  },
+	  "System.Composition.Convention/1.4.0": {
+		"sha512": "jEi0SwIWi4SilLi0i83U5kMM8A2aUIe8t8VmXvcoVHPKo0h0UeizLyIcQ6AgeJNVTmZ7uoRp5trWJDjtBeXsNA==",
+		"type": "package",
+		"path": "system.composition.convention/1.4.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "LICENSE.TXT",
+		  "THIRD-PARTY-NOTICES.TXT",
+		  "lib/netstandard1.0/System.Composition.Convention.dll",
+		  "lib/netstandard2.0/System.Composition.Convention.dll",
+		  "lib/netstandard2.0/System.Composition.Convention.xml",
+		  "lib/portable-net45+win8+wp8+wpa81/System.Composition.Convention.dll",
+		  "system.composition.convention.1.4.0.nupkg.sha512",
+		  "system.composition.convention.nuspec",
+		  "useSharedDesignerContext.txt",
+		  "version.txt"
+		]
+	  },
+	  "System.Composition.Hosting/1.4.0": {
+		"sha512": "Ek5lYb00NCMzBecuV/zI4734PrrdoruiMbbR7S3joTOCSrLGND2Xt3aCp8Jg1ZvgA0SFH1Aov2/ZobLazwdd6g==",
+		"type": "package",
+		"path": "system.composition.hosting/1.4.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "LICENSE.TXT",
+		  "THIRD-PARTY-NOTICES.TXT",
+		  "lib/netstandard1.0/System.Composition.Hosting.dll",
+		  "lib/netstandard2.0/System.Composition.Hosting.dll",
+		  "lib/netstandard2.0/System.Composition.Hosting.xml",
+		  "lib/portable-net45+win8+wp8+wpa81/System.Composition.Hosting.dll",
+		  "system.composition.hosting.1.4.0.nupkg.sha512",
+		  "system.composition.hosting.nuspec",
+		  "useSharedDesignerContext.txt",
+		  "version.txt"
+		]
+	  },
+	  "System.Composition.Runtime/1.4.0": {
+		"sha512": "2+RKi0a4qg7cO0w/6YS1B17MWhfSpDDUZ94ibQWhb9XM/L6MD9tcS1VgejpyPobVxsrlvh7rVtfkPPOetaKhRA==",
+		"type": "package",
+		"path": "system.composition.runtime/1.4.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "LICENSE.TXT",
+		  "THIRD-PARTY-NOTICES.TXT",
+		  "lib/netstandard1.0/System.Composition.Runtime.dll",
+		  "lib/netstandard2.0/System.Composition.Runtime.dll",
+		  "lib/netstandard2.0/System.Composition.Runtime.xml",
+		  "lib/portable-net45+win8+wp8+wpa81/System.Composition.Runtime.dll",
+		  "system.composition.runtime.1.4.0.nupkg.sha512",
+		  "system.composition.runtime.nuspec",
+		  "useSharedDesignerContext.txt",
+		  "version.txt"
+		]
+	  },
+	  "System.Composition.TypedParts/1.4.0": {
+		"sha512": "azJdjOUf50JMUH+iactt0629uJg/ahoerGDnFDeBeQ1BrV6+4cpA5tiQIIZMSTpkj3fHlmdlJQBoRsJIqtNguw==",
+		"type": "package",
+		"path": "system.composition.typedparts/1.4.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "LICENSE.TXT",
+		  "THIRD-PARTY-NOTICES.TXT",
+		  "lib/netstandard1.0/System.Composition.TypedParts.dll",
+		  "lib/netstandard2.0/System.Composition.TypedParts.dll",
+		  "lib/netstandard2.0/System.Composition.TypedParts.xml",
+		  "lib/portable-net45+win8+wp8+wpa81/System.Composition.TypedParts.dll",
+		  "system.composition.typedparts.1.4.0.nupkg.sha512",
+		  "system.composition.typedparts.nuspec",
+		  "useSharedDesignerContext.txt",
+		  "version.txt"
+		]
+	  },
+	  "System.Diagnostics.Debug/4.3.0": {
+		"sha512": "+JQLQwkH3DJpYPw2/IG8Adlo1XQ3z6TWHxo19/nKx1Gi2KZh56cghoyLGqiIuPpVT8hjDRjBz/MifnCFVBq5QQ==",
+		"type": "package",
+		"path": "system.diagnostics.debug/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net45/_._",
+		  "lib/portable-net45+win8+wp8+wpa81/_._",
+		  "lib/win8/_._",
+		  "lib/wp80/_._",
+		  "lib/wpa81/_._",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net45/_._",
+		  "ref/netcore50/System.Diagnostics.Debug.dll",
+		  "ref/netcore50/System.Diagnostics.Debug.xml",
+		  "ref/netcore50/de/System.Diagnostics.Debug.xml",
+		  "ref/netcore50/es/System.Diagnostics.Debug.xml",
+		  "ref/netcore50/fr/System.Diagnostics.Debug.xml",
+		  "ref/netcore50/it/System.Diagnostics.Debug.xml",
+		  "ref/netcore50/ja/System.Diagnostics.Debug.xml",
+		  "ref/netcore50/ko/System.Diagnostics.Debug.xml",
+		  "ref/netcore50/ru/System.Diagnostics.Debug.xml",
+		  "ref/netcore50/zh-hans/System.Diagnostics.Debug.xml",
+		  "ref/netcore50/zh-hant/System.Diagnostics.Debug.xml",
+		  "ref/netstandard1.0/System.Diagnostics.Debug.dll",
+		  "ref/netstandard1.0/System.Diagnostics.Debug.xml",
+		  "ref/netstandard1.0/de/System.Diagnostics.Debug.xml",
+		  "ref/netstandard1.0/es/System.Diagnostics.Debug.xml",
+		  "ref/netstandard1.0/fr/System.Diagnostics.Debug.xml",
+		  "ref/netstandard1.0/it/System.Diagnostics.Debug.xml",
+		  "ref/netstandard1.0/ja/System.Diagnostics.Debug.xml",
+		  "ref/netstandard1.0/ko/System.Diagnostics.Debug.xml",
+		  "ref/netstandard1.0/ru/System.Diagnostics.Debug.xml",
+		  "ref/netstandard1.0/zh-hans/System.Diagnostics.Debug.xml",
+		  "ref/netstandard1.0/zh-hant/System.Diagnostics.Debug.xml",
+		  "ref/netstandard1.3/System.Diagnostics.Debug.dll",
+		  "ref/netstandard1.3/System.Diagnostics.Debug.xml",
+		  "ref/netstandard1.3/de/System.Diagnostics.Debug.xml",
+		  "ref/netstandard1.3/es/System.Diagnostics.Debug.xml",
+		  "ref/netstandard1.3/fr/System.Diagnostics.Debug.xml",
+		  "ref/netstandard1.3/it/System.Diagnostics.Debug.xml",
+		  "ref/netstandard1.3/ja/System.Diagnostics.Debug.xml",
+		  "ref/netstandard1.3/ko/System.Diagnostics.Debug.xml",
+		  "ref/netstandard1.3/ru/System.Diagnostics.Debug.xml",
+		  "ref/netstandard1.3/zh-hans/System.Diagnostics.Debug.xml",
+		  "ref/netstandard1.3/zh-hant/System.Diagnostics.Debug.xml",
+		  "ref/portable-net45+win8+wp8+wpa81/_._",
+		  "ref/win8/_._",
+		  "ref/wp80/_._",
+		  "ref/wpa81/_._",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "system.diagnostics.debug.4.3.0.nupkg.sha512",
+		  "system.diagnostics.debug.nuspec"
+		]
+	  },
+	  "System.Diagnostics.Process/4.3.0": {
+		"sha512": "I2WdNpdNnPIXkCIkKUAB+63Ljk0y5aCQI+dA6G+9dEDp5rYfHe9KuFjJCNYj4FHnyqOPBsyyNAY/EGAcsAB52w==",
+		"type": "package",
+		"path": "system.diagnostics.process/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net46/System.Diagnostics.Process.dll",
+		  "lib/net461/System.Diagnostics.Process.dll",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net46/System.Diagnostics.Process.dll",
+		  "ref/net461/System.Diagnostics.Process.dll",
+		  "ref/netstandard1.3/System.Diagnostics.Process.dll",
+		  "ref/netstandard1.3/System.Diagnostics.Process.xml",
+		  "ref/netstandard1.3/de/System.Diagnostics.Process.xml",
+		  "ref/netstandard1.3/es/System.Diagnostics.Process.xml",
+		  "ref/netstandard1.3/fr/System.Diagnostics.Process.xml",
+		  "ref/netstandard1.3/it/System.Diagnostics.Process.xml",
+		  "ref/netstandard1.3/ja/System.Diagnostics.Process.xml",
+		  "ref/netstandard1.3/ko/System.Diagnostics.Process.xml",
+		  "ref/netstandard1.3/ru/System.Diagnostics.Process.xml",
+		  "ref/netstandard1.3/zh-hans/System.Diagnostics.Process.xml",
+		  "ref/netstandard1.3/zh-hant/System.Diagnostics.Process.xml",
+		  "ref/netstandard1.4/System.Diagnostics.Process.dll",
+		  "ref/netstandard1.4/System.Diagnostics.Process.xml",
+		  "ref/netstandard1.4/de/System.Diagnostics.Process.xml",
+		  "ref/netstandard1.4/es/System.Diagnostics.Process.xml",
+		  "ref/netstandard1.4/fr/System.Diagnostics.Process.xml",
+		  "ref/netstandard1.4/it/System.Diagnostics.Process.xml",
+		  "ref/netstandard1.4/ja/System.Diagnostics.Process.xml",
+		  "ref/netstandard1.4/ko/System.Diagnostics.Process.xml",
+		  "ref/netstandard1.4/ru/System.Diagnostics.Process.xml",
+		  "ref/netstandard1.4/zh-hans/System.Diagnostics.Process.xml",
+		  "ref/netstandard1.4/zh-hant/System.Diagnostics.Process.xml",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "runtimes/linux/lib/netstandard1.4/System.Diagnostics.Process.dll",
+		  "runtimes/osx/lib/netstandard1.4/System.Diagnostics.Process.dll",
+		  "runtimes/win/lib/net46/System.Diagnostics.Process.dll",
+		  "runtimes/win/lib/net461/System.Diagnostics.Process.dll",
+		  "runtimes/win/lib/netstandard1.4/System.Diagnostics.Process.dll",
+		  "runtimes/win7/lib/netcore50/_._",
+		  "system.diagnostics.process.4.3.0.nupkg.sha512",
+		  "system.diagnostics.process.nuspec"
+		]
+	  },
+	  "System.Dynamic.Runtime/4.3.0": {
+		"sha512": "l5L81doUOuL76M73yPKLoZkRx4nJHVgdE8WAB4B8Pu9aKYXehBJXQCSfY++PqEiM/1HiSbj1UBaQPm6NZbrzMA==",
+		"type": "package",
+		"path": "system.dynamic.runtime/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net45/_._",
+		  "lib/netcore50/System.Dynamic.Runtime.dll",
+		  "lib/netstandard1.3/System.Dynamic.Runtime.dll",
+		  "lib/portable-net45+win8+wp8+wpa81/_._",
+		  "lib/win8/_._",
+		  "lib/wp80/_._",
+		  "lib/wpa81/_._",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net45/_._",
+		  "ref/netcore50/System.Dynamic.Runtime.dll",
+		  "ref/netcore50/System.Dynamic.Runtime.xml",
+		  "ref/netcore50/de/System.Dynamic.Runtime.xml",
+		  "ref/netcore50/es/System.Dynamic.Runtime.xml",
+		  "ref/netcore50/fr/System.Dynamic.Runtime.xml",
+		  "ref/netcore50/it/System.Dynamic.Runtime.xml",
+		  "ref/netcore50/ja/System.Dynamic.Runtime.xml",
+		  "ref/netcore50/ko/System.Dynamic.Runtime.xml",
+		  "ref/netcore50/ru/System.Dynamic.Runtime.xml",
+		  "ref/netcore50/zh-hans/System.Dynamic.Runtime.xml",
+		  "ref/netcore50/zh-hant/System.Dynamic.Runtime.xml",
+		  "ref/netstandard1.0/System.Dynamic.Runtime.dll",
+		  "ref/netstandard1.0/System.Dynamic.Runtime.xml",
+		  "ref/netstandard1.0/de/System.Dynamic.Runtime.xml",
+		  "ref/netstandard1.0/es/System.Dynamic.Runtime.xml",
+		  "ref/netstandard1.0/fr/System.Dynamic.Runtime.xml",
+		  "ref/netstandard1.0/it/System.Dynamic.Runtime.xml",
+		  "ref/netstandard1.0/ja/System.Dynamic.Runtime.xml",
+		  "ref/netstandard1.0/ko/System.Dynamic.Runtime.xml",
+		  "ref/netstandard1.0/ru/System.Dynamic.Runtime.xml",
+		  "ref/netstandard1.0/zh-hans/System.Dynamic.Runtime.xml",
+		  "ref/netstandard1.0/zh-hant/System.Dynamic.Runtime.xml",
+		  "ref/netstandard1.3/System.Dynamic.Runtime.dll",
+		  "ref/netstandard1.3/System.Dynamic.Runtime.xml",
+		  "ref/netstandard1.3/de/System.Dynamic.Runtime.xml",
+		  "ref/netstandard1.3/es/System.Dynamic.Runtime.xml",
+		  "ref/netstandard1.3/fr/System.Dynamic.Runtime.xml",
+		  "ref/netstandard1.3/it/System.Dynamic.Runtime.xml",
+		  "ref/netstandard1.3/ja/System.Dynamic.Runtime.xml",
+		  "ref/netstandard1.3/ko/System.Dynamic.Runtime.xml",
+		  "ref/netstandard1.3/ru/System.Dynamic.Runtime.xml",
+		  "ref/netstandard1.3/zh-hans/System.Dynamic.Runtime.xml",
+		  "ref/netstandard1.3/zh-hant/System.Dynamic.Runtime.xml",
+		  "ref/portable-net45+win8+wp8+wpa81/_._",
+		  "ref/win8/_._",
+		  "ref/wp80/_._",
+		  "ref/wpa81/_._",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "runtimes/aot/lib/netcore50/System.Dynamic.Runtime.dll",
+		  "system.dynamic.runtime.4.3.0.nupkg.sha512",
+		  "system.dynamic.runtime.nuspec"
+		]
+	  },
+	  "System.Globalization/4.3.0": {
+		"sha512": "WXTmCgGfVvz26QPu0iO68Yz61JP0H2bgTTPy1c59dPiL2MtBjuNmAei2REyp/hJQ8Qzimm9vu8V42jX+XBUw8A==",
+		"type": "package",
+		"path": "system.globalization/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net45/_._",
+		  "lib/portable-net45+win8+wp8+wpa81/_._",
+		  "lib/win8/_._",
+		  "lib/wp80/_._",
+		  "lib/wpa81/_._",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net45/_._",
+		  "ref/netcore50/System.Globalization.dll",
+		  "ref/netcore50/System.Globalization.xml",
+		  "ref/netcore50/de/System.Globalization.xml",
+		  "ref/netcore50/es/System.Globalization.xml",
+		  "ref/netcore50/fr/System.Globalization.xml",
+		  "ref/netcore50/it/System.Globalization.xml",
+		  "ref/netcore50/ja/System.Globalization.xml",
+		  "ref/netcore50/ko/System.Globalization.xml",
+		  "ref/netcore50/ru/System.Globalization.xml",
+		  "ref/netcore50/zh-hans/System.Globalization.xml",
+		  "ref/netcore50/zh-hant/System.Globalization.xml",
+		  "ref/netstandard1.0/System.Globalization.dll",
+		  "ref/netstandard1.0/System.Globalization.xml",
+		  "ref/netstandard1.0/de/System.Globalization.xml",
+		  "ref/netstandard1.0/es/System.Globalization.xml",
+		  "ref/netstandard1.0/fr/System.Globalization.xml",
+		  "ref/netstandard1.0/it/System.Globalization.xml",
+		  "ref/netstandard1.0/ja/System.Globalization.xml",
+		  "ref/netstandard1.0/ko/System.Globalization.xml",
+		  "ref/netstandard1.0/ru/System.Globalization.xml",
+		  "ref/netstandard1.0/zh-hans/System.Globalization.xml",
+		  "ref/netstandard1.0/zh-hant/System.Globalization.xml",
+		  "ref/netstandard1.3/System.Globalization.dll",
+		  "ref/netstandard1.3/System.Globalization.xml",
+		  "ref/netstandard1.3/de/System.Globalization.xml",
+		  "ref/netstandard1.3/es/System.Globalization.xml",
+		  "ref/netstandard1.3/fr/System.Globalization.xml",
+		  "ref/netstandard1.3/it/System.Globalization.xml",
+		  "ref/netstandard1.3/ja/System.Globalization.xml",
+		  "ref/netstandard1.3/ko/System.Globalization.xml",
+		  "ref/netstandard1.3/ru/System.Globalization.xml",
+		  "ref/netstandard1.3/zh-hans/System.Globalization.xml",
+		  "ref/netstandard1.3/zh-hant/System.Globalization.xml",
+		  "ref/portable-net45+win8+wp8+wpa81/_._",
+		  "ref/win8/_._",
+		  "ref/wp80/_._",
+		  "ref/wpa81/_._",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "system.globalization.4.3.0.nupkg.sha512",
+		  "system.globalization.nuspec"
+		]
+	  },
+	  "System.IO/4.3.0": {
+		"sha512": "w9k/LKzv6pDgC/HUgJkNMVCC0DZZ6yaYoj0H2WNK2VOA7a0di04PDnprGU0PIVQi+3d1RIoBvp9Eom2tbKIgkA==",
+		"type": "package",
+		"path": "system.io/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net45/_._",
+		  "lib/net462/System.IO.dll",
+		  "lib/portable-net45+win8+wp8+wpa81/_._",
+		  "lib/win8/_._",
+		  "lib/wp80/_._",
+		  "lib/wpa81/_._",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net45/_._",
+		  "ref/net462/System.IO.dll",
+		  "ref/netcore50/System.IO.dll",
+		  "ref/netcore50/System.IO.xml",
+		  "ref/netcore50/de/System.IO.xml",
+		  "ref/netcore50/es/System.IO.xml",
+		  "ref/netcore50/fr/System.IO.xml",
+		  "ref/netcore50/it/System.IO.xml",
+		  "ref/netcore50/ja/System.IO.xml",
+		  "ref/netcore50/ko/System.IO.xml",
+		  "ref/netcore50/ru/System.IO.xml",
+		  "ref/netcore50/zh-hans/System.IO.xml",
+		  "ref/netcore50/zh-hant/System.IO.xml",
+		  "ref/netstandard1.0/System.IO.dll",
+		  "ref/netstandard1.0/System.IO.xml",
+		  "ref/netstandard1.0/de/System.IO.xml",
+		  "ref/netstandard1.0/es/System.IO.xml",
+		  "ref/netstandard1.0/fr/System.IO.xml",
+		  "ref/netstandard1.0/it/System.IO.xml",
+		  "ref/netstandard1.0/ja/System.IO.xml",
+		  "ref/netstandard1.0/ko/System.IO.xml",
+		  "ref/netstandard1.0/ru/System.IO.xml",
+		  "ref/netstandard1.0/zh-hans/System.IO.xml",
+		  "ref/netstandard1.0/zh-hant/System.IO.xml",
+		  "ref/netstandard1.3/System.IO.dll",
+		  "ref/netstandard1.3/System.IO.xml",
+		  "ref/netstandard1.3/de/System.IO.xml",
+		  "ref/netstandard1.3/es/System.IO.xml",
+		  "ref/netstandard1.3/fr/System.IO.xml",
+		  "ref/netstandard1.3/it/System.IO.xml",
+		  "ref/netstandard1.3/ja/System.IO.xml",
+		  "ref/netstandard1.3/ko/System.IO.xml",
+		  "ref/netstandard1.3/ru/System.IO.xml",
+		  "ref/netstandard1.3/zh-hans/System.IO.xml",
+		  "ref/netstandard1.3/zh-hant/System.IO.xml",
+		  "ref/netstandard1.5/System.IO.dll",
+		  "ref/netstandard1.5/System.IO.xml",
+		  "ref/netstandard1.5/de/System.IO.xml",
+		  "ref/netstandard1.5/es/System.IO.xml",
+		  "ref/netstandard1.5/fr/System.IO.xml",
+		  "ref/netstandard1.5/it/System.IO.xml",
+		  "ref/netstandard1.5/ja/System.IO.xml",
+		  "ref/netstandard1.5/ko/System.IO.xml",
+		  "ref/netstandard1.5/ru/System.IO.xml",
+		  "ref/netstandard1.5/zh-hans/System.IO.xml",
+		  "ref/netstandard1.5/zh-hant/System.IO.xml",
+		  "ref/portable-net45+win8+wp8+wpa81/_._",
+		  "ref/win8/_._",
+		  "ref/wp80/_._",
+		  "ref/wpa81/_._",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "system.io.4.3.0.nupkg.sha512",
+		  "system.io.nuspec"
+		]
+	  },
+	  "System.IO.FileSystem/4.3.0": {
+		"sha512": "QQnhkv65V0zVq5cppNWTicGvyV9HtGEMpXJ+TQKG5K/uhaStgyDHFmq3R3Gmg1KNOqMLdrVOhsrUxGv+C3M+iw==",
+		"type": "package",
+		"path": "system.io.filesystem/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net46/System.IO.FileSystem.dll",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net46/System.IO.FileSystem.dll",
+		  "ref/netstandard1.3/System.IO.FileSystem.dll",
+		  "ref/netstandard1.3/System.IO.FileSystem.xml",
+		  "ref/netstandard1.3/de/System.IO.FileSystem.xml",
+		  "ref/netstandard1.3/es/System.IO.FileSystem.xml",
+		  "ref/netstandard1.3/fr/System.IO.FileSystem.xml",
+		  "ref/netstandard1.3/it/System.IO.FileSystem.xml",
+		  "ref/netstandard1.3/ja/System.IO.FileSystem.xml",
+		  "ref/netstandard1.3/ko/System.IO.FileSystem.xml",
+		  "ref/netstandard1.3/ru/System.IO.FileSystem.xml",
+		  "ref/netstandard1.3/zh-hans/System.IO.FileSystem.xml",
+		  "ref/netstandard1.3/zh-hant/System.IO.FileSystem.xml",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "system.io.filesystem.4.3.0.nupkg.sha512",
+		  "system.io.filesystem.nuspec"
+		]
+	  },
+	  "System.IO.FileSystem.Primitives/4.3.0": {
+		"sha512": "NrkZFVXkZHpfgN+2iPZWKNaACNRRXPbL0Lm2tuGIj2qVGJlNlQ/CGQ/m9iR2jUYmV98trqT5xvCzQSr3Ab8RwQ==",
+		"type": "package",
+		"path": "system.io.filesystem.primitives/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net46/System.IO.FileSystem.Primitives.dll",
+		  "lib/netstandard1.3/System.IO.FileSystem.Primitives.dll",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net46/System.IO.FileSystem.Primitives.dll",
+		  "ref/netstandard1.3/System.IO.FileSystem.Primitives.dll",
+		  "ref/netstandard1.3/System.IO.FileSystem.Primitives.xml",
+		  "ref/netstandard1.3/de/System.IO.FileSystem.Primitives.xml",
+		  "ref/netstandard1.3/es/System.IO.FileSystem.Primitives.xml",
+		  "ref/netstandard1.3/fr/System.IO.FileSystem.Primitives.xml",
+		  "ref/netstandard1.3/it/System.IO.FileSystem.Primitives.xml",
+		  "ref/netstandard1.3/ja/System.IO.FileSystem.Primitives.xml",
+		  "ref/netstandard1.3/ko/System.IO.FileSystem.Primitives.xml",
+		  "ref/netstandard1.3/ru/System.IO.FileSystem.Primitives.xml",
+		  "ref/netstandard1.3/zh-hans/System.IO.FileSystem.Primitives.xml",
+		  "ref/netstandard1.3/zh-hant/System.IO.FileSystem.Primitives.xml",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "system.io.filesystem.primitives.4.3.0.nupkg.sha512",
+		  "system.io.filesystem.primitives.nuspec"
+		]
+	  },
+	  "System.Linq/4.3.0": {
+		"sha512": "dQA9IhOzcYRz2q90OShLjwfeemcUxsiagzikDibqY39PFxxcZvi5FMxyLG8E94V7L3OvL6Eabfl6GtKHKgj9YQ==",
+		"type": "package",
+		"path": "system.linq/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net45/_._",
+		  "lib/net463/System.Linq.dll",
+		  "lib/netcore50/System.Linq.dll",
+		  "lib/netstandard1.6/System.Linq.dll",
+		  "lib/portable-net45+win8+wp8+wpa81/_._",
+		  "lib/win8/_._",
+		  "lib/wp80/_._",
+		  "lib/wpa81/_._",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net45/_._",
+		  "ref/net463/System.Linq.dll",
+		  "ref/netcore50/System.Linq.dll",
+		  "ref/netcore50/System.Linq.xml",
+		  "ref/netcore50/de/System.Linq.xml",
+		  "ref/netcore50/es/System.Linq.xml",
+		  "ref/netcore50/fr/System.Linq.xml",
+		  "ref/netcore50/it/System.Linq.xml",
+		  "ref/netcore50/ja/System.Linq.xml",
+		  "ref/netcore50/ko/System.Linq.xml",
+		  "ref/netcore50/ru/System.Linq.xml",
+		  "ref/netcore50/zh-hans/System.Linq.xml",
+		  "ref/netcore50/zh-hant/System.Linq.xml",
+		  "ref/netstandard1.0/System.Linq.dll",
+		  "ref/netstandard1.0/System.Linq.xml",
+		  "ref/netstandard1.0/de/System.Linq.xml",
+		  "ref/netstandard1.0/es/System.Linq.xml",
+		  "ref/netstandard1.0/fr/System.Linq.xml",
+		  "ref/netstandard1.0/it/System.Linq.xml",
+		  "ref/netstandard1.0/ja/System.Linq.xml",
+		  "ref/netstandard1.0/ko/System.Linq.xml",
+		  "ref/netstandard1.0/ru/System.Linq.xml",
+		  "ref/netstandard1.0/zh-hans/System.Linq.xml",
+		  "ref/netstandard1.0/zh-hant/System.Linq.xml",
+		  "ref/netstandard1.6/System.Linq.dll",
+		  "ref/netstandard1.6/System.Linq.xml",
+		  "ref/netstandard1.6/de/System.Linq.xml",
+		  "ref/netstandard1.6/es/System.Linq.xml",
+		  "ref/netstandard1.6/fr/System.Linq.xml",
+		  "ref/netstandard1.6/it/System.Linq.xml",
+		  "ref/netstandard1.6/ja/System.Linq.xml",
+		  "ref/netstandard1.6/ko/System.Linq.xml",
+		  "ref/netstandard1.6/ru/System.Linq.xml",
+		  "ref/netstandard1.6/zh-hans/System.Linq.xml",
+		  "ref/netstandard1.6/zh-hant/System.Linq.xml",
+		  "ref/portable-net45+win8+wp8+wpa81/_._",
+		  "ref/win8/_._",
+		  "ref/wp80/_._",
+		  "ref/wpa81/_._",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "system.linq.4.3.0.nupkg.sha512",
+		  "system.linq.nuspec"
+		]
+	  },
+	  "System.Linq.Expressions/4.3.0": {
+		"sha512": "zUg8OF7g77ecmKolYZDseScBDzMZTZ2MtGnVpjQavuhHH1dPDF8dvZVgPxbMWG3vSA3j1hrUibFCgs5IraCZ2w==",
+		"type": "package",
+		"path": "system.linq.expressions/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net45/_._",
+		  "lib/net463/System.Linq.Expressions.dll",
+		  "lib/netcore50/System.Linq.Expressions.dll",
+		  "lib/netstandard1.6/System.Linq.Expressions.dll",
+		  "lib/portable-net45+win8+wp8+wpa81/_._",
+		  "lib/win8/_._",
+		  "lib/wp80/_._",
+		  "lib/wpa81/_._",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net45/_._",
+		  "ref/net463/System.Linq.Expressions.dll",
+		  "ref/netcore50/System.Linq.Expressions.dll",
+		  "ref/netcore50/System.Linq.Expressions.xml",
+		  "ref/netcore50/de/System.Linq.Expressions.xml",
+		  "ref/netcore50/es/System.Linq.Expressions.xml",
+		  "ref/netcore50/fr/System.Linq.Expressions.xml",
+		  "ref/netcore50/it/System.Linq.Expressions.xml",
+		  "ref/netcore50/ja/System.Linq.Expressions.xml",
+		  "ref/netcore50/ko/System.Linq.Expressions.xml",
+		  "ref/netcore50/ru/System.Linq.Expressions.xml",
+		  "ref/netcore50/zh-hans/System.Linq.Expressions.xml",
+		  "ref/netcore50/zh-hant/System.Linq.Expressions.xml",
+		  "ref/netstandard1.0/System.Linq.Expressions.dll",
+		  "ref/netstandard1.0/System.Linq.Expressions.xml",
+		  "ref/netstandard1.0/de/System.Linq.Expressions.xml",
+		  "ref/netstandard1.0/es/System.Linq.Expressions.xml",
+		  "ref/netstandard1.0/fr/System.Linq.Expressions.xml",
+		  "ref/netstandard1.0/it/System.Linq.Expressions.xml",
+		  "ref/netstandard1.0/ja/System.Linq.Expressions.xml",
+		  "ref/netstandard1.0/ko/System.Linq.Expressions.xml",
+		  "ref/netstandard1.0/ru/System.Linq.Expressions.xml",
+		  "ref/netstandard1.0/zh-hans/System.Linq.Expressions.xml",
+		  "ref/netstandard1.0/zh-hant/System.Linq.Expressions.xml",
+		  "ref/netstandard1.3/System.Linq.Expressions.dll",
+		  "ref/netstandard1.3/System.Linq.Expressions.xml",
+		  "ref/netstandard1.3/de/System.Linq.Expressions.xml",
+		  "ref/netstandard1.3/es/System.Linq.Expressions.xml",
+		  "ref/netstandard1.3/fr/System.Linq.Expressions.xml",
+		  "ref/netstandard1.3/it/System.Linq.Expressions.xml",
+		  "ref/netstandard1.3/ja/System.Linq.Expressions.xml",
+		  "ref/netstandard1.3/ko/System.Linq.Expressions.xml",
+		  "ref/netstandard1.3/ru/System.Linq.Expressions.xml",
+		  "ref/netstandard1.3/zh-hans/System.Linq.Expressions.xml",
+		  "ref/netstandard1.3/zh-hant/System.Linq.Expressions.xml",
+		  "ref/netstandard1.6/System.Linq.Expressions.dll",
+		  "ref/netstandard1.6/System.Linq.Expressions.xml",
+		  "ref/netstandard1.6/de/System.Linq.Expressions.xml",
+		  "ref/netstandard1.6/es/System.Linq.Expressions.xml",
+		  "ref/netstandard1.6/fr/System.Linq.Expressions.xml",
+		  "ref/netstandard1.6/it/System.Linq.Expressions.xml",
+		  "ref/netstandard1.6/ja/System.Linq.Expressions.xml",
+		  "ref/netstandard1.6/ko/System.Linq.Expressions.xml",
+		  "ref/netstandard1.6/ru/System.Linq.Expressions.xml",
+		  "ref/netstandard1.6/zh-hans/System.Linq.Expressions.xml",
+		  "ref/netstandard1.6/zh-hant/System.Linq.Expressions.xml",
+		  "ref/portable-net45+win8+wp8+wpa81/_._",
+		  "ref/win8/_._",
+		  "ref/wp80/_._",
+		  "ref/wpa81/_._",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "runtimes/aot/lib/netcore50/System.Linq.Expressions.dll",
+		  "system.linq.expressions.4.3.0.nupkg.sha512",
+		  "system.linq.expressions.nuspec"
+		]
+	  },
+	  "System.ObjectModel/4.3.0": {
+		"sha512": "QiHZaRQrXrIbx1wh6/kiKQ7Oc5yqZpSx1XckIB1KAybkhN3l/di0+P6v69XfpGw4WtPjBS7370wXrTVv5oeF5Q==",
+		"type": "package",
+		"path": "system.objectmodel/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net45/_._",
+		  "lib/netcore50/System.ObjectModel.dll",
+		  "lib/netstandard1.3/System.ObjectModel.dll",
+		  "lib/portable-net45+win8+wp8+wpa81/_._",
+		  "lib/win8/_._",
+		  "lib/wp80/_._",
+		  "lib/wpa81/_._",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net45/_._",
+		  "ref/netcore50/System.ObjectModel.dll",
+		  "ref/netcore50/System.ObjectModel.xml",
+		  "ref/netcore50/de/System.ObjectModel.xml",
+		  "ref/netcore50/es/System.ObjectModel.xml",
+		  "ref/netcore50/fr/System.ObjectModel.xml",
+		  "ref/netcore50/it/System.ObjectModel.xml",
+		  "ref/netcore50/ja/System.ObjectModel.xml",
+		  "ref/netcore50/ko/System.ObjectModel.xml",
+		  "ref/netcore50/ru/System.ObjectModel.xml",
+		  "ref/netcore50/zh-hans/System.ObjectModel.xml",
+		  "ref/netcore50/zh-hant/System.ObjectModel.xml",
+		  "ref/netstandard1.0/System.ObjectModel.dll",
+		  "ref/netstandard1.0/System.ObjectModel.xml",
+		  "ref/netstandard1.0/de/System.ObjectModel.xml",
+		  "ref/netstandard1.0/es/System.ObjectModel.xml",
+		  "ref/netstandard1.0/fr/System.ObjectModel.xml",
+		  "ref/netstandard1.0/it/System.ObjectModel.xml",
+		  "ref/netstandard1.0/ja/System.ObjectModel.xml",
+		  "ref/netstandard1.0/ko/System.ObjectModel.xml",
+		  "ref/netstandard1.0/ru/System.ObjectModel.xml",
+		  "ref/netstandard1.0/zh-hans/System.ObjectModel.xml",
+		  "ref/netstandard1.0/zh-hant/System.ObjectModel.xml",
+		  "ref/netstandard1.3/System.ObjectModel.dll",
+		  "ref/netstandard1.3/System.ObjectModel.xml",
+		  "ref/netstandard1.3/de/System.ObjectModel.xml",
+		  "ref/netstandard1.3/es/System.ObjectModel.xml",
+		  "ref/netstandard1.3/fr/System.ObjectModel.xml",
+		  "ref/netstandard1.3/it/System.ObjectModel.xml",
+		  "ref/netstandard1.3/ja/System.ObjectModel.xml",
+		  "ref/netstandard1.3/ko/System.ObjectModel.xml",
+		  "ref/netstandard1.3/ru/System.ObjectModel.xml",
+		  "ref/netstandard1.3/zh-hans/System.ObjectModel.xml",
+		  "ref/netstandard1.3/zh-hant/System.ObjectModel.xml",
+		  "ref/portable-net45+win8+wp8+wpa81/_._",
+		  "ref/win8/_._",
+		  "ref/wp80/_._",
+		  "ref/wpa81/_._",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "system.objectmodel.4.3.0.nupkg.sha512",
+		  "system.objectmodel.nuspec"
+		]
+	  },
+	  "System.Reactive/4.1.2": {
+		"sha512": "QRxhdvoP51UuXZbSzcIiFu3/MCSAlR8rz3G/XMcm3b+a2zOC5ropDVaZrjXAO+7VF04Aqk4MCcLEdhxTfWVlZw==",
+		"type": "package",
+		"path": "system.reactive/4.1.2",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "lib/net46/System.Reactive.dll",
+		  "lib/net46/System.Reactive.xml",
+		  "lib/netstandard2.0/System.Reactive.dll",
+		  "lib/netstandard2.0/System.Reactive.xml",
+		  "lib/uap10.0.16299/System.Reactive.dll",
+		  "lib/uap10.0.16299/System.Reactive.pri",
+		  "lib/uap10.0.16299/System.Reactive.xml",
+		  "lib/uap10.0/System.Reactive.dll",
+		  "lib/uap10.0/System.Reactive.pri",
+		  "lib/uap10.0/System.Reactive.xml",
+		  "system.reactive.4.1.2.nupkg.sha512",
+		  "system.reactive.nuspec"
+		]
+	  },
+	  "System.Reflection/4.3.0": {
+		"sha512": "vVNX6iFKa2XrZUoZWE8AFDpInpdiqwt5HLrsb1YHLpj+cylnPySK1QOIeWMOG7WOCoQg341bQQ5yXmeKNUkIag==",
+		"type": "package",
+		"path": "system.reflection/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net45/_._",
+		  "lib/net462/System.Reflection.dll",
+		  "lib/portable-net45+win8+wp8+wpa81/_._",
+		  "lib/win8/_._",
+		  "lib/wp80/_._",
+		  "lib/wpa81/_._",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net45/_._",
+		  "ref/net462/System.Reflection.dll",
+		  "ref/netcore50/System.Reflection.dll",
+		  "ref/netcore50/System.Reflection.xml",
+		  "ref/netcore50/de/System.Reflection.xml",
+		  "ref/netcore50/es/System.Reflection.xml",
+		  "ref/netcore50/fr/System.Reflection.xml",
+		  "ref/netcore50/it/System.Reflection.xml",
+		  "ref/netcore50/ja/System.Reflection.xml",
+		  "ref/netcore50/ko/System.Reflection.xml",
+		  "ref/netcore50/ru/System.Reflection.xml",
+		  "ref/netcore50/zh-hans/System.Reflection.xml",
+		  "ref/netcore50/zh-hant/System.Reflection.xml",
+		  "ref/netstandard1.0/System.Reflection.dll",
+		  "ref/netstandard1.0/System.Reflection.xml",
+		  "ref/netstandard1.0/de/System.Reflection.xml",
+		  "ref/netstandard1.0/es/System.Reflection.xml",
+		  "ref/netstandard1.0/fr/System.Reflection.xml",
+		  "ref/netstandard1.0/it/System.Reflection.xml",
+		  "ref/netstandard1.0/ja/System.Reflection.xml",
+		  "ref/netstandard1.0/ko/System.Reflection.xml",
+		  "ref/netstandard1.0/ru/System.Reflection.xml",
+		  "ref/netstandard1.0/zh-hans/System.Reflection.xml",
+		  "ref/netstandard1.0/zh-hant/System.Reflection.xml",
+		  "ref/netstandard1.3/System.Reflection.dll",
+		  "ref/netstandard1.3/System.Reflection.xml",
+		  "ref/netstandard1.3/de/System.Reflection.xml",
+		  "ref/netstandard1.3/es/System.Reflection.xml",
+		  "ref/netstandard1.3/fr/System.Reflection.xml",
+		  "ref/netstandard1.3/it/System.Reflection.xml",
+		  "ref/netstandard1.3/ja/System.Reflection.xml",
+		  "ref/netstandard1.3/ko/System.Reflection.xml",
+		  "ref/netstandard1.3/ru/System.Reflection.xml",
+		  "ref/netstandard1.3/zh-hans/System.Reflection.xml",
+		  "ref/netstandard1.3/zh-hant/System.Reflection.xml",
+		  "ref/netstandard1.5/System.Reflection.dll",
+		  "ref/netstandard1.5/System.Reflection.xml",
+		  "ref/netstandard1.5/de/System.Reflection.xml",
+		  "ref/netstandard1.5/es/System.Reflection.xml",
+		  "ref/netstandard1.5/fr/System.Reflection.xml",
+		  "ref/netstandard1.5/it/System.Reflection.xml",
+		  "ref/netstandard1.5/ja/System.Reflection.xml",
+		  "ref/netstandard1.5/ko/System.Reflection.xml",
+		  "ref/netstandard1.5/ru/System.Reflection.xml",
+		  "ref/netstandard1.5/zh-hans/System.Reflection.xml",
+		  "ref/netstandard1.5/zh-hant/System.Reflection.xml",
+		  "ref/portable-net45+win8+wp8+wpa81/_._",
+		  "ref/win8/_._",
+		  "ref/wp80/_._",
+		  "ref/wpa81/_._",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "system.reflection.4.3.0.nupkg.sha512",
+		  "system.reflection.nuspec"
+		]
+	  },
+	  "System.Reflection.Emit/4.3.0": {
+		"sha512": "3zIG0WX7VfgjYmX5BjmLHCvLlWFpbxgIxyJTmpcbdz7FUqQvbVSs9hfOfl97ud26pAGKglBiy6L1HfSzoSxHsQ==",
+		"type": "package",
+		"path": "system.reflection.emit/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/monotouch10/_._",
+		  "lib/net45/_._",
+		  "lib/netcore50/System.Reflection.Emit.dll",
+		  "lib/netstandard1.3/System.Reflection.Emit.dll",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/net45/_._",
+		  "ref/netstandard1.1/System.Reflection.Emit.dll",
+		  "ref/netstandard1.1/System.Reflection.Emit.xml",
+		  "ref/netstandard1.1/de/System.Reflection.Emit.xml",
+		  "ref/netstandard1.1/es/System.Reflection.Emit.xml",
+		  "ref/netstandard1.1/fr/System.Reflection.Emit.xml",
+		  "ref/netstandard1.1/it/System.Reflection.Emit.xml",
+		  "ref/netstandard1.1/ja/System.Reflection.Emit.xml",
+		  "ref/netstandard1.1/ko/System.Reflection.Emit.xml",
+		  "ref/netstandard1.1/ru/System.Reflection.Emit.xml",
+		  "ref/netstandard1.1/zh-hans/System.Reflection.Emit.xml",
+		  "ref/netstandard1.1/zh-hant/System.Reflection.Emit.xml",
+		  "ref/xamarinmac20/_._",
+		  "system.reflection.emit.4.3.0.nupkg.sha512",
+		  "system.reflection.emit.nuspec"
+		]
+	  },
+	  "System.Reflection.Emit.ILGeneration/4.3.0": {
+		"sha512": "Mc/J1aFhbZVSc8+479qA0plqL/S8uWVjCd0Z3klzfd9bZP5mb9B9LcNVOpskC0nuQc/DwOY0Knt0D9+oNdYcmQ==",
+		"type": "package",
+		"path": "system.reflection.emit.ilgeneration/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net45/_._",
+		  "lib/netcore50/System.Reflection.Emit.ILGeneration.dll",
+		  "lib/netstandard1.3/System.Reflection.Emit.ILGeneration.dll",
+		  "lib/portable-net45+wp8/_._",
+		  "lib/wp80/_._",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net45/_._",
+		  "ref/netstandard1.0/System.Reflection.Emit.ILGeneration.dll",
+		  "ref/netstandard1.0/System.Reflection.Emit.ILGeneration.xml",
+		  "ref/netstandard1.0/de/System.Reflection.Emit.ILGeneration.xml",
+		  "ref/netstandard1.0/es/System.Reflection.Emit.ILGeneration.xml",
+		  "ref/netstandard1.0/fr/System.Reflection.Emit.ILGeneration.xml",
+		  "ref/netstandard1.0/it/System.Reflection.Emit.ILGeneration.xml",
+		  "ref/netstandard1.0/ja/System.Reflection.Emit.ILGeneration.xml",
+		  "ref/netstandard1.0/ko/System.Reflection.Emit.ILGeneration.xml",
+		  "ref/netstandard1.0/ru/System.Reflection.Emit.ILGeneration.xml",
+		  "ref/netstandard1.0/zh-hans/System.Reflection.Emit.ILGeneration.xml",
+		  "ref/netstandard1.0/zh-hant/System.Reflection.Emit.ILGeneration.xml",
+		  "ref/portable-net45+wp8/_._",
+		  "ref/wp80/_._",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "runtimes/aot/lib/netcore50/_._",
+		  "system.reflection.emit.ilgeneration.4.3.0.nupkg.sha512",
+		  "system.reflection.emit.ilgeneration.nuspec"
+		]
+	  },
+	  "System.Reflection.Emit.Lightweight/4.3.0": {
+		"sha512": "Sj4SlGByuZFZJvTWFGVMKvsUC9mma++NxccuAiBsZq8muCID7YD3+INgw/d4ReK0PakK5nWDms8RYnPEljyq1Q==",
+		"type": "package",
+		"path": "system.reflection.emit.lightweight/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net45/_._",
+		  "lib/netcore50/System.Reflection.Emit.Lightweight.dll",
+		  "lib/netstandard1.3/System.Reflection.Emit.Lightweight.dll",
+		  "lib/portable-net45+wp8/_._",
+		  "lib/wp80/_._",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net45/_._",
+		  "ref/netstandard1.0/System.Reflection.Emit.Lightweight.dll",
+		  "ref/netstandard1.0/System.Reflection.Emit.Lightweight.xml",
+		  "ref/netstandard1.0/de/System.Reflection.Emit.Lightweight.xml",
+		  "ref/netstandard1.0/es/System.Reflection.Emit.Lightweight.xml",
+		  "ref/netstandard1.0/fr/System.Reflection.Emit.Lightweight.xml",
+		  "ref/netstandard1.0/it/System.Reflection.Emit.Lightweight.xml",
+		  "ref/netstandard1.0/ja/System.Reflection.Emit.Lightweight.xml",
+		  "ref/netstandard1.0/ko/System.Reflection.Emit.Lightweight.xml",
+		  "ref/netstandard1.0/ru/System.Reflection.Emit.Lightweight.xml",
+		  "ref/netstandard1.0/zh-hans/System.Reflection.Emit.Lightweight.xml",
+		  "ref/netstandard1.0/zh-hant/System.Reflection.Emit.Lightweight.xml",
+		  "ref/portable-net45+wp8/_._",
+		  "ref/wp80/_._",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "runtimes/aot/lib/netcore50/_._",
+		  "system.reflection.emit.lightweight.4.3.0.nupkg.sha512",
+		  "system.reflection.emit.lightweight.nuspec"
+		]
+	  },
+	  "System.Reflection.Extensions/4.3.0": {
+		"sha512": "cZ5tI5Vya2+k3RregWqugc+zWwdxRgFSt2GF7ju/2J21fu5koK0cXDk0RtCSOteONBNmvz27SH+Cj/XL4vfXcw==",
+		"type": "package",
+		"path": "system.reflection.extensions/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net45/_._",
+		  "lib/portable-net45+win8+wp8+wpa81/_._",
+		  "lib/win8/_._",
+		  "lib/wp80/_._",
+		  "lib/wpa81/_._",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net45/_._",
+		  "ref/netcore50/System.Reflection.Extensions.dll",
+		  "ref/netcore50/System.Reflection.Extensions.xml",
+		  "ref/netcore50/de/System.Reflection.Extensions.xml",
+		  "ref/netcore50/es/System.Reflection.Extensions.xml",
+		  "ref/netcore50/fr/System.Reflection.Extensions.xml",
+		  "ref/netcore50/it/System.Reflection.Extensions.xml",
+		  "ref/netcore50/ja/System.Reflection.Extensions.xml",
+		  "ref/netcore50/ko/System.Reflection.Extensions.xml",
+		  "ref/netcore50/ru/System.Reflection.Extensions.xml",
+		  "ref/netcore50/zh-hans/System.Reflection.Extensions.xml",
+		  "ref/netcore50/zh-hant/System.Reflection.Extensions.xml",
+		  "ref/netstandard1.0/System.Reflection.Extensions.dll",
+		  "ref/netstandard1.0/System.Reflection.Extensions.xml",
+		  "ref/netstandard1.0/de/System.Reflection.Extensions.xml",
+		  "ref/netstandard1.0/es/System.Reflection.Extensions.xml",
+		  "ref/netstandard1.0/fr/System.Reflection.Extensions.xml",
+		  "ref/netstandard1.0/it/System.Reflection.Extensions.xml",
+		  "ref/netstandard1.0/ja/System.Reflection.Extensions.xml",
+		  "ref/netstandard1.0/ko/System.Reflection.Extensions.xml",
+		  "ref/netstandard1.0/ru/System.Reflection.Extensions.xml",
+		  "ref/netstandard1.0/zh-hans/System.Reflection.Extensions.xml",
+		  "ref/netstandard1.0/zh-hant/System.Reflection.Extensions.xml",
+		  "ref/portable-net45+win8+wp8+wpa81/_._",
+		  "ref/win8/_._",
+		  "ref/wp80/_._",
+		  "ref/wpa81/_._",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "system.reflection.extensions.4.3.0.nupkg.sha512",
+		  "system.reflection.extensions.nuspec"
+		]
+	  },
+	  "System.Reflection.Primitives/4.3.0": {
+		"sha512": "oaBgqLXHdK81uoGe83s5r+iP+uOk5ZaNp/LxS4ity/V5gDCk6KGdXMTQruGHDl1EajJ11xf5wDMDdz9febmTrw==",
+		"type": "package",
+		"path": "system.reflection.primitives/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net45/_._",
+		  "lib/portable-net45+win8+wp8+wpa81/_._",
+		  "lib/win8/_._",
+		  "lib/wp80/_._",
+		  "lib/wpa81/_._",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net45/_._",
+		  "ref/netcore50/System.Reflection.Primitives.dll",
+		  "ref/netcore50/System.Reflection.Primitives.xml",
+		  "ref/netcore50/de/System.Reflection.Primitives.xml",
+		  "ref/netcore50/es/System.Reflection.Primitives.xml",
+		  "ref/netcore50/fr/System.Reflection.Primitives.xml",
+		  "ref/netcore50/it/System.Reflection.Primitives.xml",
+		  "ref/netcore50/ja/System.Reflection.Primitives.xml",
+		  "ref/netcore50/ko/System.Reflection.Primitives.xml",
+		  "ref/netcore50/ru/System.Reflection.Primitives.xml",
+		  "ref/netcore50/zh-hans/System.Reflection.Primitives.xml",
+		  "ref/netcore50/zh-hant/System.Reflection.Primitives.xml",
+		  "ref/netstandard1.0/System.Reflection.Primitives.dll",
+		  "ref/netstandard1.0/System.Reflection.Primitives.xml",
+		  "ref/netstandard1.0/de/System.Reflection.Primitives.xml",
+		  "ref/netstandard1.0/es/System.Reflection.Primitives.xml",
+		  "ref/netstandard1.0/fr/System.Reflection.Primitives.xml",
+		  "ref/netstandard1.0/it/System.Reflection.Primitives.xml",
+		  "ref/netstandard1.0/ja/System.Reflection.Primitives.xml",
+		  "ref/netstandard1.0/ko/System.Reflection.Primitives.xml",
+		  "ref/netstandard1.0/ru/System.Reflection.Primitives.xml",
+		  "ref/netstandard1.0/zh-hans/System.Reflection.Primitives.xml",
+		  "ref/netstandard1.0/zh-hant/System.Reflection.Primitives.xml",
+		  "ref/portable-net45+win8+wp8+wpa81/_._",
+		  "ref/win8/_._",
+		  "ref/wp80/_._",
+		  "ref/wpa81/_._",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "system.reflection.primitives.4.3.0.nupkg.sha512",
+		  "system.reflection.primitives.nuspec"
+		]
+	  },
+	  "System.Reflection.TypeExtensions/4.3.0": {
+		"sha512": "2fhSLJUwruemnj/qArAMODeruJOttBpNX8v78qsJj8TrFcN0U50jfVn0mmFa/L9YxCGiEXwPrVYJ3VFtbh2v2A==",
+		"type": "package",
+		"path": "system.reflection.typeextensions/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net46/System.Reflection.TypeExtensions.dll",
+		  "lib/net462/System.Reflection.TypeExtensions.dll",
+		  "lib/netcore50/System.Reflection.TypeExtensions.dll",
+		  "lib/netstandard1.5/System.Reflection.TypeExtensions.dll",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net46/System.Reflection.TypeExtensions.dll",
+		  "ref/net462/System.Reflection.TypeExtensions.dll",
+		  "ref/netstandard1.3/System.Reflection.TypeExtensions.dll",
+		  "ref/netstandard1.3/System.Reflection.TypeExtensions.xml",
+		  "ref/netstandard1.3/de/System.Reflection.TypeExtensions.xml",
+		  "ref/netstandard1.3/es/System.Reflection.TypeExtensions.xml",
+		  "ref/netstandard1.3/fr/System.Reflection.TypeExtensions.xml",
+		  "ref/netstandard1.3/it/System.Reflection.TypeExtensions.xml",
+		  "ref/netstandard1.3/ja/System.Reflection.TypeExtensions.xml",
+		  "ref/netstandard1.3/ko/System.Reflection.TypeExtensions.xml",
+		  "ref/netstandard1.3/ru/System.Reflection.TypeExtensions.xml",
+		  "ref/netstandard1.3/zh-hans/System.Reflection.TypeExtensions.xml",
+		  "ref/netstandard1.3/zh-hant/System.Reflection.TypeExtensions.xml",
+		  "ref/netstandard1.5/System.Reflection.TypeExtensions.dll",
+		  "ref/netstandard1.5/System.Reflection.TypeExtensions.xml",
+		  "ref/netstandard1.5/de/System.Reflection.TypeExtensions.xml",
+		  "ref/netstandard1.5/es/System.Reflection.TypeExtensions.xml",
+		  "ref/netstandard1.5/fr/System.Reflection.TypeExtensions.xml",
+		  "ref/netstandard1.5/it/System.Reflection.TypeExtensions.xml",
+		  "ref/netstandard1.5/ja/System.Reflection.TypeExtensions.xml",
+		  "ref/netstandard1.5/ko/System.Reflection.TypeExtensions.xml",
+		  "ref/netstandard1.5/ru/System.Reflection.TypeExtensions.xml",
+		  "ref/netstandard1.5/zh-hans/System.Reflection.TypeExtensions.xml",
+		  "ref/netstandard1.5/zh-hant/System.Reflection.TypeExtensions.xml",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "runtimes/aot/lib/netcore50/System.Reflection.TypeExtensions.dll",
+		  "system.reflection.typeextensions.4.3.0.nupkg.sha512",
+		  "system.reflection.typeextensions.nuspec"
+		]
+	  },
+	  "System.Resources.ResourceManager/4.3.0": {
+		"sha512": "auIAchCOROdIxkZ9khiPLPiRK2v6IG4Ecph5gv1I9oqND+5PqWShRjOZKuTcMCRzVIR+PAc+SzeHZQ0UB2TsgA==",
+		"type": "package",
+		"path": "system.resources.resourcemanager/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net45/_._",
+		  "lib/portable-net45+win8+wp8+wpa81/_._",
+		  "lib/win8/_._",
+		  "lib/wp80/_._",
+		  "lib/wpa81/_._",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net45/_._",
+		  "ref/netcore50/System.Resources.ResourceManager.dll",
+		  "ref/netcore50/System.Resources.ResourceManager.xml",
+		  "ref/netcore50/de/System.Resources.ResourceManager.xml",
+		  "ref/netcore50/es/System.Resources.ResourceManager.xml",
+		  "ref/netcore50/fr/System.Resources.ResourceManager.xml",
+		  "ref/netcore50/it/System.Resources.ResourceManager.xml",
+		  "ref/netcore50/ja/System.Resources.ResourceManager.xml",
+		  "ref/netcore50/ko/System.Resources.ResourceManager.xml",
+		  "ref/netcore50/ru/System.Resources.ResourceManager.xml",
+		  "ref/netcore50/zh-hans/System.Resources.ResourceManager.xml",
+		  "ref/netcore50/zh-hant/System.Resources.ResourceManager.xml",
+		  "ref/netstandard1.0/System.Resources.ResourceManager.dll",
+		  "ref/netstandard1.0/System.Resources.ResourceManager.xml",
+		  "ref/netstandard1.0/de/System.Resources.ResourceManager.xml",
+		  "ref/netstandard1.0/es/System.Resources.ResourceManager.xml",
+		  "ref/netstandard1.0/fr/System.Resources.ResourceManager.xml",
+		  "ref/netstandard1.0/it/System.Resources.ResourceManager.xml",
+		  "ref/netstandard1.0/ja/System.Resources.ResourceManager.xml",
+		  "ref/netstandard1.0/ko/System.Resources.ResourceManager.xml",
+		  "ref/netstandard1.0/ru/System.Resources.ResourceManager.xml",
+		  "ref/netstandard1.0/zh-hans/System.Resources.ResourceManager.xml",
+		  "ref/netstandard1.0/zh-hant/System.Resources.ResourceManager.xml",
+		  "ref/portable-net45+win8+wp8+wpa81/_._",
+		  "ref/win8/_._",
+		  "ref/wp80/_._",
+		  "ref/wpa81/_._",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "system.resources.resourcemanager.4.3.0.nupkg.sha512",
+		  "system.resources.resourcemanager.nuspec"
+		]
+	  },
+	  "System.Runtime/4.3.0": {
+		"sha512": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
+		"type": "package",
+		"path": "system.runtime/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net45/_._",
+		  "lib/net462/System.Runtime.dll",
+		  "lib/portable-net45+win8+wp80+wpa81/_._",
+		  "lib/win8/_._",
+		  "lib/wp80/_._",
+		  "lib/wpa81/_._",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net45/_._",
+		  "ref/net462/System.Runtime.dll",
+		  "ref/netcore50/System.Runtime.dll",
+		  "ref/netcore50/System.Runtime.xml",
+		  "ref/netcore50/de/System.Runtime.xml",
+		  "ref/netcore50/es/System.Runtime.xml",
+		  "ref/netcore50/fr/System.Runtime.xml",
+		  "ref/netcore50/it/System.Runtime.xml",
+		  "ref/netcore50/ja/System.Runtime.xml",
+		  "ref/netcore50/ko/System.Runtime.xml",
+		  "ref/netcore50/ru/System.Runtime.xml",
+		  "ref/netcore50/zh-hans/System.Runtime.xml",
+		  "ref/netcore50/zh-hant/System.Runtime.xml",
+		  "ref/netstandard1.0/System.Runtime.dll",
+		  "ref/netstandard1.0/System.Runtime.xml",
+		  "ref/netstandard1.0/de/System.Runtime.xml",
+		  "ref/netstandard1.0/es/System.Runtime.xml",
+		  "ref/netstandard1.0/fr/System.Runtime.xml",
+		  "ref/netstandard1.0/it/System.Runtime.xml",
+		  "ref/netstandard1.0/ja/System.Runtime.xml",
+		  "ref/netstandard1.0/ko/System.Runtime.xml",
+		  "ref/netstandard1.0/ru/System.Runtime.xml",
+		  "ref/netstandard1.0/zh-hans/System.Runtime.xml",
+		  "ref/netstandard1.0/zh-hant/System.Runtime.xml",
+		  "ref/netstandard1.2/System.Runtime.dll",
+		  "ref/netstandard1.2/System.Runtime.xml",
+		  "ref/netstandard1.2/de/System.Runtime.xml",
+		  "ref/netstandard1.2/es/System.Runtime.xml",
+		  "ref/netstandard1.2/fr/System.Runtime.xml",
+		  "ref/netstandard1.2/it/System.Runtime.xml",
+		  "ref/netstandard1.2/ja/System.Runtime.xml",
+		  "ref/netstandard1.2/ko/System.Runtime.xml",
+		  "ref/netstandard1.2/ru/System.Runtime.xml",
+		  "ref/netstandard1.2/zh-hans/System.Runtime.xml",
+		  "ref/netstandard1.2/zh-hant/System.Runtime.xml",
+		  "ref/netstandard1.3/System.Runtime.dll",
+		  "ref/netstandard1.3/System.Runtime.xml",
+		  "ref/netstandard1.3/de/System.Runtime.xml",
+		  "ref/netstandard1.3/es/System.Runtime.xml",
+		  "ref/netstandard1.3/fr/System.Runtime.xml",
+		  "ref/netstandard1.3/it/System.Runtime.xml",
+		  "ref/netstandard1.3/ja/System.Runtime.xml",
+		  "ref/netstandard1.3/ko/System.Runtime.xml",
+		  "ref/netstandard1.3/ru/System.Runtime.xml",
+		  "ref/netstandard1.3/zh-hans/System.Runtime.xml",
+		  "ref/netstandard1.3/zh-hant/System.Runtime.xml",
+		  "ref/netstandard1.5/System.Runtime.dll",
+		  "ref/netstandard1.5/System.Runtime.xml",
+		  "ref/netstandard1.5/de/System.Runtime.xml",
+		  "ref/netstandard1.5/es/System.Runtime.xml",
+		  "ref/netstandard1.5/fr/System.Runtime.xml",
+		  "ref/netstandard1.5/it/System.Runtime.xml",
+		  "ref/netstandard1.5/ja/System.Runtime.xml",
+		  "ref/netstandard1.5/ko/System.Runtime.xml",
+		  "ref/netstandard1.5/ru/System.Runtime.xml",
+		  "ref/netstandard1.5/zh-hans/System.Runtime.xml",
+		  "ref/netstandard1.5/zh-hant/System.Runtime.xml",
+		  "ref/portable-net45+win8+wp80+wpa81/_._",
+		  "ref/win8/_._",
+		  "ref/wp80/_._",
+		  "ref/wpa81/_._",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "system.runtime.4.3.0.nupkg.sha512",
+		  "system.runtime.nuspec"
+		]
+	  },
+	  "System.Runtime.Extensions/4.3.0": {
+		"sha512": "yt+AqvxHbHImbFtN/bLLdm494IYMVqBCIGu8yk6p4Vh7j+c5y1MQz1zg1Pl3lrKZxb1VUxZs4rNxSX2tBMMd5w==",
+		"type": "package",
+		"path": "system.runtime.extensions/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net45/_._",
+		  "lib/net462/System.Runtime.Extensions.dll",
+		  "lib/portable-net45+win8+wp8+wpa81/_._",
+		  "lib/win8/_._",
+		  "lib/wp80/_._",
+		  "lib/wpa81/_._",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net45/_._",
+		  "ref/net462/System.Runtime.Extensions.dll",
+		  "ref/netcore50/System.Runtime.Extensions.dll",
+		  "ref/netcore50/System.Runtime.Extensions.xml",
+		  "ref/netcore50/de/System.Runtime.Extensions.xml",
+		  "ref/netcore50/es/System.Runtime.Extensions.xml",
+		  "ref/netcore50/fr/System.Runtime.Extensions.xml",
+		  "ref/netcore50/it/System.Runtime.Extensions.xml",
+		  "ref/netcore50/ja/System.Runtime.Extensions.xml",
+		  "ref/netcore50/ko/System.Runtime.Extensions.xml",
+		  "ref/netcore50/ru/System.Runtime.Extensions.xml",
+		  "ref/netcore50/zh-hans/System.Runtime.Extensions.xml",
+		  "ref/netcore50/zh-hant/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.0/System.Runtime.Extensions.dll",
+		  "ref/netstandard1.0/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.0/de/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.0/es/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.0/fr/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.0/it/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.0/ja/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.0/ko/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.0/ru/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.0/zh-hans/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.0/zh-hant/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.3/System.Runtime.Extensions.dll",
+		  "ref/netstandard1.3/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.3/de/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.3/es/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.3/fr/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.3/it/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.3/ja/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.3/ko/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.3/ru/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.3/zh-hans/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.3/zh-hant/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.5/System.Runtime.Extensions.dll",
+		  "ref/netstandard1.5/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.5/de/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.5/es/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.5/fr/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.5/it/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.5/ja/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.5/ko/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.5/ru/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.5/zh-hans/System.Runtime.Extensions.xml",
+		  "ref/netstandard1.5/zh-hant/System.Runtime.Extensions.xml",
+		  "ref/portable-net45+win8+wp8+wpa81/_._",
+		  "ref/win8/_._",
+		  "ref/wp80/_._",
+		  "ref/wpa81/_._",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "system.runtime.extensions.4.3.0.nupkg.sha512",
+		  "system.runtime.extensions.nuspec"
+		]
+	  },
+	  "System.Runtime.Handles/4.3.0": {
+		"sha512": "V3MooCI0PHMoaTiuHWyDsSTL0nM2CZaIrSCorymuyn1PUoQhyBeAV2CC75oNIut3jt5b5yXpDZfIHkP0xWfRhw==",
+		"type": "package",
+		"path": "system.runtime.handles/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net46/_._",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net46/_._",
+		  "ref/netstandard1.3/System.Runtime.Handles.dll",
+		  "ref/netstandard1.3/System.Runtime.Handles.xml",
+		  "ref/netstandard1.3/de/System.Runtime.Handles.xml",
+		  "ref/netstandard1.3/es/System.Runtime.Handles.xml",
+		  "ref/netstandard1.3/fr/System.Runtime.Handles.xml",
+		  "ref/netstandard1.3/it/System.Runtime.Handles.xml",
+		  "ref/netstandard1.3/ja/System.Runtime.Handles.xml",
+		  "ref/netstandard1.3/ko/System.Runtime.Handles.xml",
+		  "ref/netstandard1.3/ru/System.Runtime.Handles.xml",
+		  "ref/netstandard1.3/zh-hans/System.Runtime.Handles.xml",
+		  "ref/netstandard1.3/zh-hant/System.Runtime.Handles.xml",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "system.runtime.handles.4.3.0.nupkg.sha512",
+		  "system.runtime.handles.nuspec"
+		]
+	  },
+	  "System.Runtime.InteropServices/4.3.0": {
+		"sha512": "wI+FumZaz/hXeWfTCgfm0gCI6mchKPbnXO+/GMiLj5KrQpIKe/vmTfFRNAcJdnsBCxxWfGD5QfWzOe0vXpndYQ==",
+		"type": "package",
+		"path": "system.runtime.interopservices/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net45/_._",
+		  "lib/net462/System.Runtime.InteropServices.dll",
+		  "lib/net463/System.Runtime.InteropServices.dll",
+		  "lib/portable-net45+win8+wpa81/_._",
+		  "lib/win8/_._",
+		  "lib/wpa81/_._",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net45/_._",
+		  "ref/net462/System.Runtime.InteropServices.dll",
+		  "ref/net463/System.Runtime.InteropServices.dll",
+		  "ref/netcore50/System.Runtime.InteropServices.dll",
+		  "ref/netcore50/System.Runtime.InteropServices.xml",
+		  "ref/netcore50/de/System.Runtime.InteropServices.xml",
+		  "ref/netcore50/es/System.Runtime.InteropServices.xml",
+		  "ref/netcore50/fr/System.Runtime.InteropServices.xml",
+		  "ref/netcore50/it/System.Runtime.InteropServices.xml",
+		  "ref/netcore50/ja/System.Runtime.InteropServices.xml",
+		  "ref/netcore50/ko/System.Runtime.InteropServices.xml",
+		  "ref/netcore50/ru/System.Runtime.InteropServices.xml",
+		  "ref/netcore50/zh-hans/System.Runtime.InteropServices.xml",
+		  "ref/netcore50/zh-hant/System.Runtime.InteropServices.xml",
+		  "ref/netcoreapp1.1/System.Runtime.InteropServices.dll",
+		  "ref/netstandard1.1/System.Runtime.InteropServices.dll",
+		  "ref/netstandard1.1/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.1/de/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.1/es/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.1/fr/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.1/it/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.1/ja/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.1/ko/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.1/ru/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.1/zh-hans/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.1/zh-hant/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.2/System.Runtime.InteropServices.dll",
+		  "ref/netstandard1.2/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.2/de/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.2/es/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.2/fr/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.2/it/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.2/ja/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.2/ko/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.2/ru/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.2/zh-hans/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.2/zh-hant/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.3/System.Runtime.InteropServices.dll",
+		  "ref/netstandard1.3/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.3/de/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.3/es/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.3/fr/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.3/it/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.3/ja/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.3/ko/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.3/ru/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.3/zh-hans/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.3/zh-hant/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.5/System.Runtime.InteropServices.dll",
+		  "ref/netstandard1.5/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.5/de/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.5/es/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.5/fr/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.5/it/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.5/ja/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.5/ko/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.5/ru/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.5/zh-hans/System.Runtime.InteropServices.xml",
+		  "ref/netstandard1.5/zh-hant/System.Runtime.InteropServices.xml",
+		  "ref/portable-net45+win8+wpa81/_._",
+		  "ref/win8/_._",
+		  "ref/wpa81/_._",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "system.runtime.interopservices.4.3.0.nupkg.sha512",
+		  "system.runtime.interopservices.nuspec"
+		]
+	  },
+	  "System.Runtime.InteropServices.WindowsRuntime/4.3.0": {
+		"sha512": "J4GUi3xZQLUBasNwZnjrffN8i5wpHrBtZoLG+OhRyGo/+YunMRWWtwoMDlUAIdmX0uRfpHIBDSV6zyr3yf00TA==",
+		"type": "package",
+		"path": "system.runtime.interopservices.windowsruntime/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net45/_._",
+		  "lib/netcore50/System.Runtime.InteropServices.WindowsRuntime.dll",
+		  "lib/netstandard1.3/System.Runtime.InteropServices.WindowsRuntime.dll",
+		  "lib/portable-net45+win8+wp8+wpa81/_._",
+		  "lib/win8/_._",
+		  "lib/wp80/_._",
+		  "lib/wpa81/_._",
+		  "lib/xamarinios1/_._",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net45/_._",
+		  "ref/netcore50/System.Runtime.InteropServices.WindowsRuntime.dll",
+		  "ref/netcore50/System.Runtime.InteropServices.WindowsRuntime.xml",
+		  "ref/netcore50/de/System.Runtime.InteropServices.WindowsRuntime.xml",
+		  "ref/netcore50/es/System.Runtime.InteropServices.WindowsRuntime.xml",
+		  "ref/netcore50/fr/System.Runtime.InteropServices.WindowsRuntime.xml",
+		  "ref/netcore50/it/System.Runtime.InteropServices.WindowsRuntime.xml",
+		  "ref/netcore50/ja/System.Runtime.InteropServices.WindowsRuntime.xml",
+		  "ref/netcore50/ko/System.Runtime.InteropServices.WindowsRuntime.xml",
+		  "ref/netcore50/ru/System.Runtime.InteropServices.WindowsRuntime.xml",
+		  "ref/netcore50/zh-hans/System.Runtime.InteropServices.WindowsRuntime.xml",
+		  "ref/netcore50/zh-hant/System.Runtime.InteropServices.WindowsRuntime.xml",
+		  "ref/netstandard1.0/System.Runtime.InteropServices.WindowsRuntime.dll",
+		  "ref/netstandard1.0/System.Runtime.InteropServices.WindowsRuntime.xml",
+		  "ref/netstandard1.0/de/System.Runtime.InteropServices.WindowsRuntime.xml",
+		  "ref/netstandard1.0/es/System.Runtime.InteropServices.WindowsRuntime.xml",
+		  "ref/netstandard1.0/fr/System.Runtime.InteropServices.WindowsRuntime.xml",
+		  "ref/netstandard1.0/it/System.Runtime.InteropServices.WindowsRuntime.xml",
+		  "ref/netstandard1.0/ja/System.Runtime.InteropServices.WindowsRuntime.xml",
+		  "ref/netstandard1.0/ko/System.Runtime.InteropServices.WindowsRuntime.xml",
+		  "ref/netstandard1.0/ru/System.Runtime.InteropServices.WindowsRuntime.xml",
+		  "ref/netstandard1.0/zh-hans/System.Runtime.InteropServices.WindowsRuntime.xml",
+		  "ref/netstandard1.0/zh-hant/System.Runtime.InteropServices.WindowsRuntime.xml",
+		  "ref/portable-net45+win8+wp8+wpa81/_._",
+		  "ref/win8/_._",
+		  "ref/wp80/_._",
+		  "ref/wpa81/_._",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "runtimes/aot/lib/netcore50/System.Runtime.InteropServices.WindowsRuntime.dll",
+		  "system.runtime.interopservices.windowsruntime.4.3.0.nupkg.sha512",
+		  "system.runtime.interopservices.windowsruntime.nuspec"
+		]
+	  },
+	  "System.Security.Cryptography.Primitives/4.3.0": {
+		"sha512": "KPbXYG4gvrm+AzckXHktg7DYC5j4xsvdmva4oJLdir3uFZjoYY2NG2nBKdL33St1C1tVF5rsKSAILKqeGqjY2g==",
+		"type": "package",
+		"path": "system.security.cryptography.primitives/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net46/System.Security.Cryptography.Primitives.dll",
+		  "lib/netstandard1.3/System.Security.Cryptography.Primitives.dll",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net46/System.Security.Cryptography.Primitives.dll",
+		  "ref/netstandard1.3/System.Security.Cryptography.Primitives.dll",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "system.security.cryptography.primitives.4.3.0.nupkg.sha512",
+		  "system.security.cryptography.primitives.nuspec"
+		]
+	  },
+	  "System.Security.Cryptography.ProtectedData/4.3.0": {
+		"sha512": "qBUHUk7IqrPHY96THHTa1akCxw0GsNFpsk3XFHbi0A0tMUDBpQprtY1Tbl6yaS1x4c96ilcXU8PocYtmSmkaQQ==",
+		"type": "package",
+		"path": "system.security.cryptography.protecteddata/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net46/System.Security.Cryptography.ProtectedData.dll",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net46/System.Security.Cryptography.ProtectedData.dll",
+		  "ref/netstandard1.3/System.Security.Cryptography.ProtectedData.dll",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "runtimes/unix/lib/netstandard1.3/System.Security.Cryptography.ProtectedData.dll",
+		  "runtimes/win/lib/net46/System.Security.Cryptography.ProtectedData.dll",
+		  "runtimes/win/lib/netstandard1.3/System.Security.Cryptography.ProtectedData.dll",
+		  "system.security.cryptography.protecteddata.4.3.0.nupkg.sha512",
+		  "system.security.cryptography.protecteddata.nuspec"
+		]
+	  },
+	  "System.Text.Encoding/4.3.0": {
+		"sha512": "A/CSPPY+HAH7x7IYKU7KGIRHWwHcDi+Ai9ERC30fpCbUY1SpzFapRPPB5xYep9GWG/TnJD9/prAwvsdyrTHDog==",
+		"type": "package",
+		"path": "system.text.encoding/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net45/_._",
+		  "lib/portable-net45+win8+wp8+wpa81/_._",
+		  "lib/win8/_._",
+		  "lib/wp80/_._",
+		  "lib/wpa81/_._",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net45/_._",
+		  "ref/netcore50/System.Text.Encoding.dll",
+		  "ref/netcore50/System.Text.Encoding.xml",
+		  "ref/netcore50/de/System.Text.Encoding.xml",
+		  "ref/netcore50/es/System.Text.Encoding.xml",
+		  "ref/netcore50/fr/System.Text.Encoding.xml",
+		  "ref/netcore50/it/System.Text.Encoding.xml",
+		  "ref/netcore50/ja/System.Text.Encoding.xml",
+		  "ref/netcore50/ko/System.Text.Encoding.xml",
+		  "ref/netcore50/ru/System.Text.Encoding.xml",
+		  "ref/netcore50/zh-hans/System.Text.Encoding.xml",
+		  "ref/netcore50/zh-hant/System.Text.Encoding.xml",
+		  "ref/netstandard1.0/System.Text.Encoding.dll",
+		  "ref/netstandard1.0/System.Text.Encoding.xml",
+		  "ref/netstandard1.0/de/System.Text.Encoding.xml",
+		  "ref/netstandard1.0/es/System.Text.Encoding.xml",
+		  "ref/netstandard1.0/fr/System.Text.Encoding.xml",
+		  "ref/netstandard1.0/it/System.Text.Encoding.xml",
+		  "ref/netstandard1.0/ja/System.Text.Encoding.xml",
+		  "ref/netstandard1.0/ko/System.Text.Encoding.xml",
+		  "ref/netstandard1.0/ru/System.Text.Encoding.xml",
+		  "ref/netstandard1.0/zh-hans/System.Text.Encoding.xml",
+		  "ref/netstandard1.0/zh-hant/System.Text.Encoding.xml",
+		  "ref/netstandard1.3/System.Text.Encoding.dll",
+		  "ref/netstandard1.3/System.Text.Encoding.xml",
+		  "ref/netstandard1.3/de/System.Text.Encoding.xml",
+		  "ref/netstandard1.3/es/System.Text.Encoding.xml",
+		  "ref/netstandard1.3/fr/System.Text.Encoding.xml",
+		  "ref/netstandard1.3/it/System.Text.Encoding.xml",
+		  "ref/netstandard1.3/ja/System.Text.Encoding.xml",
+		  "ref/netstandard1.3/ko/System.Text.Encoding.xml",
+		  "ref/netstandard1.3/ru/System.Text.Encoding.xml",
+		  "ref/netstandard1.3/zh-hans/System.Text.Encoding.xml",
+		  "ref/netstandard1.3/zh-hant/System.Text.Encoding.xml",
+		  "ref/portable-net45+win8+wp8+wpa81/_._",
+		  "ref/win8/_._",
+		  "ref/wp80/_._",
+		  "ref/wpa81/_._",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "system.text.encoding.4.3.0.nupkg.sha512",
+		  "system.text.encoding.nuspec"
+		]
+	  },
+	  "System.Text.Encoding.Extensions/4.3.0": {
+		"sha512": "/Kfn26qRhqTegFFNLc6o2hM3EtDzyh8Kf94yik6DFVyGGmVilGQj0CK6I2xbMVT+PMHeF3V6njeQPc8lfuBicg==",
+		"type": "package",
+		"path": "system.text.encoding.extensions/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net45/_._",
+		  "lib/portable-net45+win8+wp8+wpa81/_._",
+		  "lib/win8/_._",
+		  "lib/wp80/_._",
+		  "lib/wpa81/_._",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net45/_._",
+		  "ref/netcore50/System.Text.Encoding.Extensions.dll",
+		  "ref/netcore50/System.Text.Encoding.Extensions.xml",
+		  "ref/netcore50/de/System.Text.Encoding.Extensions.xml",
+		  "ref/netcore50/es/System.Text.Encoding.Extensions.xml",
+		  "ref/netcore50/fr/System.Text.Encoding.Extensions.xml",
+		  "ref/netcore50/it/System.Text.Encoding.Extensions.xml",
+		  "ref/netcore50/ja/System.Text.Encoding.Extensions.xml",
+		  "ref/netcore50/ko/System.Text.Encoding.Extensions.xml",
+		  "ref/netcore50/ru/System.Text.Encoding.Extensions.xml",
+		  "ref/netcore50/zh-hans/System.Text.Encoding.Extensions.xml",
+		  "ref/netcore50/zh-hant/System.Text.Encoding.Extensions.xml",
+		  "ref/netstandard1.0/System.Text.Encoding.Extensions.dll",
+		  "ref/netstandard1.0/System.Text.Encoding.Extensions.xml",
+		  "ref/netstandard1.0/de/System.Text.Encoding.Extensions.xml",
+		  "ref/netstandard1.0/es/System.Text.Encoding.Extensions.xml",
+		  "ref/netstandard1.0/fr/System.Text.Encoding.Extensions.xml",
+		  "ref/netstandard1.0/it/System.Text.Encoding.Extensions.xml",
+		  "ref/netstandard1.0/ja/System.Text.Encoding.Extensions.xml",
+		  "ref/netstandard1.0/ko/System.Text.Encoding.Extensions.xml",
+		  "ref/netstandard1.0/ru/System.Text.Encoding.Extensions.xml",
+		  "ref/netstandard1.0/zh-hans/System.Text.Encoding.Extensions.xml",
+		  "ref/netstandard1.0/zh-hant/System.Text.Encoding.Extensions.xml",
+		  "ref/netstandard1.3/System.Text.Encoding.Extensions.dll",
+		  "ref/netstandard1.3/System.Text.Encoding.Extensions.xml",
+		  "ref/netstandard1.3/de/System.Text.Encoding.Extensions.xml",
+		  "ref/netstandard1.3/es/System.Text.Encoding.Extensions.xml",
+		  "ref/netstandard1.3/fr/System.Text.Encoding.Extensions.xml",
+		  "ref/netstandard1.3/it/System.Text.Encoding.Extensions.xml",
+		  "ref/netstandard1.3/ja/System.Text.Encoding.Extensions.xml",
+		  "ref/netstandard1.3/ko/System.Text.Encoding.Extensions.xml",
+		  "ref/netstandard1.3/ru/System.Text.Encoding.Extensions.xml",
+		  "ref/netstandard1.3/zh-hans/System.Text.Encoding.Extensions.xml",
+		  "ref/netstandard1.3/zh-hant/System.Text.Encoding.Extensions.xml",
+		  "ref/portable-net45+win8+wp8+wpa81/_._",
+		  "ref/win8/_._",
+		  "ref/wp80/_._",
+		  "ref/wpa81/_._",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "system.text.encoding.extensions.4.3.0.nupkg.sha512",
+		  "system.text.encoding.extensions.nuspec"
+		]
+	  },
+	  "System.Threading/4.3.0": {
+		"sha512": "TXL0xLMyMsjF+GVjnlZGQDJ+ht94pa51Sdu49b/rtiRQqjnCr5wU26qDL1Um5Xkygy97TiWdZviBksmrK0Sjlw==",
+		"type": "package",
+		"path": "system.threading/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net45/_._",
+		  "lib/netcore50/System.Threading.dll",
+		  "lib/netstandard1.3/System.Threading.dll",
+		  "lib/portable-net45+win8+wp8+wpa81/_._",
+		  "lib/win8/_._",
+		  "lib/wp80/_._",
+		  "lib/wpa81/_._",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net45/_._",
+		  "ref/netcore50/System.Threading.dll",
+		  "ref/netcore50/System.Threading.xml",
+		  "ref/netcore50/de/System.Threading.xml",
+		  "ref/netcore50/es/System.Threading.xml",
+		  "ref/netcore50/fr/System.Threading.xml",
+		  "ref/netcore50/it/System.Threading.xml",
+		  "ref/netcore50/ja/System.Threading.xml",
+		  "ref/netcore50/ko/System.Threading.xml",
+		  "ref/netcore50/ru/System.Threading.xml",
+		  "ref/netcore50/zh-hans/System.Threading.xml",
+		  "ref/netcore50/zh-hant/System.Threading.xml",
+		  "ref/netstandard1.0/System.Threading.dll",
+		  "ref/netstandard1.0/System.Threading.xml",
+		  "ref/netstandard1.0/de/System.Threading.xml",
+		  "ref/netstandard1.0/es/System.Threading.xml",
+		  "ref/netstandard1.0/fr/System.Threading.xml",
+		  "ref/netstandard1.0/it/System.Threading.xml",
+		  "ref/netstandard1.0/ja/System.Threading.xml",
+		  "ref/netstandard1.0/ko/System.Threading.xml",
+		  "ref/netstandard1.0/ru/System.Threading.xml",
+		  "ref/netstandard1.0/zh-hans/System.Threading.xml",
+		  "ref/netstandard1.0/zh-hant/System.Threading.xml",
+		  "ref/netstandard1.3/System.Threading.dll",
+		  "ref/netstandard1.3/System.Threading.xml",
+		  "ref/netstandard1.3/de/System.Threading.xml",
+		  "ref/netstandard1.3/es/System.Threading.xml",
+		  "ref/netstandard1.3/fr/System.Threading.xml",
+		  "ref/netstandard1.3/it/System.Threading.xml",
+		  "ref/netstandard1.3/ja/System.Threading.xml",
+		  "ref/netstandard1.3/ko/System.Threading.xml",
+		  "ref/netstandard1.3/ru/System.Threading.xml",
+		  "ref/netstandard1.3/zh-hans/System.Threading.xml",
+		  "ref/netstandard1.3/zh-hant/System.Threading.xml",
+		  "ref/portable-net45+win8+wp8+wpa81/_._",
+		  "ref/win8/_._",
+		  "ref/wp80/_._",
+		  "ref/wpa81/_._",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "runtimes/aot/lib/netcore50/System.Threading.dll",
+		  "system.threading.4.3.0.nupkg.sha512",
+		  "system.threading.nuspec"
+		]
+	  },
+	  "System.Threading.Tasks/4.3.0": {
+		"sha512": "hMoUsp8EfrVWub6+ZRT9EXmi3C8E/ZX4dpayEXKlygNneCnRZTNiWACsICU5Y5MY84W3NLNEu2nhop2nX/fT0A==",
+		"type": "package",
+		"path": "system.threading.tasks/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net45/_._",
+		  "lib/portable-net45+win8+wp8+wpa81/_._",
+		  "lib/win8/_._",
+		  "lib/wp80/_._",
+		  "lib/wpa81/_._",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net45/_._",
+		  "ref/netcore50/System.Threading.Tasks.dll",
+		  "ref/netcore50/System.Threading.Tasks.xml",
+		  "ref/netcore50/de/System.Threading.Tasks.xml",
+		  "ref/netcore50/es/System.Threading.Tasks.xml",
+		  "ref/netcore50/fr/System.Threading.Tasks.xml",
+		  "ref/netcore50/it/System.Threading.Tasks.xml",
+		  "ref/netcore50/ja/System.Threading.Tasks.xml",
+		  "ref/netcore50/ko/System.Threading.Tasks.xml",
+		  "ref/netcore50/ru/System.Threading.Tasks.xml",
+		  "ref/netcore50/zh-hans/System.Threading.Tasks.xml",
+		  "ref/netcore50/zh-hant/System.Threading.Tasks.xml",
+		  "ref/netstandard1.0/System.Threading.Tasks.dll",
+		  "ref/netstandard1.0/System.Threading.Tasks.xml",
+		  "ref/netstandard1.0/de/System.Threading.Tasks.xml",
+		  "ref/netstandard1.0/es/System.Threading.Tasks.xml",
+		  "ref/netstandard1.0/fr/System.Threading.Tasks.xml",
+		  "ref/netstandard1.0/it/System.Threading.Tasks.xml",
+		  "ref/netstandard1.0/ja/System.Threading.Tasks.xml",
+		  "ref/netstandard1.0/ko/System.Threading.Tasks.xml",
+		  "ref/netstandard1.0/ru/System.Threading.Tasks.xml",
+		  "ref/netstandard1.0/zh-hans/System.Threading.Tasks.xml",
+		  "ref/netstandard1.0/zh-hant/System.Threading.Tasks.xml",
+		  "ref/netstandard1.3/System.Threading.Tasks.dll",
+		  "ref/netstandard1.3/System.Threading.Tasks.xml",
+		  "ref/netstandard1.3/de/System.Threading.Tasks.xml",
+		  "ref/netstandard1.3/es/System.Threading.Tasks.xml",
+		  "ref/netstandard1.3/fr/System.Threading.Tasks.xml",
+		  "ref/netstandard1.3/it/System.Threading.Tasks.xml",
+		  "ref/netstandard1.3/ja/System.Threading.Tasks.xml",
+		  "ref/netstandard1.3/ko/System.Threading.Tasks.xml",
+		  "ref/netstandard1.3/ru/System.Threading.Tasks.xml",
+		  "ref/netstandard1.3/zh-hans/System.Threading.Tasks.xml",
+		  "ref/netstandard1.3/zh-hant/System.Threading.Tasks.xml",
+		  "ref/portable-net45+win8+wp8+wpa81/_._",
+		  "ref/win8/_._",
+		  "ref/wp80/_._",
+		  "ref/wpa81/_._",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "system.threading.tasks.4.3.0.nupkg.sha512",
+		  "system.threading.tasks.nuspec"
+		]
+	  },
+	  "System.Threading.Tasks.Dataflow/4.9.0": {
+		"sha512": "dTS+3D/GtG2/Pvc3E5YzVvAa7aQJgLDlZDIzukMOJjYudVOQOUXEU68y6Zi3Nn/jqIeB5kOCwrGbQFAKHVzXEQ==",
+		"type": "package",
+		"path": "system.threading.tasks.dataflow/4.9.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "LICENSE.TXT",
+		  "THIRD-PARTY-NOTICES.TXT",
+		  "lib/netstandard1.0/System.Threading.Tasks.Dataflow.dll",
+		  "lib/netstandard1.0/System.Threading.Tasks.Dataflow.xml",
+		  "lib/netstandard1.1/System.Threading.Tasks.Dataflow.dll",
+		  "lib/netstandard1.1/System.Threading.Tasks.Dataflow.xml",
+		  "lib/netstandard2.0/System.Threading.Tasks.Dataflow.dll",
+		  "lib/netstandard2.0/System.Threading.Tasks.Dataflow.xml",
+		  "lib/portable-net45+win8+wpa81/System.Threading.Tasks.Dataflow.dll",
+		  "lib/portable-net45+win8+wpa81/System.Threading.Tasks.Dataflow.xml",
+		  "paket-installmodel.cache",
+		  "system.threading.tasks.dataflow.4.9.0.nupkg.sha512",
+		  "system.threading.tasks.dataflow.nuspec",
+		  "useSharedDesignerContext.txt",
+		  "version.txt"
+		]
+	  },
+	  "System.Threading.Tasks.Extensions/4.5.1": {
+		"sha512": "WSKUTtLhPR8gllzIWO2x6l4lmAIfbyMAiTlyXAis4QBDonXK4b4S6F8zGARX4/P8wH3DH+sLdhamCiHn+fTU1A==",
+		"type": "package",
+		"path": "system.threading.tasks.extensions/4.5.1",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "LICENSE.TXT",
+		  "THIRD-PARTY-NOTICES.TXT",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/netcoreapp2.1/_._",
+		  "lib/netstandard1.0/System.Threading.Tasks.Extensions.dll",
+		  "lib/netstandard1.0/System.Threading.Tasks.Extensions.xml",
+		  "lib/netstandard2.0/System.Threading.Tasks.Extensions.dll",
+		  "lib/netstandard2.0/System.Threading.Tasks.Extensions.xml",
+		  "lib/portable-net45+win8+wp8+wpa81/System.Threading.Tasks.Extensions.dll",
+		  "lib/portable-net45+win8+wp8+wpa81/System.Threading.Tasks.Extensions.xml",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/netcoreapp2.1/_._",
+		  "ref/netstandard1.0/System.Threading.Tasks.Extensions.dll",
+		  "ref/netstandard1.0/System.Threading.Tasks.Extensions.xml",
+		  "ref/netstandard2.0/System.Threading.Tasks.Extensions.dll",
+		  "ref/netstandard2.0/System.Threading.Tasks.Extensions.xml",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "system.threading.tasks.extensions.4.5.1.nupkg.sha512",
+		  "system.threading.tasks.extensions.nuspec",
+		  "useSharedDesignerContext.txt",
+		  "version.txt"
+		]
+	  },
+	  "System.Threading.Thread/4.3.0": {
+		"sha512": "p9W93yjgtQL+YruKrMfhysDbGHxCw6B+3ZI8xqEZmYoqYXVTEnkuBdv5iFSLfD1FVknC8VoSSnteKmuGCPH6Yg==",
+		"type": "package",
+		"path": "system.threading.thread/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net46/System.Threading.Thread.dll",
+		  "lib/netcore50/_._",
+		  "lib/netstandard1.3/System.Threading.Thread.dll",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net46/System.Threading.Thread.dll",
+		  "ref/netstandard1.3/System.Threading.Thread.dll",
+		  "ref/netstandard1.3/System.Threading.Thread.xml",
+		  "ref/netstandard1.3/de/System.Threading.Thread.xml",
+		  "ref/netstandard1.3/es/System.Threading.Thread.xml",
+		  "ref/netstandard1.3/fr/System.Threading.Thread.xml",
+		  "ref/netstandard1.3/it/System.Threading.Thread.xml",
+		  "ref/netstandard1.3/ja/System.Threading.Thread.xml",
+		  "ref/netstandard1.3/ko/System.Threading.Thread.xml",
+		  "ref/netstandard1.3/ru/System.Threading.Thread.xml",
+		  "ref/netstandard1.3/zh-hans/System.Threading.Thread.xml",
+		  "ref/netstandard1.3/zh-hant/System.Threading.Thread.xml",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "system.threading.thread.4.3.0.nupkg.sha512",
+		  "system.threading.thread.nuspec"
+		]
+	  },
+	  "System.Threading.ThreadPool/4.3.0": {
+		"sha512": "DlJ/+1Fj+RgS7IKXJWgXIVT02dDxTMRuAuNx74EetSuQ6/1hb011GkX8ulvc6xkL9KEzQ6kNMy//pfxCt4ux4Q==",
+		"type": "package",
+		"path": "system.threading.threadpool/4.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  "ThirdPartyNotices.txt",
+		  "dotnet_library_license.txt",
+		  "lib/MonoAndroid10/_._",
+		  "lib/MonoTouch10/_._",
+		  "lib/net46/System.Threading.ThreadPool.dll",
+		  "lib/netcore50/_._",
+		  "lib/netstandard1.3/System.Threading.ThreadPool.dll",
+		  "lib/xamarinios10/_._",
+		  "lib/xamarinmac20/_._",
+		  "lib/xamarintvos10/_._",
+		  "lib/xamarinwatchos10/_._",
+		  "paket-installmodel.cache",
+		  "ref/MonoAndroid10/_._",
+		  "ref/MonoTouch10/_._",
+		  "ref/net46/System.Threading.ThreadPool.dll",
+		  "ref/netstandard1.3/System.Threading.ThreadPool.dll",
+		  "ref/netstandard1.3/System.Threading.ThreadPool.xml",
+		  "ref/netstandard1.3/de/System.Threading.ThreadPool.xml",
+		  "ref/netstandard1.3/es/System.Threading.ThreadPool.xml",
+		  "ref/netstandard1.3/fr/System.Threading.ThreadPool.xml",
+		  "ref/netstandard1.3/it/System.Threading.ThreadPool.xml",
+		  "ref/netstandard1.3/ja/System.Threading.ThreadPool.xml",
+		  "ref/netstandard1.3/ko/System.Threading.ThreadPool.xml",
+		  "ref/netstandard1.3/ru/System.Threading.ThreadPool.xml",
+		  "ref/netstandard1.3/zh-hans/System.Threading.ThreadPool.xml",
+		  "ref/netstandard1.3/zh-hant/System.Threading.ThreadPool.xml",
+		  "ref/xamarinios10/_._",
+		  "ref/xamarinmac20/_._",
+		  "ref/xamarintvos10/_._",
+		  "ref/xamarinwatchos10/_._",
+		  "system.threading.threadpool.4.3.0.nupkg.sha512",
+		  "system.threading.threadpool.nuspec"
+		]
+	  },
+	  "YamlDotNet/5.3.0": {
+		"sha512": "w9oY3x4qZ9PR5fj5P9NIxXFyx47ZTHy30J7JKkYwDABh8quAs5H66Rv+9F5ib9je9lEdm2vrBRlLPght9erRQg==",
+		"type": "package",
+		"path": "yamldotnet/5.3.0",
+		"files": [
+		  ".nupkg.metadata",
+		  ".signature.p7s",
+		  "lib/net20/YamlDotNet.dll",
+		  "lib/net20/YamlDotNet.xml",
+		  "lib/net35/YamlDotNet.dll",
+		  "lib/net35/YamlDotNet.xml",
+		  "lib/net45/YamlDotNet.dll",
+		  "lib/net45/YamlDotNet.xml",
+		  "lib/netstandard1.3/YamlDotNet.dll",
+		  "lib/netstandard1.3/YamlDotNet.xml",
+		  "yamldotnet.5.3.0.nupkg.sha512",
+		  "yamldotnet.nuspec"
+		]
+	  },
+	  "Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common/1.0.0": {
+		"type": "project",
+		"path": "../Common/MS.VS.Services.Governance.ComponentDetection.Common.csproj",
+		"msbuildProject": "../Common/MS.VS.Services.Governance.ComponentDetection.Common.csproj"
+	  },
+	  "Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts/1.0.0": {
+		"type": "project",
+		"path": "../Contracts/MS.VS.Services.Governance.ComponentDetection.Contracts.csproj",
+		"msbuildProject": "../Contracts/MS.VS.Services.Governance.ComponentDetection.Contracts.csproj"
+	  }
+	},
+	"projectFileDependencyGroups": {
+	  ".NETCoreApp,Version=v2.2": [
+		"DotNet.Glob >= 2.1.1",
+		"Microsoft.NETCore.App >= 2.2.8",
+		"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Common >= 1.0.0",
+		"Microsoft.VisualStudio.Services.Governance.ComponentDetection.Contracts >= 1.0.0",
+		"MinVer >= 2.5.0",
+		"Nett >= 0.10.0",
+		"Newtonsoft.Json >= 12.0.3",
+		"NuGet.ProjectModel >= 5.6.0",
+		"NuGet.Versioning >= 5.6.0",
+		"Polly >= 7.0.3",
+		"SemanticVersioning >= 1.2.0",
+		"StyleCop.Analyzers >= 1.0.2",
+		"System.Composition.AttributedModel >= 1.4.0",
+		"System.Composition.Convention >= 1.4.0",
+		"System.Composition.Hosting >= 1.4.0",
+		"System.Composition.Runtime >= 1.4.0",
+		"System.Composition.TypedParts >= 1.4.0",
+		"System.Reactive >= 4.1.2",
+		"System.Threading.Tasks.Dataflow >= 4.9.0",
+		"coverlet.msbuild >= 2.5.1",
+		"yamldotnet >= 5.3.0"
+	  ]
+	},
+	"packageFolders": {
+	  "C:\\Users\\brphelps\\.nuget\\packages\\": {},
+	  "C:\\Program Files\\dotnet\\sdk\\NuGetFallbackFolder": {}
+	},
+	"project": {
+	  "version": "1.0.0",
+	  "restore": {
+		"projectUniqueName": "D:\\Source\\componentdetection-bcde\\src\\Detectors\\MS.VS.Services.Governance.ComponentDetection.Detectors.csproj",
+		"projectName": "Microsoft.VisualStudio.Services.Governance.ComponentDetection.Detectors",
+		"projectPath": "D:\\Source\\componentdetection-bcde\\src\\Detectors\\MS.VS.Services.Governance.ComponentDetection.Detectors.csproj",
+		"packagesPath": "C:\\Users\\brphelps\\.nuget\\packages\\",
+		"outputPath": "D:\\Source\\componentdetection-bcde\\src\\Detectors\\obj\\",
+		"projectStyle": "PackageReference",
+		"fallbackFolders": [
+		  "C:\\Program Files\\dotnet\\sdk\\NuGetFallbackFolder"
+		],
+		"configFilePaths": [
+		  "C:\\Users\\brphelps\\AppData\\Roaming\\NuGet\\NuGet.Config",
+		  "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
+		],
+		"originalTargetFrameworks": [
+		  "netcoreapp2.2"
+		],
+		"sources": {
+		  "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
+		  "C:\\temp\\AI": {}
+		},
+		"frameworks": {
+		  "netcoreapp2.2": {
+			"projectReferences": {
+			  "D:\\Source\\componentdetection-bcde\\src\\Common\\MS.VS.Services.Governance.ComponentDetection.Common.csproj": {
+				"projectPath": "D:\\Source\\componentdetection-bcde\\src\\Common\\MS.VS.Services.Governance.ComponentDetection.Common.csproj"
+			  },
+			  "D:\\Source\\componentdetection-bcde\\src\\Contracts\\MS.VS.Services.Governance.ComponentDetection.Contracts.csproj": {
+				"projectPath": "D:\\Source\\componentdetection-bcde\\src\\Contracts\\MS.VS.Services.Governance.ComponentDetection.Contracts.csproj"
+			  }
+			}
+		  }
+		},
+		"warningProperties": {
+		  "noWarn": [
+			"NU1608"
+		  ],
+		  "warnAsError": [
+			"NU1605"
+		  ]
+		}
+	  },
+	  "frameworks": {
+		"netcoreapp2.2": {
+		  "dependencies": {
+			"DotNet.Glob": {
+			  "suppressParent": "Compile",
+			  "target": "Package",
+			  "version": "[2.1.1, )"
+			},
+			"Microsoft.NETCore.App": {
+			  "suppressParent": "All",
+			  "target": "Package",
+			  "version": "[2.2.8, )",
+			  "autoReferenced": true
+			},
+			"MinVer": {
+			  "include": "Build, Analyzers",
+			  "suppressParent": "All",
+			  "target": "Package",
+			  "version": "[2.5.0, )"
+			},
+			"Nett": {
+			  "suppressParent": "Compile",
+			  "target": "Package",
+			  "version": "[0.10.0, )"
+			},
+			"Newtonsoft.Json": {
+			  "suppressParent": "Compile",
+			  "target": "Package",
+			  "version": "[12.0.3, )"
+			},
+			"NuGet.ProjectModel": {
+			  "suppressParent": "Compile",
+			  "target": "Package",
+			  "version": "[5.6.0, )"
+			},
+			"NuGet.Versioning": {
+			  "suppressParent": "Compile",
+			  "target": "Package",
+			  "version": "[5.6.0, )"
+			},
+			"Polly": {
+			  "suppressParent": "Compile",
+			  "target": "Package",
+			  "version": "[7.0.3, )"
+			},
+			"SemanticVersioning": {
+			  "suppressParent": "Compile",
+			  "target": "Package",
+			  "version": "[1.2.0, )"
+			},
+			"StyleCop.Analyzers": {
+			  "include": "Build, Analyzers",
+			  "suppressParent": "All",
+			  "target": "Package",
+			  "version": "[1.0.2, )"
+			},
+			"System.Composition.AttributedModel": {
+			  "suppressParent": "Compile",
+			  "target": "Package",
+			  "version": "[1.4.0, )"
+			},
+			"System.Composition.Convention": {
+			  "suppressParent": "Compile",
+			  "target": "Package",
+			  "version": "[1.4.0, )"
+			},
+			"System.Composition.Hosting": {
+			  "suppressParent": "Compile",
+			  "target": "Package",
+			  "version": "[1.4.0, )"
+			},
+			"System.Composition.Runtime": {
+			  "suppressParent": "Compile",
+			  "target": "Package",
+			  "version": "[1.4.0, )"
+			},
+			"System.Composition.TypedParts": {
+			  "suppressParent": "Compile",
+			  "target": "Package",
+			  "version": "[1.4.0, )"
+			},
+			"System.Reactive": {
+			  "suppressParent": "Compile",
+			  "target": "Package",
+			  "version": "[4.1.2, )"
+			},
+			"System.Threading.Tasks.Dataflow": {
+			  "suppressParent": "Compile",
+			  "target": "Package",
+			  "version": "[4.9.0, )"
+			},
+			"coverlet.msbuild": {
+			  "include": "Build, Analyzers",
+			  "suppressParent": "All",
+			  "target": "Package",
+			  "version": "[2.5.1, )"
+			},
+			"yamldotnet": {
+			  "suppressParent": "Compile",
+			  "target": "Package",
+			  "version": "[5.3.0, )"
+			}
+		  },
+		  "imports": [
+			"net461",
+			"net462",
+			"net47",
+			"net471",
+			"net472",
+			"net48"
+		  ],
+		  "assetTargetFallback": true,
+		  "warn": true,
+		  "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\3.1.201\\RuntimeIdentifierGraph.json"
+		}
+	  }
+	}
+  }
\ No newline at end of file
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/Resources/project_assets_3_1.json b/test/Microsoft.ComponentDetection.Detectors.Tests/Resources/project_assets_3_1.json
new file mode 100644
index 000000000..ebf002261
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/Resources/project_assets_3_1.json
@@ -0,0 +1,782 @@
+{
+    "version": 3,
+    "targets": {
+      ".NETCoreApp,Version=v3.1": {
+        "Microsoft.Extensions.DependencyModel/3.0.0": {
+          "type": "package",
+          "dependencies": {
+            "System.Text.Json": "4.6.0"
+          },
+          "compile": {
+            "lib/netstandard2.0/Microsoft.Extensions.DependencyModel.dll": {}
+          },
+          "runtime": {
+            "lib/netstandard2.0/Microsoft.Extensions.DependencyModel.dll": {}
+          }
+        },
+        "Microsoft.NETCore.Platforms/1.1.0": {
+          "type": "package",
+          "compile": {
+            "lib/netstandard1.0/_._": {}
+          },
+          "runtime": {
+            "lib/netstandard1.0/_._": {}
+          }
+        },
+        "Microsoft.NETCore.Targets/1.1.0": {
+          "type": "package",
+          "compile": {
+            "lib/netstandard1.0/_._": {}
+          },
+          "runtime": {
+            "lib/netstandard1.0/_._": {}
+          }
+        },
+        "System.IO/4.3.0": {
+          "type": "package",
+          "dependencies": {
+            "Microsoft.NETCore.Platforms": "1.1.0",
+            "Microsoft.NETCore.Targets": "1.1.0",
+            "System.Runtime": "4.3.0",
+            "System.Text.Encoding": "4.3.0",
+            "System.Threading.Tasks": "4.3.0"
+          },
+          "compile": {
+            "ref/netstandard1.5/System.IO.dll": {}
+          }
+        },
+        "System.Reflection/4.3.0": {
+          "type": "package",
+          "dependencies": {
+            "Microsoft.NETCore.Platforms": "1.1.0",
+            "Microsoft.NETCore.Targets": "1.1.0",
+            "System.IO": "4.3.0",
+            "System.Reflection.Primitives": "4.3.0",
+            "System.Runtime": "4.3.0"
+          },
+          "compile": {
+            "ref/netstandard1.5/System.Reflection.dll": {}
+          }
+        },
+        "System.Reflection.Primitives/4.3.0": {
+          "type": "package",
+          "dependencies": {
+            "Microsoft.NETCore.Platforms": "1.1.0",
+            "Microsoft.NETCore.Targets": "1.1.0",
+            "System.Runtime": "4.3.0"
+          },
+          "compile": {
+            "ref/netstandard1.0/System.Reflection.Primitives.dll": {}
+          }
+        },
+        "System.Runtime/4.3.0": {
+          "type": "package",
+          "dependencies": {
+            "Microsoft.NETCore.Platforms": "1.1.0",
+            "Microsoft.NETCore.Targets": "1.1.0"
+          },
+          "compile": {
+            "ref/netstandard1.5/System.Runtime.dll": {}
+          }
+        },
+        "System.Runtime.Loader/4.3.0": {
+          "type": "package",
+          "dependencies": {
+            "System.IO": "4.3.0",
+            "System.Reflection": "4.3.0",
+            "System.Runtime": "4.3.0"
+          },
+          "compile": {
+            "ref/netstandard1.5/System.Runtime.Loader.dll": {}
+          },
+          "runtime": {
+            "lib/netstandard1.5/System.Runtime.Loader.dll": {}
+          }
+        },
+        "System.Text.Encoding/4.3.0": {
+          "type": "package",
+          "dependencies": {
+            "Microsoft.NETCore.Platforms": "1.1.0",
+            "Microsoft.NETCore.Targets": "1.1.0",
+            "System.Runtime": "4.3.0"
+          },
+          "compile": {
+            "ref/netstandard1.3/System.Text.Encoding.dll": {}
+          }
+        },
+        "System.Text.Json/4.6.0": {
+          "type": "package",
+          "compile": {
+            "lib/netcoreapp3.0/System.Text.Json.dll": {}
+          },
+          "runtime": {
+            "lib/netcoreapp3.0/System.Text.Json.dll": {}
+          }
+        },
+        "System.Threading.Tasks/4.3.0": {
+          "type": "package",
+          "dependencies": {
+            "Microsoft.NETCore.Platforms": "1.1.0",
+            "Microsoft.NETCore.Targets": "1.1.0",
+            "System.Runtime": "4.3.0"
+          },
+          "compile": {
+            "ref/netstandard1.3/System.Threading.Tasks.dll": {}
+          }
+        },
+        "ExtCore.Infrastructure/5.1.0": {
+          "type": "project",
+          "framework": ".NETCoreApp,Version=v3.1",
+          "compile": {
+            "bin/placeholder/ExtCore.Infrastructure.dll": {}
+          },
+          "runtime": {
+            "bin/placeholder/ExtCore.Infrastructure.dll": {}
+          },
+          "frameworkReferences": [
+            "Microsoft.AspNetCore.App"
+          ]
+        }
+      }
+    },
+    "libraries": {
+      "Microsoft.Extensions.DependencyModel/3.0.0": {
+        "sha512": "Iaectmzg9Dc4ZbKX/FurrRjgO/I8rTumL5UU+Uube6vZuGetcnXoIgTA94RthFWePhdMVm8MMhVFJZdbzMsdyQ==",
+        "type": "package",
+        "path": "microsoft.extensions.dependencymodel/3.0.0",
+        "files": [
+          ".nupkg.metadata",
+          ".signature.p7s",
+          "LICENSE.TXT",
+          "THIRD-PARTY-NOTICES.TXT",
+          "lib/net451/Microsoft.Extensions.DependencyModel.dll",
+          "lib/net451/Microsoft.Extensions.DependencyModel.xml",
+          "lib/netstandard1.3/Microsoft.Extensions.DependencyModel.dll",
+          "lib/netstandard1.3/Microsoft.Extensions.DependencyModel.xml",
+          "lib/netstandard1.6/Microsoft.Extensions.DependencyModel.dll",
+          "lib/netstandard1.6/Microsoft.Extensions.DependencyModel.xml",
+          "lib/netstandard2.0/Microsoft.Extensions.DependencyModel.dll",
+          "lib/netstandard2.0/Microsoft.Extensions.DependencyModel.xml",
+          "microsoft.extensions.dependencymodel.3.0.0.nupkg.sha512",
+          "microsoft.extensions.dependencymodel.nuspec"
+        ]
+      },
+      "Microsoft.NETCore.Platforms/1.1.0": {
+        "sha512": "PnIZID2//DQPMbWy3cc6UCscCdT7TXl00Y7xVweMs4W2RCiXTbMQvFvaVTLPe45qMvnizE8Mj1j2V91K6WzVMw==",
+        "type": "package",
+        "path": "microsoft.netcore.platforms/1.1.0",
+        "files": [
+          ".nupkg.metadata",
+          ".signature.p7s",
+          "ThirdPartyNotices.txt",
+          "dotnet_library_license.txt",
+          "lib/netstandard1.0/_._",
+          "microsoft.netcore.platforms.1.1.0.nupkg.sha512",
+          "microsoft.netcore.platforms.nuspec",
+          "runtime.json"
+        ]
+      },
+      "Microsoft.NETCore.Targets/1.1.0": {
+        "sha512": "vVxDoK+YKIwR/JVnHzUZgsmfpP/2n+Y6A1hsiV5q0Kp9M3emwvtCbIH6zSi64Nvb75+NMl1CiJxbq17oZ5OaWg==",
+        "type": "package",
+        "path": "microsoft.netcore.targets/1.1.0",
+        "files": [
+          ".nupkg.metadata",
+          ".signature.p7s",
+          "ThirdPartyNotices.txt",
+          "dotnet_library_license.txt",
+          "lib/netstandard1.0/_._",
+          "microsoft.netcore.targets.1.1.0.nupkg.sha512",
+          "microsoft.netcore.targets.nuspec",
+          "runtime.json"
+        ]
+      },
+      "System.IO/4.3.0": {
+        "sha512": "w9k/LKzv6pDgC/HUgJkNMVCC0DZZ6yaYoj0H2WNK2VOA7a0di04PDnprGU0PIVQi+3d1RIoBvp9Eom2tbKIgkA==",
+        "type": "package",
+        "path": "system.io/4.3.0",
+        "files": [
+          ".nupkg.metadata",
+          "ThirdPartyNotices.txt",
+          "dotnet_library_license.txt",
+          "lib/MonoAndroid10/_._",
+          "lib/MonoTouch10/_._",
+          "lib/net45/_._",
+          "lib/net462/System.IO.dll",
+          "lib/portable-net45+win8+wp8+wpa81/_._",
+          "lib/win8/_._",
+          "lib/wp80/_._",
+          "lib/wpa81/_._",
+          "lib/xamarinios10/_._",
+          "lib/xamarinmac20/_._",
+          "lib/xamarintvos10/_._",
+          "lib/xamarinwatchos10/_._",
+          "paket-installmodel.cache",
+          "ref/MonoAndroid10/_._",
+          "ref/MonoTouch10/_._",
+          "ref/net45/_._",
+          "ref/net462/System.IO.dll",
+          "ref/netcore50/System.IO.dll",
+          "ref/netcore50/System.IO.xml",
+          "ref/netcore50/de/System.IO.xml",
+          "ref/netcore50/es/System.IO.xml",
+          "ref/netcore50/fr/System.IO.xml",
+          "ref/netcore50/it/System.IO.xml",
+          "ref/netcore50/ja/System.IO.xml",
+          "ref/netcore50/ko/System.IO.xml",
+          "ref/netcore50/ru/System.IO.xml",
+          "ref/netcore50/zh-hans/System.IO.xml",
+          "ref/netcore50/zh-hant/System.IO.xml",
+          "ref/netstandard1.0/System.IO.dll",
+          "ref/netstandard1.0/System.IO.xml",
+          "ref/netstandard1.0/de/System.IO.xml",
+          "ref/netstandard1.0/es/System.IO.xml",
+          "ref/netstandard1.0/fr/System.IO.xml",
+          "ref/netstandard1.0/it/System.IO.xml",
+          "ref/netstandard1.0/ja/System.IO.xml",
+          "ref/netstandard1.0/ko/System.IO.xml",
+          "ref/netstandard1.0/ru/System.IO.xml",
+          "ref/netstandard1.0/zh-hans/System.IO.xml",
+          "ref/netstandard1.0/zh-hant/System.IO.xml",
+          "ref/netstandard1.3/System.IO.dll",
+          "ref/netstandard1.3/System.IO.xml",
+          "ref/netstandard1.3/de/System.IO.xml",
+          "ref/netstandard1.3/es/System.IO.xml",
+          "ref/netstandard1.3/fr/System.IO.xml",
+          "ref/netstandard1.3/it/System.IO.xml",
+          "ref/netstandard1.3/ja/System.IO.xml",
+          "ref/netstandard1.3/ko/System.IO.xml",
+          "ref/netstandard1.3/ru/System.IO.xml",
+          "ref/netstandard1.3/zh-hans/System.IO.xml",
+          "ref/netstandard1.3/zh-hant/System.IO.xml",
+          "ref/netstandard1.5/System.IO.dll",
+          "ref/netstandard1.5/System.IO.xml",
+          "ref/netstandard1.5/de/System.IO.xml",
+          "ref/netstandard1.5/es/System.IO.xml",
+          "ref/netstandard1.5/fr/System.IO.xml",
+          "ref/netstandard1.5/it/System.IO.xml",
+          "ref/netstandard1.5/ja/System.IO.xml",
+          "ref/netstandard1.5/ko/System.IO.xml",
+          "ref/netstandard1.5/ru/System.IO.xml",
+          "ref/netstandard1.5/zh-hans/System.IO.xml",
+          "ref/netstandard1.5/zh-hant/System.IO.xml",
+          "ref/portable-net45+win8+wp8+wpa81/_._",
+          "ref/win8/_._",
+          "ref/wp80/_._",
+          "ref/wpa81/_._",
+          "ref/xamarinios10/_._",
+          "ref/xamarinmac20/_._",
+          "ref/xamarintvos10/_._",
+          "ref/xamarinwatchos10/_._",
+          "system.io.4.3.0.nupkg.sha512",
+          "system.io.nuspec"
+        ]
+      },
+      "System.Reflection/4.3.0": {
+        "sha512": "vVNX6iFKa2XrZUoZWE8AFDpInpdiqwt5HLrsb1YHLpj+cylnPySK1QOIeWMOG7WOCoQg341bQQ5yXmeKNUkIag==",
+        "type": "package",
+        "path": "system.reflection/4.3.0",
+        "files": [
+          ".nupkg.metadata",
+          "ThirdPartyNotices.txt",
+          "dotnet_library_license.txt",
+          "lib/MonoAndroid10/_._",
+          "lib/MonoTouch10/_._",
+          "lib/net45/_._",
+          "lib/net462/System.Reflection.dll",
+          "lib/portable-net45+win8+wp8+wpa81/_._",
+          "lib/win8/_._",
+          "lib/wp80/_._",
+          "lib/wpa81/_._",
+          "lib/xamarinios10/_._",
+          "lib/xamarinmac20/_._",
+          "lib/xamarintvos10/_._",
+          "lib/xamarinwatchos10/_._",
+          "paket-installmodel.cache",
+          "ref/MonoAndroid10/_._",
+          "ref/MonoTouch10/_._",
+          "ref/net45/_._",
+          "ref/net462/System.Reflection.dll",
+          "ref/netcore50/System.Reflection.dll",
+          "ref/netcore50/System.Reflection.xml",
+          "ref/netcore50/de/System.Reflection.xml",
+          "ref/netcore50/es/System.Reflection.xml",
+          "ref/netcore50/fr/System.Reflection.xml",
+          "ref/netcore50/it/System.Reflection.xml",
+          "ref/netcore50/ja/System.Reflection.xml",
+          "ref/netcore50/ko/System.Reflection.xml",
+          "ref/netcore50/ru/System.Reflection.xml",
+          "ref/netcore50/zh-hans/System.Reflection.xml",
+          "ref/netcore50/zh-hant/System.Reflection.xml",
+          "ref/netstandard1.0/System.Reflection.dll",
+          "ref/netstandard1.0/System.Reflection.xml",
+          "ref/netstandard1.0/de/System.Reflection.xml",
+          "ref/netstandard1.0/es/System.Reflection.xml",
+          "ref/netstandard1.0/fr/System.Reflection.xml",
+          "ref/netstandard1.0/it/System.Reflection.xml",
+          "ref/netstandard1.0/ja/System.Reflection.xml",
+          "ref/netstandard1.0/ko/System.Reflection.xml",
+          "ref/netstandard1.0/ru/System.Reflection.xml",
+          "ref/netstandard1.0/zh-hans/System.Reflection.xml",
+          "ref/netstandard1.0/zh-hant/System.Reflection.xml",
+          "ref/netstandard1.3/System.Reflection.dll",
+          "ref/netstandard1.3/System.Reflection.xml",
+          "ref/netstandard1.3/de/System.Reflection.xml",
+          "ref/netstandard1.3/es/System.Reflection.xml",
+          "ref/netstandard1.3/fr/System.Reflection.xml",
+          "ref/netstandard1.3/it/System.Reflection.xml",
+          "ref/netstandard1.3/ja/System.Reflection.xml",
+          "ref/netstandard1.3/ko/System.Reflection.xml",
+          "ref/netstandard1.3/ru/System.Reflection.xml",
+          "ref/netstandard1.3/zh-hans/System.Reflection.xml",
+          "ref/netstandard1.3/zh-hant/System.Reflection.xml",
+          "ref/netstandard1.5/System.Reflection.dll",
+          "ref/netstandard1.5/System.Reflection.xml",
+          "ref/netstandard1.5/de/System.Reflection.xml",
+          "ref/netstandard1.5/es/System.Reflection.xml",
+          "ref/netstandard1.5/fr/System.Reflection.xml",
+          "ref/netstandard1.5/it/System.Reflection.xml",
+          "ref/netstandard1.5/ja/System.Reflection.xml",
+          "ref/netstandard1.5/ko/System.Reflection.xml",
+          "ref/netstandard1.5/ru/System.Reflection.xml",
+          "ref/netstandard1.5/zh-hans/System.Reflection.xml",
+          "ref/netstandard1.5/zh-hant/System.Reflection.xml",
+          "ref/portable-net45+win8+wp8+wpa81/_._",
+          "ref/win8/_._",
+          "ref/wp80/_._",
+          "ref/wpa81/_._",
+          "ref/xamarinios10/_._",
+          "ref/xamarinmac20/_._",
+          "ref/xamarintvos10/_._",
+          "ref/xamarinwatchos10/_._",
+          "system.reflection.4.3.0.nupkg.sha512",
+          "system.reflection.nuspec"
+        ]
+      },
+      "System.Reflection.Primitives/4.3.0": {
+        "sha512": "oaBgqLXHdK81uoGe83s5r+iP+uOk5ZaNp/LxS4ity/V5gDCk6KGdXMTQruGHDl1EajJ11xf5wDMDdz9febmTrw==",
+        "type": "package",
+        "path": "system.reflection.primitives/4.3.0",
+        "files": [
+          ".nupkg.metadata",
+          "ThirdPartyNotices.txt",
+          "dotnet_library_license.txt",
+          "lib/MonoAndroid10/_._",
+          "lib/MonoTouch10/_._",
+          "lib/net45/_._",
+          "lib/portable-net45+win8+wp8+wpa81/_._",
+          "lib/win8/_._",
+          "lib/wp80/_._",
+          "lib/wpa81/_._",
+          "lib/xamarinios10/_._",
+          "lib/xamarinmac20/_._",
+          "lib/xamarintvos10/_._",
+          "lib/xamarinwatchos10/_._",
+          "paket-installmodel.cache",
+          "ref/MonoAndroid10/_._",
+          "ref/MonoTouch10/_._",
+          "ref/net45/_._",
+          "ref/netcore50/System.Reflection.Primitives.dll",
+          "ref/netcore50/System.Reflection.Primitives.xml",
+          "ref/netcore50/de/System.Reflection.Primitives.xml",
+          "ref/netcore50/es/System.Reflection.Primitives.xml",
+          "ref/netcore50/fr/System.Reflection.Primitives.xml",
+          "ref/netcore50/it/System.Reflection.Primitives.xml",
+          "ref/netcore50/ja/System.Reflection.Primitives.xml",
+          "ref/netcore50/ko/System.Reflection.Primitives.xml",
+          "ref/netcore50/ru/System.Reflection.Primitives.xml",
+          "ref/netcore50/zh-hans/System.Reflection.Primitives.xml",
+          "ref/netcore50/zh-hant/System.Reflection.Primitives.xml",
+          "ref/netstandard1.0/System.Reflection.Primitives.dll",
+          "ref/netstandard1.0/System.Reflection.Primitives.xml",
+          "ref/netstandard1.0/de/System.Reflection.Primitives.xml",
+          "ref/netstandard1.0/es/System.Reflection.Primitives.xml",
+          "ref/netstandard1.0/fr/System.Reflection.Primitives.xml",
+          "ref/netstandard1.0/it/System.Reflection.Primitives.xml",
+          "ref/netstandard1.0/ja/System.Reflection.Primitives.xml",
+          "ref/netstandard1.0/ko/System.Reflection.Primitives.xml",
+          "ref/netstandard1.0/ru/System.Reflection.Primitives.xml",
+          "ref/netstandard1.0/zh-hans/System.Reflection.Primitives.xml",
+          "ref/netstandard1.0/zh-hant/System.Reflection.Primitives.xml",
+          "ref/portable-net45+win8+wp8+wpa81/_._",
+          "ref/win8/_._",
+          "ref/wp80/_._",
+          "ref/wpa81/_._",
+          "ref/xamarinios10/_._",
+          "ref/xamarinmac20/_._",
+          "ref/xamarintvos10/_._",
+          "ref/xamarinwatchos10/_._",
+          "system.reflection.primitives.4.3.0.nupkg.sha512",
+          "system.reflection.primitives.nuspec"
+        ]
+      },
+      "System.Runtime/4.3.0": {
+        "sha512": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
+        "type": "package",
+        "path": "system.runtime/4.3.0",
+        "files": [
+          ".nupkg.metadata",
+          ".signature.p7s",
+          "ThirdPartyNotices.txt",
+          "dotnet_library_license.txt",
+          "lib/MonoAndroid10/_._",
+          "lib/MonoTouch10/_._",
+          "lib/net45/_._",
+          "lib/net462/System.Runtime.dll",
+          "lib/portable-net45+win8+wp80+wpa81/_._",
+          "lib/win8/_._",
+          "lib/wp80/_._",
+          "lib/wpa81/_._",
+          "lib/xamarinios10/_._",
+          "lib/xamarinmac20/_._",
+          "lib/xamarintvos10/_._",
+          "lib/xamarinwatchos10/_._",
+          "ref/MonoAndroid10/_._",
+          "ref/MonoTouch10/_._",
+          "ref/net45/_._",
+          "ref/net462/System.Runtime.dll",
+          "ref/netcore50/System.Runtime.dll",
+          "ref/netcore50/System.Runtime.xml",
+          "ref/netcore50/de/System.Runtime.xml",
+          "ref/netcore50/es/System.Runtime.xml",
+          "ref/netcore50/fr/System.Runtime.xml",
+          "ref/netcore50/it/System.Runtime.xml",
+          "ref/netcore50/ja/System.Runtime.xml",
+          "ref/netcore50/ko/System.Runtime.xml",
+          "ref/netcore50/ru/System.Runtime.xml",
+          "ref/netcore50/zh-hans/System.Runtime.xml",
+          "ref/netcore50/zh-hant/System.Runtime.xml",
+          "ref/netstandard1.0/System.Runtime.dll",
+          "ref/netstandard1.0/System.Runtime.xml",
+          "ref/netstandard1.0/de/System.Runtime.xml",
+          "ref/netstandard1.0/es/System.Runtime.xml",
+          "ref/netstandard1.0/fr/System.Runtime.xml",
+          "ref/netstandard1.0/it/System.Runtime.xml",
+          "ref/netstandard1.0/ja/System.Runtime.xml",
+          "ref/netstandard1.0/ko/System.Runtime.xml",
+          "ref/netstandard1.0/ru/System.Runtime.xml",
+          "ref/netstandard1.0/zh-hans/System.Runtime.xml",
+          "ref/netstandard1.0/zh-hant/System.Runtime.xml",
+          "ref/netstandard1.2/System.Runtime.dll",
+          "ref/netstandard1.2/System.Runtime.xml",
+          "ref/netstandard1.2/de/System.Runtime.xml",
+          "ref/netstandard1.2/es/System.Runtime.xml",
+          "ref/netstandard1.2/fr/System.Runtime.xml",
+          "ref/netstandard1.2/it/System.Runtime.xml",
+          "ref/netstandard1.2/ja/System.Runtime.xml",
+          "ref/netstandard1.2/ko/System.Runtime.xml",
+          "ref/netstandard1.2/ru/System.Runtime.xml",
+          "ref/netstandard1.2/zh-hans/System.Runtime.xml",
+          "ref/netstandard1.2/zh-hant/System.Runtime.xml",
+          "ref/netstandard1.3/System.Runtime.dll",
+          "ref/netstandard1.3/System.Runtime.xml",
+          "ref/netstandard1.3/de/System.Runtime.xml",
+          "ref/netstandard1.3/es/System.Runtime.xml",
+          "ref/netstandard1.3/fr/System.Runtime.xml",
+          "ref/netstandard1.3/it/System.Runtime.xml",
+          "ref/netstandard1.3/ja/System.Runtime.xml",
+          "ref/netstandard1.3/ko/System.Runtime.xml",
+          "ref/netstandard1.3/ru/System.Runtime.xml",
+          "ref/netstandard1.3/zh-hans/System.Runtime.xml",
+          "ref/netstandard1.3/zh-hant/System.Runtime.xml",
+          "ref/netstandard1.5/System.Runtime.dll",
+          "ref/netstandard1.5/System.Runtime.xml",
+          "ref/netstandard1.5/de/System.Runtime.xml",
+          "ref/netstandard1.5/es/System.Runtime.xml",
+          "ref/netstandard1.5/fr/System.Runtime.xml",
+          "ref/netstandard1.5/it/System.Runtime.xml",
+          "ref/netstandard1.5/ja/System.Runtime.xml",
+          "ref/netstandard1.5/ko/System.Runtime.xml",
+          "ref/netstandard1.5/ru/System.Runtime.xml",
+          "ref/netstandard1.5/zh-hans/System.Runtime.xml",
+          "ref/netstandard1.5/zh-hant/System.Runtime.xml",
+          "ref/portable-net45+win8+wp80+wpa81/_._",
+          "ref/win8/_._",
+          "ref/wp80/_._",
+          "ref/wpa81/_._",
+          "ref/xamarinios10/_._",
+          "ref/xamarinmac20/_._",
+          "ref/xamarintvos10/_._",
+          "ref/xamarinwatchos10/_._",
+          "system.runtime.4.3.0.nupkg.sha512",
+          "system.runtime.nuspec"
+        ]
+      },
+      "System.Runtime.Loader/4.3.0": {
+        "sha512": "DHMaRn8D8YCK2GG2pw+UzNxn/OHVfaWx7OTLBD/hPegHZZgcZh3H6seWegrC4BYwsfuGrywIuT+MQs+rPqRLTQ==",
+        "type": "package",
+        "path": "system.runtime.loader/4.3.0",
+        "files": [
+          ".nupkg.metadata",
+          ".signature.p7s",
+          "ThirdPartyNotices.txt",
+          "dotnet_library_license.txt",
+          "lib/MonoAndroid10/_._",
+          "lib/MonoTouch10/_._",
+          "lib/net462/_._",
+          "lib/netstandard1.5/System.Runtime.Loader.dll",
+          "lib/xamarinios10/_._",
+          "lib/xamarinmac20/_._",
+          "lib/xamarintvos10/_._",
+          "lib/xamarinwatchos10/_._",
+          "paket-installmodel.cache",
+          "ref/netstandard1.5/System.Runtime.Loader.dll",
+          "ref/netstandard1.5/System.Runtime.Loader.xml",
+          "ref/netstandard1.5/de/System.Runtime.Loader.xml",
+          "ref/netstandard1.5/es/System.Runtime.Loader.xml",
+          "ref/netstandard1.5/fr/System.Runtime.Loader.xml",
+          "ref/netstandard1.5/it/System.Runtime.Loader.xml",
+          "ref/netstandard1.5/ja/System.Runtime.Loader.xml",
+          "ref/netstandard1.5/ko/System.Runtime.Loader.xml",
+          "ref/netstandard1.5/ru/System.Runtime.Loader.xml",
+          "ref/netstandard1.5/zh-hans/System.Runtime.Loader.xml",
+          "ref/netstandard1.5/zh-hant/System.Runtime.Loader.xml",
+          "system.runtime.loader.4.3.0.nupkg.sha512",
+          "system.runtime.loader.nuspec"
+        ]
+      },
+      "System.Text.Encoding/4.3.0": {
+        "sha512": "A/CSPPY+HAH7x7IYKU7KGIRHWwHcDi+Ai9ERC30fpCbUY1SpzFapRPPB5xYep9GWG/TnJD9/prAwvsdyrTHDog==",
+        "type": "package",
+        "path": "system.text.encoding/4.3.0",
+        "files": [
+          ".nupkg.metadata",
+          "ThirdPartyNotices.txt",
+          "dotnet_library_license.txt",
+          "lib/MonoAndroid10/_._",
+          "lib/MonoTouch10/_._",
+          "lib/net45/_._",
+          "lib/portable-net45+win8+wp8+wpa81/_._",
+          "lib/win8/_._",
+          "lib/wp80/_._",
+          "lib/wpa81/_._",
+          "lib/xamarinios10/_._",
+          "lib/xamarinmac20/_._",
+          "lib/xamarintvos10/_._",
+          "lib/xamarinwatchos10/_._",
+          "paket-installmodel.cache",
+          "ref/MonoAndroid10/_._",
+          "ref/MonoTouch10/_._",
+          "ref/net45/_._",
+          "ref/netcore50/System.Text.Encoding.dll",
+          "ref/netcore50/System.Text.Encoding.xml",
+          "ref/netcore50/de/System.Text.Encoding.xml",
+          "ref/netcore50/es/System.Text.Encoding.xml",
+          "ref/netcore50/fr/System.Text.Encoding.xml",
+          "ref/netcore50/it/System.Text.Encoding.xml",
+          "ref/netcore50/ja/System.Text.Encoding.xml",
+          "ref/netcore50/ko/System.Text.Encoding.xml",
+          "ref/netcore50/ru/System.Text.Encoding.xml",
+          "ref/netcore50/zh-hans/System.Text.Encoding.xml",
+          "ref/netcore50/zh-hant/System.Text.Encoding.xml",
+          "ref/netstandard1.0/System.Text.Encoding.dll",
+          "ref/netstandard1.0/System.Text.Encoding.xml",
+          "ref/netstandard1.0/de/System.Text.Encoding.xml",
+          "ref/netstandard1.0/es/System.Text.Encoding.xml",
+          "ref/netstandard1.0/fr/System.Text.Encoding.xml",
+          "ref/netstandard1.0/it/System.Text.Encoding.xml",
+          "ref/netstandard1.0/ja/System.Text.Encoding.xml",
+          "ref/netstandard1.0/ko/System.Text.Encoding.xml",
+          "ref/netstandard1.0/ru/System.Text.Encoding.xml",
+          "ref/netstandard1.0/zh-hans/System.Text.Encoding.xml",
+          "ref/netstandard1.0/zh-hant/System.Text.Encoding.xml",
+          "ref/netstandard1.3/System.Text.Encoding.dll",
+          "ref/netstandard1.3/System.Text.Encoding.xml",
+          "ref/netstandard1.3/de/System.Text.Encoding.xml",
+          "ref/netstandard1.3/es/System.Text.Encoding.xml",
+          "ref/netstandard1.3/fr/System.Text.Encoding.xml",
+          "ref/netstandard1.3/it/System.Text.Encoding.xml",
+          "ref/netstandard1.3/ja/System.Text.Encoding.xml",
+          "ref/netstandard1.3/ko/System.Text.Encoding.xml",
+          "ref/netstandard1.3/ru/System.Text.Encoding.xml",
+          "ref/netstandard1.3/zh-hans/System.Text.Encoding.xml",
+          "ref/netstandard1.3/zh-hant/System.Text.Encoding.xml",
+          "ref/portable-net45+win8+wp8+wpa81/_._",
+          "ref/win8/_._",
+          "ref/wp80/_._",
+          "ref/wpa81/_._",
+          "ref/xamarinios10/_._",
+          "ref/xamarinmac20/_._",
+          "ref/xamarintvos10/_._",
+          "ref/xamarinwatchos10/_._",
+          "system.text.encoding.4.3.0.nupkg.sha512",
+          "system.text.encoding.nuspec"
+        ]
+      },
+      "System.Text.Json/4.6.0": {
+        "sha512": "4F8Xe+JIkVoDJ8hDAZ7HqLkjctN/6WItJIzQaifBwClC7wmoLSda/Sv2i6i1kycqDb3hWF4JCVbpAweyOKHEUA==",
+        "type": "package",
+        "path": "system.text.json/4.6.0",
+        "files": [
+          ".nupkg.metadata",
+          ".signature.p7s",
+          "LICENSE.TXT",
+          "THIRD-PARTY-NOTICES.TXT",
+          "lib/net461/System.Text.Json.dll",
+          "lib/net461/System.Text.Json.xml",
+          "lib/netcoreapp3.0/System.Text.Json.dll",
+          "lib/netcoreapp3.0/System.Text.Json.xml",
+          "lib/netstandard2.0/System.Text.Json.dll",
+          "lib/netstandard2.0/System.Text.Json.xml",
+          "system.text.json.4.6.0.nupkg.sha512",
+          "system.text.json.nuspec",
+          "useSharedDesignerContext.txt",
+          "version.txt"
+        ]
+      },
+      "System.Threading.Tasks/4.3.0": {
+        "sha512": "hMoUsp8EfrVWub6+ZRT9EXmi3C8E/ZX4dpayEXKlygNneCnRZTNiWACsICU5Y5MY84W3NLNEu2nhop2nX/fT0A==",
+        "type": "package",
+        "path": "system.threading.tasks/4.3.0",
+        "files": [
+          ".nupkg.metadata",
+          "ThirdPartyNotices.txt",
+          "dotnet_library_license.txt",
+          "lib/MonoAndroid10/_._",
+          "lib/MonoTouch10/_._",
+          "lib/net45/_._",
+          "lib/portable-net45+win8+wp8+wpa81/_._",
+          "lib/win8/_._",
+          "lib/wp80/_._",
+          "lib/wpa81/_._",
+          "lib/xamarinios10/_._",
+          "lib/xamarinmac20/_._",
+          "lib/xamarintvos10/_._",
+          "lib/xamarinwatchos10/_._",
+          "paket-installmodel.cache",
+          "ref/MonoAndroid10/_._",
+          "ref/MonoTouch10/_._",
+          "ref/net45/_._",
+          "ref/netcore50/System.Threading.Tasks.dll",
+          "ref/netcore50/System.Threading.Tasks.xml",
+          "ref/netcore50/de/System.Threading.Tasks.xml",
+          "ref/netcore50/es/System.Threading.Tasks.xml",
+          "ref/netcore50/fr/System.Threading.Tasks.xml",
+          "ref/netcore50/it/System.Threading.Tasks.xml",
+          "ref/netcore50/ja/System.Threading.Tasks.xml",
+          "ref/netcore50/ko/System.Threading.Tasks.xml",
+          "ref/netcore50/ru/System.Threading.Tasks.xml",
+          "ref/netcore50/zh-hans/System.Threading.Tasks.xml",
+          "ref/netcore50/zh-hant/System.Threading.Tasks.xml",
+          "ref/netstandard1.0/System.Threading.Tasks.dll",
+          "ref/netstandard1.0/System.Threading.Tasks.xml",
+          "ref/netstandard1.0/de/System.Threading.Tasks.xml",
+          "ref/netstandard1.0/es/System.Threading.Tasks.xml",
+          "ref/netstandard1.0/fr/System.Threading.Tasks.xml",
+          "ref/netstandard1.0/it/System.Threading.Tasks.xml",
+          "ref/netstandard1.0/ja/System.Threading.Tasks.xml",
+          "ref/netstandard1.0/ko/System.Threading.Tasks.xml",
+          "ref/netstandard1.0/ru/System.Threading.Tasks.xml",
+          "ref/netstandard1.0/zh-hans/System.Threading.Tasks.xml",
+          "ref/netstandard1.0/zh-hant/System.Threading.Tasks.xml",
+          "ref/netstandard1.3/System.Threading.Tasks.dll",
+          "ref/netstandard1.3/System.Threading.Tasks.xml",
+          "ref/netstandard1.3/de/System.Threading.Tasks.xml",
+          "ref/netstandard1.3/es/System.Threading.Tasks.xml",
+          "ref/netstandard1.3/fr/System.Threading.Tasks.xml",
+          "ref/netstandard1.3/it/System.Threading.Tasks.xml",
+          "ref/netstandard1.3/ja/System.Threading.Tasks.xml",
+          "ref/netstandard1.3/ko/System.Threading.Tasks.xml",
+          "ref/netstandard1.3/ru/System.Threading.Tasks.xml",
+          "ref/netstandard1.3/zh-hans/System.Threading.Tasks.xml",
+          "ref/netstandard1.3/zh-hant/System.Threading.Tasks.xml",
+          "ref/portable-net45+win8+wp8+wpa81/_._",
+          "ref/win8/_._",
+          "ref/wp80/_._",
+          "ref/wpa81/_._",
+          "ref/xamarinios10/_._",
+          "ref/xamarinmac20/_._",
+          "ref/xamarintvos10/_._",
+          "ref/xamarinwatchos10/_._",
+          "system.threading.tasks.4.3.0.nupkg.sha512",
+          "system.threading.tasks.nuspec"
+        ]
+      },
+      "ExtCore.Infrastructure/5.1.0": {
+        "type": "project",
+        "path": "../ExtCore.Infrastructure/ExtCore.Infrastructure.csproj",
+        "msbuildProject": "../ExtCore.Infrastructure/ExtCore.Infrastructure.csproj"
+      }
+    },
+    "projectFileDependencyGroups": {
+      ".NETCoreApp,Version=v3.1": [
+        "ExtCore.Infrastructure >= 5.1.0",
+        "Microsoft.Extensions.DependencyModel >= 3.0.0",
+        "System.Runtime.Loader >= 4.3.0"
+      ]
+    },
+    "packageFolders": {
+      "C:\\Users\\brphelps\\.nuget\\packages\\": {},
+      "C:\\Program Files\\dotnet\\sdk\\NuGetFallbackFolder": {}
+    },
+    "project": {
+      "version": "5.1.0",
+      "restore": {
+        "projectUniqueName": "D:\\Source\\ExtCore\\src\\ExtCore.WebApplication\\ExtCore.WebApplication.csproj",
+        "projectName": "ExtCore.WebApplication",
+        "projectPath": "D:\\Source\\ExtCore\\src\\ExtCore.WebApplication\\ExtCore.WebApplication.csproj",
+        "packagesPath": "C:\\Users\\brphelps\\.nuget\\packages\\",
+        "outputPath": "D:\\Source\\ExtCore\\src\\ExtCore.WebApplication\\obj\\",
+        "projectStyle": "PackageReference",
+        "fallbackFolders": [
+          "C:\\Program Files\\dotnet\\sdk\\NuGetFallbackFolder"
+        ],
+        "configFilePaths": [
+          "C:\\Users\\brphelps\\AppData\\Roaming\\NuGet\\NuGet.Config",
+          "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
+        ],
+        "originalTargetFrameworks": [
+          "netcoreapp3.1"
+        ],
+        "sources": {
+          "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
+          "C:\\temp\\AI": {},
+          "https://api.nuget.org/v3/index.json": {}
+        },
+        "frameworks": {
+          "netcoreapp3.1": {
+            "projectReferences": {
+              "D:\\Source\\ExtCore\\src\\ExtCore.Infrastructure\\ExtCore.Infrastructure.csproj": {
+                "projectPath": "D:\\Source\\ExtCore\\src\\ExtCore.Infrastructure\\ExtCore.Infrastructure.csproj"
+              }
+            }
+          }
+        },
+        "warningProperties": {
+          "warnAsError": [
+            "NU1605"
+          ]
+        }
+      },
+      "frameworks": {
+        "netcoreapp3.1": {
+          "dependencies": {
+            "Microsoft.Extensions.DependencyModel": {
+              "target": "Package",
+              "version": "[3.0.0, )"
+            },
+            "System.Runtime.Loader": {
+              "target": "Package",
+              "version": "[4.3.0, )"
+            }
+          },
+          "imports": [
+            "net461",
+            "net462",
+            "net47",
+            "net471",
+            "net472",
+            "net48"
+          ],
+          "assetTargetFallback": true,
+          "warn": true,
+          "frameworkReferences": {
+            "Microsoft.NETCore.App": {
+              "privateAssets": "all"
+            }
+          },
+          "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\3.1.301\\RuntimeIdentifierGraph.json"
+        }
+      }
+    }
+  }
\ No newline at end of file
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/RubyDetectorTest.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/RubyDetectorTest.cs
new file mode 100644
index 000000000..2592d620c
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/RubyDetectorTest.cs
@@ -0,0 +1,436 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Common.DependencyGraph;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Detectors.Ruby;
+using Microsoft.ComponentDetection.Detectors.Tests.Utilities;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Microsoft.ComponentDetection.TestsUtilities;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class RubyDetectorTest
+    {
+        private Mock loggerMock;
+        private RubyComponentDetector rubyDetector;
+        private DetectorTestUtility detectorTestUtility;
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            loggerMock = new Mock();
+            rubyDetector = new RubyComponentDetector
+            {
+                Logger = loggerMock.Object,
+            };
+            detectorTestUtility = DetectorTestUtilityCreator.Create()
+                                    .WithScanRequest(new ScanRequest(
+                                        new DirectoryInfo(Path.GetTempPath()), null, null, new Dictionary(), null,
+                                        new ComponentRecorder(enableManualTrackingOfExplicitReferences: !rubyDetector.NeedsAutomaticRootDependencyCalculation)));
+        }
+
+        [TestMethod]
+        public async Task TestRubyDetector_TestMultipleLockfiles()
+        {
+            var gemFileLockContent = @"GEM
+  remote: https://rubygems.org/
+  specs:
+    acme-client (2.0.0)
+      faraday (~> 0.9, >= 0.9.1)
+      actioncable (= 5.2.2.1)
+    actioncable (5.2.1)
+      nio4r (~> 2.0)
+      websocket-driver (>= 0.6.1)
+    faraday (1.0.0)
+    nio4r (5.2.1)
+    websocket-driver (0.6.1)
+
+BUNDLED WITH
+    1.17.2";
+
+            var gemFileLockContent2 = @"GEM
+  remote: https://rubygems.org/
+  specs:
+    acme-client (2.0.0)
+      faraday (~> 0.9, >= 0.9.1)
+      actioncable (= 5.2.2.1)
+    actioncable (5.2.1)
+      nio4r (~> 2.0)
+      websocket-driver (>= 0.6.1)
+    faraday (1.0.0)
+    nio4r (5.2.1)
+    websocket-driver (0.6.1)
+
+BUNDLED WITH
+    1.17.3";
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("1Gemfile.lock", gemFileLockContent)
+                                                    .WithFile("2Gemfile.lock", gemFileLockContent2)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(7, detectedComponents.Count());
+
+            AssertRubyComponentNameAndVersion(detectedComponents, "acme-client", "2.0.0");
+            AssertRubyComponentNameAndVersion(detectedComponents, "actioncable", "5.2.1");
+            AssertRubyComponentNameAndVersion(detectedComponents, "faraday", "1.0.0");
+            AssertRubyComponentNameAndVersion(detectedComponents, "nio4r", "5.2.1");
+            AssertRubyComponentNameAndVersion(detectedComponents, "websocket-driver", "0.6.1");
+            AssertRubyComponentNameAndVersion(detectedComponents, "bundler", "1.17.2");
+            AssertRubyComponentNameAndVersion(detectedComponents, "bundler", "1.17.3");
+        }
+
+        [TestMethod]
+        public async Task TestRubyDetector_TestGemsWithUppercase_LockFile()
+        {
+            var gemFileLockContent = @"GEM
+  remote: https://rubygems.org/
+  specs:
+    CFPropertyList (3.0.4)
+      rexml 
+
+BUNDLED WITH
+    2.2.28";
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("1Gemfile.lock", gemFileLockContent)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(2, detectedComponents.Count());
+
+            // we do not record invalid/unknown versions
+            AssertRubyComponentNameAndVersion(detectedComponents, "CFPropertyList", "3.0.4");
+            AssertRubyComponentNameAndVersion(detectedComponents, "bundler", "2.2.28");
+        }
+
+        [TestMethod]
+        public async Task TestRubyDetector_DetectorParseWithBundlerVersion()
+        {
+            var gemFileLockContent = @"GEM
+  remote: https://rubygems.org/
+  specs:
+    acme-client (2.0.0)
+      faraday (~> 0.9, >= 0.9.1)
+      actioncable (= 5.2.2.1)
+    actioncable (5.2.1)
+      nio4r (~> 2.0)
+      websocket-driver (>= 0.6.1)
+    faraday (1.0.0)
+    nio4r (5.2.1)
+    websocket-driver (0.6.1)
+
+BUNDLED WITH
+    1.17.3";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("1Gemfile.lock", gemFileLockContent)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(6, detectedComponents.Count());
+
+            AssertRubyComponentNameAndVersion(detectedComponents, "acme-client", "2.0.0");
+            AssertRubyComponentNameAndVersion(detectedComponents, "actioncable", "5.2.1");
+            AssertRubyComponentNameAndVersion(detectedComponents, "faraday", "1.0.0");
+            AssertRubyComponentNameAndVersion(detectedComponents, "nio4r", "5.2.1");
+            AssertRubyComponentNameAndVersion(detectedComponents, "websocket-driver", "0.6.1");
+            AssertRubyComponentNameAndVersion(detectedComponents, "bundler", "1.17.3");
+        }
+
+        [TestMethod]
+        public async Task TestRubyDetector_DetectorRecognizeGemComponents()
+        {
+            var gemFileLockContent = @"GEM
+  remote: https://rubygems.org/
+  specs:
+    acme-client (2.0.0)
+      faraday (~> 0.9, >= 0.9.1)
+      actioncable (= 5.2.2.1)
+    actioncable (5.2.1)
+      nio4r (~> 2.0)
+      websocket-driver (>= 0.6.1)
+    faraday (1.0.0)
+    nio4r (5.2.1)
+    nokogiri (~> 1.8.2)
+    websocket-driver (0.6.1)";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("1Gemfile.lock", gemFileLockContent)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(5, detectedComponents.Count());
+
+            AssertRubyComponentNameAndVersion(detectedComponents, "acme-client", "2.0.0");
+            AssertRubyComponentNameAndVersion(detectedComponents, "actioncable", "5.2.1");
+            AssertRubyComponentNameAndVersion(detectedComponents, "faraday", "1.0.0");
+            AssertRubyComponentNameAndVersion(detectedComponents, "nio4r", "5.2.1");
+            AssertRubyComponentNameAndVersion(detectedComponents, "websocket-driver", "0.6.1");
+        }
+
+        [TestMethod]
+        public async Task TestRubyDetector_ParentWithTildeInVersion_IsExcluded()
+        {
+            var gemFileLockContent = @"GEM
+  remote: https://rubygems.org/
+  specs:
+    acme-client (2.0.0)
+      faraday (~> 0.9, >= 0.9.1)
+      nokogiri (~> 1.8.2)
+    faraday (1.0.0)
+    nokogiri (~> 1.8.2)
+      mini_portile2 (~> 2.3.0)
+    mini_portile2 (2.3.0)";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("1Gemfile.lock", gemFileLockContent)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(3, detectedComponents.Count());
+
+            AssertRubyComponentNameAndVersion(detectedComponents, "acme-client", "2.0.0");
+            AssertRubyComponentNameAndVersion(detectedComponents, "faraday", "1.0.0");
+            AssertRubyComponentNameAndVersion(detectedComponents, "mini_portile2", "2.3.0");
+        }
+
+        [TestMethod]
+        public async Task TestRubyDetector_DetectorCreatesADependencyGraph()
+        {
+            var gemFileLockContent = @"GIT
+  remote: https://github.com/mikel/mail.git
+  revision: 3204c0b4733166b9664a552006286227dea09953
+  branch: 2-7-stable
+  specs:
+    mail (2.7.2.edge)
+      websocket-driver (>= 0.6.1)
+
+GEM
+  remote: https://rubygems.org/
+  specs:
+    acme-client (2.0.0)
+      faraday (~> 0.9, >= 0.9.1)
+      actioncable (= 5.2.2.1)
+    actioncable (5.2.1)
+      nio4r (~> 2.0)
+    faraday (1.0.0)
+    nio4r (5.2.1)
+    websocket-driver (0.6.1)";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("1Gemfile.lock", gemFileLockContent)
+                                                    .ExecuteDetector();
+
+            var dependencyGraph = componentRecorder.GetDependencyGraphsByLocation().Values.Single();
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            var acmeClientComponentId = detectedComponents.Single(c => c.Component is RubyGemsComponent component && component.Name.Equals("acme-client")).Component.Id;
+            var faradayComponentId = detectedComponents.Single(c => c.Component is RubyGemsComponent component && component.Name.Equals("faraday")).Component.Id;
+            var actioncableComponentId = detectedComponents.Single(c => c.Component is RubyGemsComponent component && component.Name.Equals("actioncable")).Component.Id;
+            var nior4rComponentId = detectedComponents.Single(c => c.Component is RubyGemsComponent component && component.Name.Equals("nio4r")).Component.Id;
+            var websocketDriverComponentId = detectedComponents.Single(c => c.Component is RubyGemsComponent component && component.Name.Equals("websocket-driver")).Component.Id;
+            var mailComponentId = detectedComponents.Single(c => c.Component is GitComponent component && component.CommitHash.Equals("3204c0b4733166b9664a552006286227dea09953")).Component.Id;
+
+            var acmeClientDependencies = dependencyGraph.GetDependenciesForComponent(acmeClientComponentId);
+            acmeClientDependencies.Should().HaveCount(2);
+            acmeClientDependencies.Should().Contain(dep => dep == faradayComponentId);
+            acmeClientDependencies.Should().Contain(dep => dep == actioncableComponentId);
+
+            var actionCableDependencies = dependencyGraph.GetDependenciesForComponent(actioncableComponentId);
+            actionCableDependencies.Should().HaveCount(1);
+            actionCableDependencies.Should().Contain(dep => dep == nior4rComponentId);
+
+            var faradayDependencies = dependencyGraph.GetDependenciesForComponent(faradayComponentId);
+            faradayDependencies.Should().HaveCount(0);
+
+            var niorDependencies = dependencyGraph.GetDependenciesForComponent(nior4rComponentId);
+            niorDependencies.Should().HaveCount(0);
+
+            var websocketDependencies = dependencyGraph.GetDependenciesForComponent(websocketDriverComponentId);
+            websocketDependencies.Should().HaveCount(0);
+
+            var mailComponentDependencies = dependencyGraph.GetDependenciesForComponent(mailComponentId);
+            mailComponentDependencies.Should().HaveCount(1);
+            mailComponentDependencies.Should().Contain(dep => dep == websocketDriverComponentId);
+        }
+
+        [TestMethod]
+        public async Task TestRubyDetector_ComponentsRootsAreFilledCorrectly()
+        {
+            var gemFileLockContent = @"GEM
+  remote: https://rubygems.org/
+  specs:
+    acme-client (2.0.0)
+      faraday (~> 0.9, >= 0.9.1)
+      actioncable (= 5.2.2.1)
+    actioncable (5.2.1)
+      nio4r (~> 2.0)
+    faraday (1.0.0)
+    nio4r (5.2.1)
+    websocket-driver (0.6.1)";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("1Gemfile.lock", gemFileLockContent)
+                                                    .ExecuteDetector();
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            var acmeClientComponent = detectedComponents.Single(c => c.Component is RubyGemsComponent component && component.Name.Equals("acme-client"));
+            var faradayComponent = detectedComponents.Single(c => c.Component is RubyGemsComponent component && component.Name.Equals("faraday"));
+            var actioncableComponent = detectedComponents.Single(c => c.Component is RubyGemsComponent component && component.Name.Equals("actioncable"));
+            var nior4rComponent = detectedComponents.Single(c => c.Component is RubyGemsComponent component && component.Name.Equals("nio4r"));
+            var websocketDriverComponent = detectedComponents.Single(c => c.Component is RubyGemsComponent component && component.Name.Equals("websocket-driver"));
+
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                acmeClientComponent.Component.Id,
+                parentComponent => parentComponent.Id == acmeClientComponent.Component.Id);
+
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                faradayComponent.Component.Id,
+                parentComponent => parentComponent.Id == acmeClientComponent.Component.Id);
+
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                actioncableComponent.Component.Id,
+                parentComponent => parentComponent.Id == acmeClientComponent.Component.Id);
+
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                nior4rComponent.Component.Id,
+                parentComponent => parentComponent.Id == acmeClientComponent.Component.Id);
+
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                websocketDriverComponent.Component.Id,
+                parentComponent => parentComponent.Id == websocketDriverComponent.Component.Id);
+        }
+
+        [TestMethod]
+        public async Task TestRubyDetector_DetectorRecognizeGitComponents()
+        {
+            var gemFileLockContent = @"GIT
+  remote: https://github.com/test/abc.git
+  revision: commit-hash-1
+  branch: 2-7-stable
+  specs:
+    abc (2.7.2.edge)
+
+GIT
+  remote: https://github.com/mikel/mail.git
+  revision: commit-hash-2
+  branch: 2-7-stable
+  specs:
+    mail (2.7.2.edge)
+      mini_mime (>= 0.1.1)
+
+GEM
+  remote: https://rubygems.org/
+  specs:
+    mini_mime (2.0.0)";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("1Gemfile.lock", gemFileLockContent)
+                                                    .ExecuteDetector();
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(3, detectedComponents.Count());
+            AssertGitComponentHashAndUrl(detectedComponents, commitHash: "commit-hash-1", repositoryUrl: "https://github.com/test/abc.git");
+            AssertGitComponentHashAndUrl(detectedComponents, commitHash: "commit-hash-2", repositoryUrl: "https://github.com/mikel/mail.git");
+            AssertRubyComponentNameAndVersion(detectedComponents, name: "mini_mime", version: "2.0.0");
+        }
+
+        [TestMethod]
+        public async Task TestRubyDetector_DetectorRecognizeParentChildRelationshipInGitComponents()
+        {
+            var gemFileLockContent = @"GIT
+  remote: https://github.com/test/abc.git
+  revision: commit-hash-1
+  branch: 2-7-stable
+  specs:
+    abc (2.7.2.edge)
+      mail (2.7.2.edge)
+
+GIT
+  remote: https://github.com/mikel/mail.git
+  revision: commit-hash-2
+  branch: 2-7-stable
+  specs:
+    mail (2.7.2.edge)";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("1Gemfile.lock", gemFileLockContent)
+                                                    .ExecuteDetector();
+
+            AssertGitComponentAsRootAndGitComponentAsSubDependency(componentRecorder, rootHash: "commit-hash-1", subDependencyHash: "commit-hash-2");
+        }
+
+        [TestMethod]
+        public async Task TestRubyDetector_DetectorRecognizeLocalDependencies()
+        {
+            var gemFileLockContent = @"GEM
+  remote: https://rubygems.org/
+  specs:
+    mini_mime (2.0.0)
+
+PATH
+  remote: C:/test
+  specs:
+    test (1.0.0)
+
+PATH
+  remote: C:/test
+  specs:
+    test2 (1.0.0)";
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("1Gemfile.lock", gemFileLockContent)
+                                                    .ExecuteDetector();
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(3, detectedComponents.Count());
+
+            AssertRubyComponentNameAndVersion(detectedComponents, name: "mini_mime", version: "2.0.0");
+            AssertRubyComponentNameAndVersion(detectedComponents, name: "test", version: "1.0.0");
+            AssertRubyComponentNameAndVersion(detectedComponents, name: "test2", version: "1.0.0");
+        }
+
+        private void AssertRubyComponentNameAndVersion(IEnumerable detectedComponents, string name, string version)
+        {
+            Assert.IsNotNull(
+                detectedComponents.SingleOrDefault(c =>
+            c.Component is RubyGemsComponent component &&
+            component.Name.Equals(name) &&
+            component.Version.Equals(version)), $"Component with name {name} and version {version} was not found");
+        }
+
+        private void AssertGitComponentHashAndUrl(IEnumerable detectedComponents, string commitHash, string repositoryUrl)
+        {
+            Assert.IsNotNull(detectedComponents.SingleOrDefault(c =>
+            c.Component is GitComponent component &&
+            component.CommitHash.Equals(commitHash) &&
+            component.RepositoryUrl.Equals(repositoryUrl)));
+        }
+
+        private void AssertGitComponentAsRootAndGitComponentAsSubDependency(IComponentRecorder recorder, string rootHash, string subDependencyHash)
+        {
+            var childDep = recorder.GetDetectedComponents().First(x => (x.Component as GitComponent)?.CommitHash == subDependencyHash);
+            Assert.IsTrue(recorder.IsDependencyOfExplicitlyReferencedComponents(
+                childDep.Component.Id,
+                parent => parent.CommitHash == rootHash));
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/RustCrateDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/RustCrateDetectorTests.cs
new file mode 100644
index 000000000..3e7505063
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/RustCrateDetectorTests.cs
@@ -0,0 +1,997 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Common.DependencyGraph;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Detectors.Rust;
+using Microsoft.ComponentDetection.Detectors.Tests.Utilities;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Microsoft.ComponentDetection.TestsUtilities;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class RustCrateDetectorTests
+    {
+        private DetectorTestUtility detectorTestUtility;
+        private DetectorTestUtility detectorV2TestUtility;
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            detectorTestUtility = DetectorTestUtilityCreator.Create();
+            detectorV2TestUtility = DetectorTestUtilityCreator.Create();
+        }
+
+        [TestMethod]
+        public async Task TestGraphIsCorrect()
+        {
+            var (result, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("Cargo.lock", testCargoLockString)
+                                                    .WithFile("Cargo.toml", testCargoTomlString, new List { "Cargo.toml" })
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, result.ResultCode);
+            Assert.AreEqual(6, componentRecorder.GetDetectedComponents().Count());
+
+            var graph = componentRecorder.GetDependencyGraphsByLocation().Values.First(); // There should only be 1
+
+            // Verify explicitly referenced roots
+            var rootComponents = new List
+            {
+                "my_dependency 1.0.0 - Cargo",
+
+                // Note: my_other_dependency isn't here because we don't capture local deps
+                "other_dependency 0.4.0 - Cargo",
+            };
+
+            rootComponents.ForEach(rootComponentId => graph.IsComponentExplicitlyReferenced(rootComponentId).Should().BeTrue());
+
+            // Verify explicitly referenced dev roots
+            var rootDevComponents = new List { "my_dev_dependency 1.0.0 - Cargo" };
+
+            rootDevComponents.ForEach(rootDevComponentId => graph.IsComponentExplicitlyReferenced(rootDevComponentId).Should().BeTrue());
+
+            // Verify dependencies for my_dependency
+            graph.GetDependenciesForComponent("my_dependency 1.0.0 - Cargo").Should().BeEmpty();
+
+            // Verify dependencies for other_dependency
+            var other_dependencyDependencies = new List { "other_dependency_dependency 0.1.12-alpha.6 - Cargo" };
+
+            graph.GetDependenciesForComponent("other_dependency 0.4.0 - Cargo").Should().BeEquivalentTo(other_dependencyDependencies);
+
+            // Verify dependencies for my_dev_dependency
+            var my_dev_dependencyDependencies = new List { "other_dependency_dependency 0.1.12-alpha.6 - Cargo", "dev_dependency_dependency 0.2.23 - Cargo" };
+
+            graph.GetDependenciesForComponent("my_dev_dependency 1.0.0 - Cargo").Should().BeEquivalentTo(my_dev_dependencyDependencies);
+        }
+
+        [TestMethod]
+        public async Task TestRequirePairForComponents()
+        {
+            var cargoDefinitionPairsMatrix = new List<(string, string)>
+            {
+                (null, testCargoTomlString),
+                (testCargoLockString, null),
+                (null, null),
+            };
+
+            foreach (var cargoDefinitionPairs in cargoDefinitionPairsMatrix)
+            {
+                if (cargoDefinitionPairs.Item1 != null)
+                {
+                    detectorTestUtility.WithFile("Cargo.lock", cargoDefinitionPairs.Item1);
+                }
+
+                if (cargoDefinitionPairs.Item2 != null)
+                {
+                    detectorTestUtility.WithFile("Cargo.toml", cargoDefinitionPairs.Item2, new List { "Cargo.toml" });
+                }
+
+                var (result, componentRecorder) = await detectorTestUtility.ExecuteDetector();
+
+                Assert.AreEqual(ProcessingResultCode.Success, result.ResultCode);
+
+                componentRecorder.GetDetectedComponents().Count().Should().Be(0);
+            }
+        }
+
+        [TestMethod]
+        public async Task TestSupportsCargoV1AndV2DefinitionPairs()
+        {
+            var componentRecorder = new ComponentRecorder();
+            ScanRequest request = new ScanRequest(new DirectoryInfo(Path.GetTempPath()), null, null, new Dictionary(), null, componentRecorder);
+
+            var (result1, _) = await detectorTestUtility
+                                                    /* v1 files */
+                                                    .WithFile("Cargo.lock", testCargoLockString)
+                                                    .WithFile("Cargo.toml", testCargoTomlString, new List { "Cargo.toml" })
+                                                    /* v2 files */
+                                                    .WithFile("Cargo.lock", testCargoLockV2String, fileLocation: Path.Join(Path.GetTempPath(), "v2", "Cargo.lock"))
+                                                    .WithFile("Cargo.toml", testCargoTomlString, new List { "Cargo.toml" }, fileLocation: Path.Join(Path.GetTempPath(), "v2", "Cargo.toml"))
+                                                    /* so we can reuse the component recorder */
+                                                    .WithScanRequest(request)
+                                                    .ExecuteDetector();
+
+            var (result2, _) = await detectorV2TestUtility
+                                                    /* v1 files */
+                                                    .WithFile("Cargo.lock", testCargoLockString)
+                                                    .WithFile("Cargo.toml", testCargoTomlString, new List { "Cargo.toml" })
+                                                    /* v2 files */
+                                                    .WithFile("Cargo.lock", testCargoLockV2String, fileLocation: Path.Join(Path.GetTempPath(), "v2", "Cargo.lock"))
+                                                    .WithFile("Cargo.toml", testCargoTomlString, new List { "Cargo.toml" }, fileLocation: Path.Join(Path.GetTempPath(), "v2", "Cargo.toml"))
+                                                    /* so we can reuse the component recorder */
+                                                    .WithScanRequest(request)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, result1.ResultCode);
+            Assert.AreEqual(ProcessingResultCode.Success, result2.ResultCode);
+
+            var componentGraphs = componentRecorder.GetDependencyGraphsByLocation();
+
+            componentGraphs.Count.Should().Be(2); // 1 for each detector
+        }
+
+        [TestMethod]
+        public async Task TestSupportsMultipleCargoV1DefinitionPairs()
+        {
+            var (result, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("Cargo.lock", testCargoLockString)
+                                                    .WithFile("Cargo.toml", testCargoTomlString, new List { "Cargo.toml" })
+                                                    .WithFile("Cargo.lock", testCargoLockString, fileLocation: Path.Join(Path.GetTempPath(), "sub-path", "Cargo.lock"))
+                                                    .WithFile("Cargo.toml", testCargoTomlString, new List { "Cargo.toml" }, fileLocation: Path.Join(Path.GetTempPath(), "sub-path", "Cargo.toml"))
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, result.ResultCode);
+
+            var componentGraphs = componentRecorder.GetDependencyGraphsByLocation();
+
+            componentGraphs.Count.Should().Be(2); // 1 graph for each Cargo.lock
+
+            var graph1 = componentGraphs.Values.First();
+            var graph2 = componentGraphs.Values.Skip(1).First();
+
+            graph1.GetComponents().Should().BeEquivalentTo(graph2.GetComponents()); // The graphs should have detected the same components
+
+            // 4 file locations are expected. 2 for each Cargo.lock and Cargo.toml pair
+            componentRecorder.ForAllComponents(x => Enumerable.Count(x.AllFileLocations).Should().Be(4));
+        }
+
+        [TestMethod]
+        public async Task TestSupportsMultipleCargoV2DefinitionPairs()
+        {
+            var (result, componentRecorder) = await detectorV2TestUtility
+                                                    .WithFile("Cargo.lock", testCargoLockV2String)
+                                                    .WithFile("Cargo.toml", testCargoTomlString, new List { "Cargo.toml" })
+                                                    .WithFile("Cargo.lock", testCargoLockV2String, fileLocation: Path.Join(Path.GetTempPath(), "sub-path", "Cargo.lock"))
+                                                    .WithFile("Cargo.toml", testCargoTomlString, new List { "Cargo.toml" }, fileLocation: Path.Join(Path.GetTempPath(), "sub-path", "Cargo.toml"))
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, result.ResultCode);
+
+            var componentGraphs = componentRecorder.GetDependencyGraphsByLocation();
+
+            componentGraphs.Count.Should().Be(2); // 1 graph for each Cargo.lock
+
+            var graph1 = componentGraphs.Values.First();
+            var graph2 = componentGraphs.Values.Skip(1).First();
+
+            graph1.GetComponents().Should().BeEquivalentTo(graph2.GetComponents()); // The graphs should have detected the same components
+
+            // 4 file locations are expected. 2 for each Cargo.lock and Cargo.toml pair
+            componentRecorder.ForAllComponents(x => x.AllFileLocations.Count().Should().Be(4));
+        }
+
+        [TestMethod]
+        public async Task TestRustDetector()
+        {
+            var (result, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("Cargo.lock", testCargoLockString)
+                                                    .WithFile("Cargo.toml", testCargoTomlString, new List { "Cargo.toml" })
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, result.ResultCode);
+            Assert.AreEqual(6, componentRecorder.GetDetectedComponents().Count());
+
+            IDictionary packageVersions = new Dictionary()
+            {
+                { "my_dependency", "1.0.0" },
+                { "other_dependency", "0.4.0" },
+                { "other_dependency_dependency", "0.1.12-alpha.6" },
+                { "my_dev_dependency", "1.0.0" },
+                { "dev_dependency_dependency", "0.2.23" },
+                { "one_more_dev_dep", "1.0.0" },
+            };
+
+            IDictionary packageIsDevDependency = new Dictionary()
+            {
+                { "my_dependency 1.0.0 - Cargo", false },
+                { "other_dependency 0.4.0 - Cargo", false },
+                { "other_dependency_dependency 0.1.12-alpha.6 - Cargo", false },
+                { "my_dev_dependency 1.0.0 - Cargo", true },
+                { "dev_dependency_dependency 0.2.23 - Cargo", true },
+                { "one_more_dev_dep 1.0.0 - Cargo", true },
+            };
+
+            IDictionary> packageDependencyRoots = new Dictionary>()
+            {
+                { "my_dependency", new HashSet() { "my_dependency" } },
+                { "other_dependency", new HashSet() { "other_dependency" } },
+                { "other_dependency_dependency", new HashSet() { "other_dependency", "my_dev_dependency" } },
+                { "my_dev_dependency", new HashSet() { "my_dev_dependency" } },
+                { "dev_dependency_dependency", new HashSet() { "my_dev_dependency" } },
+                { "one_more_dev_dep", new HashSet() { "my_dev_dependency" } },
+            };
+
+            ISet componentNames = new HashSet();
+            foreach (var discoveredComponent in componentRecorder.GetDetectedComponents())
+            {
+                // Verify each package has the right information
+                var packageName = (discoveredComponent.Component as CargoComponent).Name;
+
+                // Verify version
+                Assert.AreEqual(packageVersions[packageName], (discoveredComponent.Component as CargoComponent).Version);
+
+                // Verify dev dependency flag
+                componentRecorder.GetEffectiveDevDependencyValue(discoveredComponent.Component.Id).Should().Be(packageIsDevDependency[discoveredComponent.Component.Id]);
+
+                var dependencyRoots = new HashSet();
+
+                componentRecorder.AssertAllExplicitlyReferencedComponents(
+                    discoveredComponent.Component.Id,
+                    packageDependencyRoots[packageName].Select(expectedRoot =>
+                        new Func(parentComponent => parentComponent.Name == expectedRoot)).ToArray());
+
+                componentNames.Add(packageName);
+            }
+
+            // Verify all packages were detected
+            foreach (var expectedPackage in packageVersions.Keys)
+            {
+                Assert.IsTrue(componentNames.Contains(expectedPackage));
+            }
+        }
+
+        [TestMethod]
+        public async Task TestRustV2Detector()
+        {
+            var (result, componentRecorder) = await detectorV2TestUtility
+                                                    .WithFile("Cargo.lock", testCargoLockV2String)
+                                                    .WithFile("Cargo.toml", testCargoTomlString, new List { "Cargo.toml" })
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, result.ResultCode);
+            Assert.AreEqual(7, componentRecorder.GetDetectedComponents().Count());
+
+            List packageVersions = new List()
+            {
+                "my_dependency 1.0.0",
+                "other_dependency 0.4.0",
+                "other_dependency_dependency 0.1.12-alpha.6",
+                "my_dev_dependency 1.0.0",
+                "dev_dependency_dependency 0.2.23",
+                "same_package 1.0.0",
+                "same_package 2.0.0",
+            };
+
+            IDictionary packageIsDevDependency = new Dictionary()
+            {
+                { "my_dependency 1.0.0 - Cargo", false },
+                { "other_dependency 0.4.0 - Cargo", false },
+                { "other_dependency_dependency 0.1.12-alpha.6 - Cargo", false },
+                { "my_dev_dependency 1.0.0 - Cargo", true },
+                { "dev_dependency_dependency 0.2.23 - Cargo", true },
+                { "same_package 1.0.0 - Cargo", false },
+                { "same_package 2.0.0 - Cargo", true },
+            };
+
+            IDictionary> packageDependencyRoots = new Dictionary>()
+            {
+                { "my_dependency 1.0.0", new HashSet() { "my_dependency 1.0.0" } },
+                { "other_dependency 0.4.0", new HashSet() { "other_dependency 0.4.0" } },
+                { "other_dependency_dependency 0.1.12-alpha.6", new HashSet() { "other_dependency 0.4.0", "my_dev_dependency 1.0.0" } },
+                { "my_dev_dependency 1.0.0", new HashSet() { "my_dev_dependency 1.0.0" } },
+                { "dev_dependency_dependency 0.2.23", new HashSet() { "my_dev_dependency 1.0.0" } },
+                { "same_package 2.0.0", new HashSet() { "my_dev_dependency 1.0.0" } },
+                { "same_package 1.0.0",  new HashSet() { "my_dependency 1.0.0" } },
+            };
+
+            ISet componentNames = new HashSet();
+            foreach (var discoveredComponent in componentRecorder.GetDetectedComponents())
+            {
+                var component = discoveredComponent.Component as CargoComponent;
+                var componentKey = $"{component.Name} {component.Version}";
+
+                // Verify version
+                Assert.IsTrue(packageVersions.Contains(componentKey));
+
+                // Verify dev dependency flag
+                componentRecorder.GetEffectiveDevDependencyValue(discoveredComponent.Component.Id).Should().Be(packageIsDevDependency[discoveredComponent.Component.Id]);
+
+                componentRecorder.AssertAllExplicitlyReferencedComponents(
+                    discoveredComponent.Component.Id,
+                    packageDependencyRoots[componentKey].Select(expectedRoot =>
+                        new Func(parentComponent => $"{parentComponent.Name} {parentComponent.Version}" == expectedRoot)).ToArray());
+
+                componentNames.Add(componentKey);
+            }
+
+            // Verify all packages were detected
+            foreach (var expectedPackage in packageVersions)
+            {
+                Assert.IsTrue(componentNames.Contains(expectedPackage));
+            }
+        }
+
+        [TestMethod]
+        public async Task TestRustV2Detector_DoesNotRunV1Format()
+        {
+            var (result, componentRecorder) = await detectorV2TestUtility
+                                                    .WithFile("Cargo.lock", testCargoLockString)
+                                                    .WithFile("Cargo.toml", testCargoTomlString, new List { "Cargo.toml" })
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, result.ResultCode);
+            Assert.AreEqual(0, componentRecorder.GetDetectedComponents().Count());
+        }
+
+        [TestMethod]
+        public async Task TestRustV1Detector_DoesNotRunV2Format()
+        {
+            var (result, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("Cargo.lock", testCargoLockV2String)
+                                                    .WithFile("Cargo.toml", testCargoTomlString, new List { "Cargo.toml" })
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, result.ResultCode);
+            Assert.AreEqual(0, componentRecorder.GetDetectedComponents().Count());
+        }
+
+        [TestMethod]
+        public async Task TestRustV2Detector_DuplicatePackage()
+        {
+            string testCargoLock = @"
+[[package]]
+name = ""my_dependency""
+version = ""1.0.0""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+dependencies = [
+  ""same_package 1.0.0""
+]
+
+[[package]]
+name = ""other_dependency""
+version = ""0.4.0""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+dependencies = [
+ ""other_dependency_dependency 0.1.12-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)"",
+]
+
+[[package]]
+name = ""other_dependency_dependency""
+version = ""0.1.12-alpha.6""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+
+[[package]]
+name = ""other_dependency_dependency""
+version = ""0.1.12-alpha.6""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+
+[[package]]
+name = ""my_dev_dependency""
+version = ""1.0.0""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+dependencies = [
+ ""other_dependency_dependency 0.1.12-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)"",
+ ""dev_dependency_dependency 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)"",
+]
+
+[[package]]
+name = ""dev_dependency_dependency""
+version = ""0.2.23""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+dependencies = [
+  ""same_package 2.0.0""
+]
+
+[[package]]
+name = ""my_other_package""
+version = ""1.0.0""
+
+[[package]]
+name = ""my_test_package""
+version = ""1.2.3""
+dependencies = [
+ ""my_dependency 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)"",
+ ""my_other_package 1.0.0"",
+ ""other_dependency 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)"",
+ ""my_dev_dependency 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)"",
+]
+
+[[package]]
+name = ""same_package""
+version = ""1.0.0""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+
+[[package]]
+name = ""same_package""
+version = ""2.0.0""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+";
+
+            var (result, componentRecorder) = await detectorV2TestUtility
+                                                    .WithFile("Cargo.lock", testCargoLock)
+                                                    .WithFile("Cargo.toml", testCargoTomlString, new List { "Cargo.toml" })
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, result.ResultCode);
+            Assert.AreEqual(7, componentRecorder.GetDetectedComponents().Count());
+
+            var graph = componentRecorder.GetDependencyGraphsByLocation().Values.First(); // There should only be 1
+
+            // Verify explicitly referenced roots
+            var rootComponents = new List
+            {
+                "my_dependency 1.0.0 - Cargo",
+
+                // Note: my_other_dependency isn't here because we don't capture local deps
+                "other_dependency 0.4.0 - Cargo",
+            };
+
+            rootComponents.ForEach(rootComponentId => graph.IsComponentExplicitlyReferenced(rootComponentId).Should().BeTrue());
+
+            // Verify explicitly referenced dev roots
+            var rootDevComponents = new List { "my_dev_dependency 1.0.0 - Cargo" };
+
+            rootDevComponents.ForEach(rootDevComponentId => graph.IsComponentExplicitlyReferenced(rootDevComponentId).Should().BeTrue());
+
+            // Verify dependencies for my_dependency
+            var my_dependencyDependencies = new List { "same_package 1.0.0 - Cargo" };
+
+            graph.GetDependenciesForComponent("my_dependency 1.0.0 - Cargo").Should().BeEquivalentTo(my_dependencyDependencies);
+
+            // Verify dependencies for other_dependency
+            var other_dependencyDependencies = new List { "other_dependency_dependency 0.1.12-alpha.6 - Cargo" };
+
+            graph.GetDependenciesForComponent("other_dependency 0.4.0 - Cargo").Should().BeEquivalentTo(other_dependencyDependencies);
+
+            // Verify dependencies for my_dev_dependency
+            var my_dev_dependencyDependencies = new List { "other_dependency_dependency 0.1.12-alpha.6 - Cargo", "dev_dependency_dependency 0.2.23 - Cargo" };
+
+            graph.GetDependenciesForComponent("my_dev_dependency 1.0.0 - Cargo").Should().BeEquivalentTo(my_dev_dependencyDependencies);
+        }
+
+        [TestMethod]
+        public async Task TestRustDetector_SupportEmptySource()
+        {
+            var testTomlString = @"
+[package]
+name = ""my_test_package""
+version = ""1.2.3""
+authors = [""example@example.com>""]
+
+[dependencies]
+my_dependency = ""1.0""
+";
+            var testLockString = @"
+[[package]]
+name = ""my_dependency""
+version = ""1.0.0""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+dependencies = [
+ ""other_dependency_dependency 0.1.12-alpha.6 ()"",
+]
+
+[[package]]
+name = ""other_dependency_dependency""
+version = ""0.1.12-alpha.6""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+";
+            var (result, componentRecorder) = await detectorV2TestUtility
+                                                    .WithFile("Cargo.lock", testLockString)
+                                                    .WithFile("Cargo.toml", testTomlString, new List { "Cargo.toml" })
+                                                    .ExecuteDetector();
+
+            result.ResultCode.Should().Be(ProcessingResultCode.Success);
+
+            var dependencyGraphs = componentRecorder.GetDependencyGraphsByLocation();
+            dependencyGraphs.Count.Should().Be(1);
+
+            var dependencyGraph = dependencyGraphs.Single().Value;
+            var foundComponents = dependencyGraph.GetComponents();
+            foundComponents.Count().Should().Be(2);
+
+            componentRecorder.ForOneComponent("other_dependency_dependency 0.1.12-alpha.6 - Cargo", (grouping) =>
+            {
+                grouping.ParentComponentIdsThatAreExplicitReferences.Should().BeEquivalentTo("my_dependency 1.0.0 - Cargo");
+            });
+        }
+
+        [TestMethod]
+        public async Task TestRustV1Detector_WorkspacesWithTopLevelDependencies()
+        {
+            var (result, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("Cargo.lock", string.Concat(testWorkspaceLockBaseDependency, testWorkspaceLockV1NoBaseString))
+                                                    .WithFile("Cargo.toml", string.Concat(testWorkspaceTomlBaseDependency, testWorkspacesBaseTomlString), new List { "Cargo.toml" })
+                                                    .WithFile("Cargo.toml", testWorkspace1TomlString, new List { "Cargo.toml" }, fileLocation: Path.Combine(Path.GetTempPath(), "test-work", "Cargo.toml"))
+                                                    .WithFile("Cargo.toml", testWorkspace2TomlString, new List { "Cargo.toml" }, fileLocation: Path.Combine(Path.GetTempPath(), "test-work2", "Cargo.toml"))
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, result.ResultCode);
+            Assert.AreEqual(7, componentRecorder.GetDetectedComponents().Count());
+
+            List packageVersions = new List()
+            {
+                "dev_dependency_dependency 0.2.23",
+                "one_more_dev_dep 1.0.0",
+                "other_dependency 0.4.0",
+                "other_dependency_dependency 0.1.12-alpha.6",
+                "my_dependency 1.0.0",
+                "same_package 1.0.0",
+                "test_package 2.0.0",
+            };
+
+            IDictionary packageIsDevDependency = new Dictionary()
+            {
+                { "dev_dependency_dependency 0.2.23 - Cargo", true },
+                { "one_more_dev_dep 1.0.0 - Cargo", true },
+                { "other_dependency 0.4.0 - Cargo", false },
+                { "other_dependency_dependency 0.1.12-alpha.6 - Cargo", false },
+                { "my_dependency 1.0.0 - Cargo", false },
+                { "same_package 1.0.0 - Cargo", false },
+                { "test_package 2.0.0 - Cargo", false },
+            };
+
+            IDictionary> packageDependencyRoots = new Dictionary>()
+            {
+                { "dev_dependency_dependency 0.2.23", new HashSet() { "dev_dependency_dependency 0.2.23" } },
+                { "one_more_dev_dep 1.0.0", new HashSet() { "dev_dependency_dependency 0.2.23" } },
+                { "other_dependency 0.4.0", new HashSet() { "other_dependency 0.4.0" } },
+                { "other_dependency_dependency 0.1.12-alpha.6", new HashSet() { "other_dependency 0.4.0" } },
+                { "my_dependency 1.0.0", new HashSet() { "my_dependency 1.0.0" } },
+                { "same_package 1.0.0",  new HashSet() { "my_dependency 1.0.0" } },
+                { "test_package 2.0.0", new HashSet() { "test_package 2.0.0" } },
+            };
+
+            ISet componentNames = new HashSet();
+            foreach (var discoveredComponent in componentRecorder.GetDetectedComponents())
+            {
+                var component = discoveredComponent.Component as CargoComponent;
+                var componentKey = $"{component.Name} {component.Version}";
+
+                // Verify version
+                Assert.IsTrue(packageVersions.Contains(componentKey));
+
+                // Verify dev dependency flag
+                componentRecorder.GetEffectiveDevDependencyValue(discoveredComponent.Component.Id).Should().Be(packageIsDevDependency[discoveredComponent.Component.Id]);
+
+                componentRecorder.AssertAllExplicitlyReferencedComponents(
+                    discoveredComponent.Component.Id,
+                    packageDependencyRoots[componentKey].Select(expectedRoot =>
+                        new Func(parentComponent => $"{parentComponent.Name} {parentComponent.Version}" == expectedRoot)).ToArray());
+
+                componentNames.Add(componentKey);
+            }
+
+            // Verify all packages were detected
+            foreach (var expectedPackage in packageVersions)
+            {
+                Assert.IsTrue(componentNames.Contains(expectedPackage));
+            }
+        }
+
+        [TestMethod]
+        public async Task TestRustV2Detector_WorkspacesWithTopLevelDependencies()
+        {
+            var (result, componentRecorder) = await detectorV2TestUtility
+                                                    .WithFile("Cargo.lock", string.Concat(testWorkspaceLockBaseDependency, testWorkspaceLockV2NoBaseString))
+                                                    .WithFile("Cargo.toml", string.Concat(testWorkspaceTomlBaseDependency, testWorkspacesBaseTomlString), new List { "Cargo.toml" })
+                                                    .WithFile("Cargo.toml", testWorkspace1TomlString, new List { "Cargo.toml" }, fileLocation: Path.Combine(Path.GetTempPath(), "test-work", "Cargo.toml"))
+                                                    .WithFile("Cargo.toml", testWorkspace2TomlString, new List { "Cargo.toml" }, fileLocation: Path.Combine(Path.GetTempPath(), "test-work2", "Cargo.toml"))
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, result.ResultCode);
+            Assert.AreEqual(7, componentRecorder.GetDetectedComponents().Count());
+
+            List packageVersions = new List()
+            {
+                "dev_dependency_dependency 0.2.23",
+                "one_more_dev_dep 1.0.0",
+                "other_dependency 0.4.0",
+                "other_dependency_dependency 0.1.12-alpha.6",
+                "my_dependency 1.0.0",
+                "same_package 1.0.0",
+                "test_package 2.0.0",
+            };
+
+            IDictionary packageIsDevDependency = new Dictionary()
+            {
+                { "dev_dependency_dependency 0.2.23 - Cargo", true },
+                { "one_more_dev_dep 1.0.0 - Cargo", true },
+                { "other_dependency 0.4.0 - Cargo", false },
+                { "other_dependency_dependency 0.1.12-alpha.6 - Cargo", false },
+                { "my_dependency 1.0.0 - Cargo", false },
+                { "same_package 1.0.0 - Cargo", false },
+                { "test_package 2.0.0 - Cargo", false },
+            };
+
+            IDictionary> packageDependencyRoots = new Dictionary>()
+            {
+                { "dev_dependency_dependency 0.2.23", new HashSet() { "dev_dependency_dependency 0.2.23" } },
+                { "one_more_dev_dep 1.0.0", new HashSet() { "dev_dependency_dependency 0.2.23" } },
+                { "other_dependency 0.4.0", new HashSet() { "other_dependency 0.4.0" } },
+                { "other_dependency_dependency 0.1.12-alpha.6", new HashSet() { "other_dependency 0.4.0" } },
+                { "my_dependency 1.0.0", new HashSet() { "my_dependency 1.0.0" } },
+                { "same_package 1.0.0",  new HashSet() { "my_dependency 1.0.0" } },
+                { "test_package 2.0.0", new HashSet() { "test_package 2.0.0" } },
+            };
+
+            ISet componentNames = new HashSet();
+            foreach (var discoveredComponent in componentRecorder.GetDetectedComponents())
+            {
+                var component = discoveredComponent.Component as CargoComponent;
+                var componentKey = $"{component.Name} {component.Version}";
+
+                // Verify version
+                Assert.IsTrue(packageVersions.Contains(componentKey));
+
+                // Verify dev dependency flag
+                componentRecorder.GetEffectiveDevDependencyValue(discoveredComponent.Component.Id).Should().Be(packageIsDevDependency[discoveredComponent.Component.Id]);
+
+                componentRecorder.AssertAllExplicitlyReferencedComponents(
+                    discoveredComponent.Component.Id,
+                    packageDependencyRoots[componentKey].Select(expectedRoot =>
+                        new Func(parentComponent => $"{parentComponent.Name} {parentComponent.Version}" == expectedRoot)).ToArray());
+
+                componentNames.Add(componentKey);
+            }
+
+            // Verify all packages were detected
+            foreach (var expectedPackage in packageVersions)
+            {
+                Assert.IsTrue(componentNames.Contains(expectedPackage));
+            }
+        }
+
+        [TestMethod]
+        public async Task TestRustV1Detector_WorkspacesNoTopLevelDependencies()
+        {
+            var (result, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("Cargo.lock", testWorkspaceLockV1NoBaseString)
+                                                    .WithFile("Cargo.toml", testWorkspacesBaseTomlString, new List { "Cargo.toml" })
+                                                    .WithFile("Cargo.toml", testWorkspace1TomlString, new List { "Cargo.toml" }, fileLocation: Path.Combine(Path.GetTempPath(), "test-work", "Cargo.toml"))
+                                                    .WithFile("Cargo.toml", testWorkspace2TomlString, new List { "Cargo.toml" }, fileLocation: Path.Combine(Path.GetTempPath(), "test-work2", "Cargo.toml"))
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, result.ResultCode);
+            Assert.AreEqual(6, componentRecorder.GetDetectedComponents().Count());
+        }
+
+        [TestMethod]
+        public async Task TestRustV2Detector_WorkspacesNoTopLevelDependencies()
+        {
+            var (result, componentRecorder) = await detectorV2TestUtility
+                                                    .WithFile("Cargo.lock", testWorkspaceLockV2NoBaseString)
+                                                    .WithFile("Cargo.toml", testWorkspacesBaseTomlString, new List { "Cargo.toml" })
+                                                    .WithFile("Cargo.toml", testWorkspace1TomlString, new List { "Cargo.toml" }, fileLocation: Path.Combine(Path.GetTempPath(), "test-work", "Cargo.toml"))
+                                                    .WithFile("Cargo.toml", testWorkspace2TomlString, new List { "Cargo.toml" }, fileLocation: Path.Combine(Path.GetTempPath(), "test-work2", "Cargo.toml"))
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, result.ResultCode);
+            Assert.AreEqual(6, componentRecorder.GetDetectedComponents().Count());
+        }
+
+        [TestMethod]
+        public async Task TestRustV1Detector_WorkspacesWithSubDirectories()
+        {
+            var (result, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("Cargo.lock", testWorkspaceLockV1NoBaseString)
+                                                    .WithFile("Cargo.toml", testWorkspacesSubdirectoryTomlString, new List { "Cargo.toml" })
+                                                    .WithFile("Cargo.toml", testWorkspace1TomlString, new List { "Cargo.toml" }, fileLocation: Path.Combine(Path.GetTempPath(), "sub//test-work", "Cargo.toml"))
+                                                    .WithFile("Cargo.toml", testWorkspace2TomlString, new List { "Cargo.toml" }, fileLocation: Path.Combine(Path.GetTempPath(), "sub2//test//test-work2", "Cargo.toml"))
+                                                    .ExecuteDetector();
+
+            var componentGraphs = componentRecorder.GetDependencyGraphsByLocation();
+
+            Assert.AreEqual(ProcessingResultCode.Success, result.ResultCode);
+            Assert.AreEqual(6, componentRecorder.GetDetectedComponents().Count());
+
+            Assert.AreEqual(1, componentGraphs.Count); // Only 1 cargo.lock is specified with multiple sub-directories of .toml
+
+            // A root Cargo.lock, Cargo.toml, and the 2 workspace Cargo.tomls should be registered
+            componentRecorder.ForAllComponents(x => x.AllFileLocations.Count().Should().Be(4));
+        }
+
+        [TestMethod]
+        public async Task TestRustV2Detector_WorkspacesWithSubDirectories()
+        {
+            var (result, componentRecorder) = await detectorV2TestUtility
+                                                    .WithFile("Cargo.lock", testWorkspaceLockV2NoBaseString)
+                                                    .WithFile("Cargo.toml", testWorkspacesSubdirectoryTomlString, new List { "Cargo.toml" })
+                                                    .WithFile("Cargo.toml", testWorkspace1TomlString, new List { "Cargo.toml" }, fileLocation: Path.Combine(Path.GetTempPath(), "sub//test-work", "Cargo.toml"))
+                                                    .WithFile("Cargo.toml", testWorkspace2TomlString, new List { "Cargo.toml" }, fileLocation: Path.Combine(Path.GetTempPath(), "sub2//test//test-work2", "Cargo.toml"))
+                                                    .ExecuteDetector();
+
+            var componentGraphs = componentRecorder.GetDependencyGraphsByLocation();
+
+            Assert.AreEqual(ProcessingResultCode.Success, result.ResultCode);
+            Assert.AreEqual(6, componentRecorder.GetDetectedComponents().Count());
+
+            Assert.AreEqual(1, componentGraphs.Count); // Only 1 cargo.lock is specified with multiple sub-directories of .toml
+
+            // A root Cargo.lock, Cargo.toml, and the 2 workspace Cargo.tomls should be registered
+            componentRecorder.ForAllComponents(x => x.AllFileLocations.Count().Should().Be(4));
+        }
+
+        /// 
+        /// (my_dependency, 1.0, root)
+        /// (my_other_dependency, 0.1.0, root)
+        /// (other_dependency, 0.4, root) -> (other_dependency_dependency, 0.1.12-alpha.6)
+        /// (my_dev_dependency, 1.0, root, dev) -> (other_dependency_dependency, 0.1.12-alpha.6)
+        ///                                     -> (dev_dependency_dependency, 0.2.23, dev) -> (one_more_dev_dep, 1.0.0, dev).
+        /// 
+        private readonly string testCargoTomlString = @"
+[package]
+name = ""my_test_package""
+version = ""1.2.3""
+authors = [""example@example.com>""]
+
+[dependencies]
+my_dependency = ""1.0""
+my_other_package = { path = ""../my_other_package_path"", version = ""0.1.0"" }
+other_dependency = { version = ""0.4"" }
+
+[dev-dependencies]
+my_dev_dependency = ""1.0""
+";
+
+        private readonly string testCargoLockString = @"
+[[package]]
+name = ""my_dependency""
+version = ""1.0.0""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+
+[[package]]
+name = ""other_dependency""
+version = ""0.4.0""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+dependencies = [
+ ""other_dependency_dependency 0.1.12-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)"",
+]
+
+[[package]]
+name = ""other_dependency_dependency""
+version = ""0.1.12-alpha.6""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+
+[[package]]
+name = ""my_dev_dependency""
+version = ""1.0.0""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+dependencies = [
+ ""other_dependency_dependency 0.1.12-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)"",
+ ""dev_dependency_dependency 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)"",
+]
+
+[[package]]
+name = ""dev_dependency_dependency""
+version = ""0.2.23""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+dependencies = [
+  ""one_more_dev_dep 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)""
+]
+
+[[package]]
+name = ""one_more_dev_dep""
+version = ""1.0.0""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+
+[[package]]
+name = ""my_other_package""
+version = ""1.0.0""
+
+[[package]]
+name = ""my_test_package""
+version = ""1.2.3""
+dependencies = [
+ ""my_dependency 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)"",
+ ""my_other_package 1.0.0"",
+ ""other_dependency 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)"",
+ ""my_dev_dependency 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)"",
+]
+
+[metadata]
+";
+
+        private readonly string testCargoLockV2String = @"
+[[package]]
+name = ""my_dependency""
+version = ""1.0.0""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+dependencies = [
+  ""same_package 1.0.0""
+]
+
+[[package]]
+name = ""other_dependency""
+version = ""0.4.0""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+dependencies = [
+ ""other_dependency_dependency"",
+]
+
+[[package]]
+name = ""other_dependency_dependency""
+version = ""0.1.12-alpha.6""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+
+[[package]]
+name = ""my_dev_dependency""
+version = ""1.0.0""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+dependencies = [
+ ""other_dependency_dependency 0.1.12-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)"",
+ ""dev_dependency_dependency 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)"",
+]
+
+[[package]]
+name = ""dev_dependency_dependency""
+version = ""0.2.23""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+dependencies = [
+  ""same_package 2.0.0""
+]
+
+[[package]]
+name = ""my_other_package""
+version = ""1.0.0""
+
+[[package]]
+name = ""my_test_package""
+version = ""1.2.3""
+dependencies = [
+ ""my_dependency 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)"",
+ ""my_other_package 1.0.0"",
+ ""other_dependency 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)"",
+ ""my_dev_dependency 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)"",
+]
+
+[[package]]
+name = ""same_package""
+version = ""1.0.0""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+
+[[package]]
+name = ""same_package""
+version = ""2.0.0""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+";
+
+        private readonly string testWorkspacesBaseTomlString = @"[workspace]
+members = [
+    ""test-work"",
+    ""test-work2"",
+]
+";
+
+        private readonly string testWorkspacesSubdirectoryTomlString = @"[workspace]
+members = [
+    ""sub/test-work"",
+    ""sub2/test/test-work2"",
+]
+";
+
+        private readonly string testWorkspaceLockV1NoBaseString = @"[[package]]
+name = ""dev_dependency_dependency""
+version = ""0.2.23""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+dependencies = [
+  ""one_more_dev_dep 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)""
+]
+
+[[package]]
+name = ""one_more_dev_dep""
+version = ""1.0.0""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+
+[[package]]
+name = ""other_dependency""
+version = ""0.4.0""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+dependencies = [
+ ""other_dependency_dependency 0.1.12-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)"",
+]
+
+[[package]]
+name = ""other_dependency_dependency""
+version = ""0.1.12-alpha.6""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+
+[[package]]
+name = ""my_dependency""
+version = ""1.0.0""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+dependencies = [
+  ""same_package 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)""
+]
+
+[[package]]
+name = ""same_package""
+version = ""1.0.0""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+
+[metadata]
+";
+
+        private readonly string testWorkspaceLockV2NoBaseString = @"[[package]]
+name = ""dev_dependency_dependency""
+version = ""0.2.23""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+dependencies = [
+  ""one_more_dev_dep 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)""
+]
+
+[[package]]
+name = ""one_more_dev_dep""
+version = ""1.0.0""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+
+[[package]]
+name = ""other_dependency""
+version = ""0.4.0""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+dependencies = [
+ ""other_dependency_dependency 0.1.12-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)"",
+]
+
+[[package]]
+name = ""other_dependency_dependency""
+version = ""0.1.12-alpha.6""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+
+[[package]]
+name = ""my_dependency""
+version = ""1.0.0""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+dependencies = [
+  ""same_package 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)""
+]
+
+[[package]]
+name = ""same_package""
+version = ""1.0.0""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+";
+
+        private readonly string testWorkspaceLockBaseDependency = @"
+[[package]]
+name = ""test_package""
+version = ""2.0.0""
+source = ""registry+https://github.com/rust-lang/crates.io-index""
+";
+
+        private readonly string testWorkspaceTomlBaseDependency = @"
+[dependencies]
+test_package = ""2.0.0""
+";
+
+        private readonly string testWorkspace1TomlString = @"
+[dependencies]
+my_dependency = ""1.0.0""
+
+[dev-dependencies]
+dev_dependency_dependency = ""0.2.23""
+";
+
+        private readonly string testWorkspace2TomlString = @"
+[dependencies]
+other_dependency = ""0.4.0""
+";
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/RustDependencySpecifierTest.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/RustDependencySpecifierTest.cs
new file mode 100644
index 000000000..c1abdb44b
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/RustDependencySpecifierTest.cs
@@ -0,0 +1,57 @@
+using System.Collections.Generic;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Detectors.Rust;
+using Microsoft.ComponentDetection.Detectors.Rust.Contracts;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class RustDependencySpecifierTest
+    {
+        [TestMethod]
+        public void DoesntMatch_WhenNoDependencyAdded()
+        {
+            var testCases = new List<(bool shouldMatch, string caseName, string specifierName, string specifierRange)>
+            {
+                (false, "DoesntMatch_WhenNoDependencyAdded", null, null),
+                (false, "DoesntMatch_WhenDependencyAddedForDifferentPackage", "some-other-package", "1.2.3"),
+                (false, "DoesntMatch_WhenPointVersionDoesntMatch", "some-cargo-package", "1.2.4"),
+                (true, "Matches_WhenPointVersionMatches", "some-cargo-package", "1.2.3"),
+                (true, "Matches_WhenRangeMatches_BottomInclusive", "some-cargo-package", ">= 1.2.3, < 1.3.3"),
+                (true, "Matches_WhenRangeMatches_TopInclusive", "some-cargo-package", ">= 0.1.1, <= 1.2.3"),
+                (false, "DoesntMatch_WhenRangeExcludes_TopExclusive", "some-cargo-package", ">= 0.1.1, < 1.2.3"),
+                (false, "DoesntMatch_WhenRangeExcludes_BottomExclusive", "some-cargo-package", "> 1.2.3, < 1.2.5"),
+                (true, "Matches_WhenRangeIncludes_SingleTerm", "some-cargo-package", "> 1.2.2"),
+                (false, "DoesntMatch_WhenRangeExcludes_SingleTerm", "some-cargo-package", "> 1.2.3"),
+                (true, "Matches_~", "some-cargo-package", "~1.2.2"),
+                (false, "DoesntMatch_~", "some-cargo-package", "~1.2.4"),
+                (true, "Matches_*", "some-cargo-package", "1.2.*"),
+                (false, "DoesntMatch_*", "some-cargo-package", "1.1.*"),
+                (false, "DoesntMatch_*", "some-cargo-package", "1.1.*"),
+                (true, "Matches_^", "some-cargo-package", "^1.1.0"),
+                (false, "DoesntMatch_^", "some-cargo-package", "^1.3.0"),
+                (true, "Matches_-", "some-cargo-package", "1.2.0 - 1.2.5"),
+                (false, "DoesntMatch_-", "some-cargo-package", "1.1.0 - 1.1.5"),
+            };
+            DoAllTheTests(testCases);
+        }
+
+        public void DoAllTheTests(IEnumerable<(bool shouldMatch, string caseName, string specifierName, string specifierRange)> testCases)
+        {
+            foreach (var testCase in testCases)
+            {
+                DependencySpecification di = new DependencySpecification();
+                if (testCase.specifierName != null)
+                {
+                    di.Add(testCase.specifierName, testCase.specifierRange);
+                }
+
+                di.MatchesPackage(new CargoPackage { name = "some-cargo-package", version = "1.2.3" })
+                    .Should().Be(testCase.shouldMatch, testCase.caseName);
+            }
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/Utilities/ComponentRecorderTestUtilities.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/Utilities/ComponentRecorderTestUtilities.cs
new file mode 100644
index 000000000..bc7ee33e8
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/Utilities/ComponentRecorderTestUtilities.cs
@@ -0,0 +1,149 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests.Utilities
+{
+    public static class ComponentRecorderTestUtilities
+    {
+        public static void ForAllComponents(this IComponentRecorder recorder, Action forEachComponent)
+        {
+            var allComponents = recorder.GetDetectedComponents();
+            var graphs = recorder.GetDependencyGraphsByLocation();
+
+            // This magic grouping is a flattening of "occurrences" of components across single file recorders. This allows aggregate operations
+            //  per component id, which is logical for most tests.
+            List> graphsAndLocationsByComponentId = GroupByComponentId(graphs);
+
+            foreach (var item in graphsAndLocationsByComponentId)
+            {
+                forEachComponent(TupleToObject(item));
+            }
+        }
+
+        public static void ForOneComponent(this IComponentRecorder recorder, string componentId, Action forOneComponent)
+        {
+            var allComponents = recorder.GetDetectedComponents();
+            var graphs = recorder.GetDependencyGraphsByLocation();
+
+            // This magic grouping is a flattening of "occurrences" of components across single file recorders. This allows aggregate operations
+            //  per component id, which is logical for most tests.
+            List> graphsAndLocationsByComponentId = GroupByComponentId(graphs);
+
+            forOneComponent(TupleToObject(graphsAndLocationsByComponentId.First(x => x.Key == componentId)));
+        }
+
+        public static bool? GetEffectiveDevDependencyValue(this IComponentRecorder recorder, string componentId)
+        {
+            bool? existingDevDepValue = null;
+            recorder.ForOneComponent(componentId, grouping =>
+            {
+                foreach (var graph in grouping.FoundInGraphs)
+                {
+                    var devDepValue = graph.graph.IsDevelopmentDependency(componentId);
+                    if (!existingDevDepValue.HasValue)
+                    {
+                        existingDevDepValue = devDepValue;
+                    }
+                    else if (devDepValue.HasValue)
+                    {
+                        existingDevDepValue &= devDepValue;
+                    }
+                }
+            });
+
+            return existingDevDepValue;
+        }
+
+        public static bool IsDependencyOfExplicitlyReferencedComponents(
+            this IComponentRecorder recorder,
+            string componentIdToValidate,
+            params Func[] locatingPredicatesForParentExplicitReference)
+        {
+            bool isDependency = false;
+            recorder.ForOneComponent(componentIdToValidate, grouping =>
+            {
+                isDependency = true;
+                foreach (var predicate in locatingPredicatesForParentExplicitReference)
+                {
+                    var dependencyModel = recorder.GetDetectedComponents().Select(x => x.Component).OfType()
+                                                                           .FirstOrDefault(predicate) as TypedComponent;
+                    isDependency &= grouping.ParentComponentIdsThatAreExplicitReferences.Contains(dependencyModel.Id);
+                }
+            });
+
+            return isDependency;
+        }
+
+        public static void AssertAllExplicitlyReferencedComponents(
+            this IComponentRecorder recorder,
+            string componentIdToValidate,
+            params Func[] locatingPredicatesForParentExplicitReference)
+        {
+            recorder.ForOneComponent(componentIdToValidate, grouping =>
+            {
+                HashSet explicitReferrers = new HashSet(grouping.ParentComponentIdsThatAreExplicitReferences);
+                int assertionIndex = 0;
+                foreach (var predicate in locatingPredicatesForParentExplicitReference)
+                {
+                    var dependencyModel = recorder.GetDetectedComponents().Select(x => x.Component).OfType()
+                                                                           .FirstOrDefault(predicate) as TypedComponent;
+                    if (dependencyModel == null)
+                    {
+                        throw new InvalidOperationException($"One of the predicates (index {assertionIndex}) failed to find a valid component in the Scan Result's discovered components.");
+                    }
+
+                    if (!grouping.ParentComponentIdsThatAreExplicitReferences.Contains(dependencyModel.Id))
+                    {
+                        throw new InvalidOperationException($"Expected component Id {componentIdToValidate} to have {dependencyModel.Id} as a parent explicit reference, but did not.");
+                    }
+
+                    explicitReferrers.Remove(dependencyModel.Id);
+                    assertionIndex++;
+                }
+
+                if (explicitReferrers.Count > 0)
+                {
+                    throw new InvalidOperationException($"Component Id {componentIdToValidate} had parent explicit references ({string.Join(',', explicitReferrers)}) that were not verified via submitted delegates.");
+                }
+            });
+        }
+
+        private static ComponentOrientedGrouping TupleToObject(IEnumerable<(string Location, IDependencyGraph Graph, string ComponentId)> x)
+        {
+            var additionalRelatedFiles = new List(x.SelectMany(y => y.Graph.GetAdditionalRelatedFiles()));
+            additionalRelatedFiles.AddRange(x.Select(y => y.Location));
+
+            return new ComponentOrientedGrouping
+            {
+                ComponentId = x.First().ComponentId,
+                FoundInGraphs = x.Select(y => (y.Location, y.Graph)).ToList(),
+                AllFileLocations = additionalRelatedFiles.Distinct().ToList(),
+                ParentComponentIdsThatAreExplicitReferences = x.SelectMany(y => y.Graph.GetExplicitReferencedDependencyIds(x.First().ComponentId)).Distinct().ToList(),
+            };
+        }
+
+        private static List> GroupByComponentId(IReadOnlyDictionary graphs)
+        {
+            return graphs
+                            .Select(x => (Location: x.Key, Graph: x.Value))
+                            .SelectMany(x => x.Graph.GetComponents()
+                                                .Select(componentId => (x.Location, x.Graph, ComponentId: componentId)))
+                            .GroupBy(x => x.ComponentId)
+                            .ToList();
+        }
+
+        public class ComponentOrientedGrouping
+        {
+            public IEnumerable<(string manifestFile, IDependencyGraph graph)> FoundInGraphs { get; set; }
+
+            public string ComponentId { get; set; }
+
+            public IEnumerable AllFileLocations { get; set; }
+
+            public IEnumerable ParentComponentIdsThatAreExplicitReferences { get; internal set; }
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/Utilities/EnumerableStringComparer.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/Utilities/EnumerableStringComparer.cs
new file mode 100644
index 000000000..18a6ce373
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/Utilities/EnumerableStringComparer.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests.Utilities
+{
+    // https://stackoverflow.com/questions/35128996/groupby-on-complex-object-e-g-listt
+    public class EnumerableStringComparer : IEqualityComparer>
+    {
+        public bool Equals([AllowNull] IEnumerable x, [AllowNull] IEnumerable y)
+        {
+            return x.SequenceEqual(y);
+        }
+
+        public int GetHashCode([DisallowNull] IEnumerable obj)
+        {
+            return obj.Aggregate(0, (a, y) => a ^ y.GetHashCode());
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/Utilities/TestUtilityExtensions.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/Utilities/TestUtilityExtensions.cs
new file mode 100644
index 000000000..257cfca00
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/Utilities/TestUtilityExtensions.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Security.Cryptography;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests.Utilities
+{
+    internal static class TestUtilityExtensions
+    {
+        public static string NewRandomVersion()
+        {
+            return new Version(
+                RandomNumberGenerator.GetInt32(0, 1000),
+                RandomNumberGenerator.GetInt32(0, 1000),
+                RandomNumberGenerator.GetInt32(0, 1000))
+                .ToString();
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/YarnBlockFileTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/YarnBlockFileTests.cs
new file mode 100644
index 000000000..c1b3a74db
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/YarnBlockFileTests.cs
@@ -0,0 +1,295 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Detectors.Yarn;
+using Microsoft.ComponentDetection.Detectors.Yarn.Parsers;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class YarnBlockFileTests
+    {
+        [TestMethod]
+        public async Task BlockFileParserWithNullStream_Fails()
+        {
+            Func action = async () => await YarnBlockFile.CreateBlockFileAsync(null);
+
+            await Assert.ThrowsExceptionAsync(action);
+        }
+
+        [TestMethod]
+        public async Task BlockFileParserWithClosedStream_Fails()
+        {
+            using MemoryStream stream = new MemoryStream();
+
+            stream.Close();
+
+            Func action = async () => await YarnBlockFile.CreateBlockFileAsync(stream);
+
+            await Assert.ThrowsExceptionAsync(action);
+        }
+
+        [TestMethod]
+        public async Task BlockFileParserWithEmptyStream_ProducesEnumerableOfZero()
+        {
+            YarnBlockFile file;
+            using (var stream = new MemoryStream())
+            {
+                file = await YarnBlockFile.CreateBlockFileAsync(stream);
+            }
+
+            Assert.AreEqual(0, file.Count());
+            Assert.AreEqual(string.Empty, file.VersionHeader);
+            Assert.AreEqual(YarnLockVersion.Invalid, file.YarnLockVersion);
+        }
+
+        [TestMethod]
+        public async Task BlockFileParserV1WithVersionString_ProducesEnumerableOfZero()
+        {
+            string yarnLockFileVersionString = "#yarn lockfile v1";
+
+            using var stream = new MemoryStream();
+
+            using var writer = new StreamWriter(stream);
+
+            writer.WriteLine(yarnLockFileVersionString);
+            writer.Flush();
+            stream.Seek(0, SeekOrigin.Begin);
+
+            var file = await YarnBlockFile.CreateBlockFileAsync(stream);
+
+            Assert.AreEqual(0, file.Count());
+            Assert.AreEqual(yarnLockFileVersionString, file.VersionHeader);
+            Assert.AreEqual(YarnLockVersion.V1, file.YarnLockVersion);
+        }
+
+        [TestMethod]
+        public async Task BlockFileParserV1WithSingleBlock_Parses()
+        {
+            string yarnLockFileVersionString = "#yarn lockfile v1";
+
+            using var stream = new MemoryStream();
+
+            using var writer = new StreamWriter(stream);
+
+            writer.WriteLine(yarnLockFileVersionString);
+            writer.WriteLine();
+            writer.WriteLine("block1:");
+            writer.WriteLine("  property \"value\"");
+            writer.WriteLine("  block2:");
+            writer.WriteLine("    otherProperty \"otherValue\"");
+
+            writer.Flush();
+            stream.Seek(0, SeekOrigin.Begin);
+
+            var file = await YarnBlockFile.CreateBlockFileAsync(stream);
+
+            var block = file.Single();
+
+            Assert.AreEqual(block.Title, "block1");
+            Assert.AreEqual(1, block.Children.Count);
+            Assert.AreEqual("value", block.Values["property"]);
+            Assert.AreEqual("otherValue", block.Children.Single(x => x.Title == "block2").Values["otherProperty"]);
+            Assert.AreEqual(yarnLockFileVersionString, file.VersionHeader);
+            Assert.AreEqual(YarnLockVersion.V1, file.YarnLockVersion);
+        }
+
+        [TestMethod]
+        public async Task BlockFileParserV1WithSeveralBlocks_Parses()
+        {
+            string yarnLockFileVersionString = "#yarn lockfile v1";
+
+            using var stream = new MemoryStream();
+
+            using var writer = new StreamWriter(stream);
+
+            writer.WriteLine(yarnLockFileVersionString);
+            writer.WriteLine();
+            writer.WriteLine("block1:");
+            writer.WriteLine("  property \"value\"");
+            writer.WriteLine("  childblock1:");
+            writer.WriteLine("    otherProperty \"otherValue\"");
+
+            writer.WriteLine();
+
+            writer.WriteLine(yarnLockFileVersionString);
+            writer.WriteLine();
+            writer.WriteLine("block2:");
+            writer.WriteLine("  property \"value\"");
+            writer.WriteLine("  childBlock2:");
+            writer.WriteLine("    otherProperty \"otherValue\"");
+
+            writer.WriteLine();
+
+            writer.WriteLine(yarnLockFileVersionString);
+            writer.WriteLine();
+            writer.WriteLine("block3:");
+            writer.WriteLine("  property \"value\"");
+            writer.WriteLine("  childBlock3:");
+            writer.WriteLine("    otherProperty \"otherValue\"");
+
+            writer.Flush();
+            stream.Seek(0, SeekOrigin.Begin);
+
+            var file = await YarnBlockFile.CreateBlockFileAsync(stream);
+
+            Assert.AreEqual(3, file.Count());
+            Assert.AreEqual(yarnLockFileVersionString, file.VersionHeader);
+            Assert.AreEqual(YarnLockVersion.V1, file.YarnLockVersion);
+        }
+
+        [TestMethod]
+        public async Task BlockFileParserV2WithMetadataBlock_Parses()
+        {
+            string yarnLockFileVersionString = "__metadata:";
+
+            using var stream = new MemoryStream();
+
+            using var writer = new StreamWriter(stream);
+
+            writer.WriteLine("# This file is generated by running \"yarn install\" inside your project.");
+            writer.WriteLine("# Manual changes might be lost - proceed with caution!");
+            writer.WriteLine();
+            writer.WriteLine("__metadata:");
+            writer.WriteLine("  version: 4");
+            writer.WriteLine("  cacheKey: 7");
+            writer.WriteLine();
+
+            writer.Flush();
+            stream.Seek(0, SeekOrigin.Begin);
+
+            var file = await YarnBlockFile.CreateBlockFileAsync(stream);
+
+            Assert.AreEqual(0, file.Count());
+            Assert.AreEqual(yarnLockFileVersionString, file.VersionHeader);
+            Assert.AreEqual(YarnLockVersion.V2, file.YarnLockVersion);
+        }
+
+        [TestMethod]
+        public async Task BlockFileParserV2WithSingleBlock_Parses()
+        {
+            string yarnLockFileVersionString = "__metadata:";
+
+            using var stream = new MemoryStream();
+
+            using var writer = new StreamWriter(stream);
+
+            writer.WriteLine("# This file is generated by running \"yarn install\" inside your project.");
+            writer.WriteLine("# Manual changes might be lost - proceed with caution!");
+            writer.WriteLine();
+            writer.WriteLine("__metadata:");
+            writer.WriteLine("  version: 4");
+            writer.WriteLine("  cacheKey: 7");
+
+            writer.WriteLine();
+
+            writer.WriteLine("block1:");
+            writer.WriteLine("  property: value");
+            writer.WriteLine("  block2:");
+            writer.WriteLine("    otherProperty: otherValue");
+
+            writer.Flush();
+            stream.Seek(0, SeekOrigin.Begin);
+
+            var file = await YarnBlockFile.CreateBlockFileAsync(stream);
+
+            var block = file.Single();
+
+            Assert.AreEqual(block.Title, "block1");
+            Assert.AreEqual(1, block.Children.Count);
+            Assert.AreEqual("value", block.Values["property"]);
+            Assert.AreEqual("otherValue", block.Children.Single(x => x.Title == "block2").Values["otherProperty"]);
+            Assert.AreEqual(yarnLockFileVersionString, file.VersionHeader);
+            Assert.AreEqual(YarnLockVersion.V2, file.YarnLockVersion);
+        }
+
+        [TestMethod]
+        public async Task BlockFileParserV2WithSingleBlock_ParsesWithQuotes()
+        {
+            string yarnLockFileVersionString = "__metadata:";
+
+            using var stream = new MemoryStream();
+
+            using var writer = new StreamWriter(stream);
+
+            writer.WriteLine("# This file is generated by running \"yarn install\" inside your project.");
+            writer.WriteLine("# Manual changes might be lost - proceed with caution!");
+            writer.WriteLine();
+            writer.WriteLine("__metadata:");
+            writer.WriteLine("  version: 4");
+            writer.WriteLine("  cacheKey: 7");
+
+            writer.WriteLine();
+
+            writer.WriteLine("block1:");
+            writer.WriteLine("  property: \"value\"");
+            writer.WriteLine("  block2:");
+            writer.WriteLine("    \"otherProperty\": otherValue");
+
+            writer.Flush();
+            stream.Seek(0, SeekOrigin.Begin);
+
+            var file = await YarnBlockFile.CreateBlockFileAsync(stream);
+
+            var block = file.Single();
+
+            Assert.AreEqual(block.Title, "block1");
+            Assert.AreEqual(1, block.Children.Count);
+            Assert.AreEqual("value", block.Values["property"]);
+            Assert.AreEqual("otherValue", block.Children.Single(x => x.Title == "block2").Values["otherProperty"]);
+            Assert.AreEqual(yarnLockFileVersionString, file.VersionHeader);
+            Assert.AreEqual(YarnLockVersion.V2, file.YarnLockVersion);
+        }
+
+        [TestMethod]
+        public async Task BlockFileParserV2WithMultipleBlocks_Parses()
+        {
+            string yarnLockFileVersionString = "__metadata:";
+
+            using var stream = new MemoryStream();
+
+            using var writer = new StreamWriter(stream);
+
+            writer.WriteLine("# This file is generated by running \"yarn install\" inside your project.");
+            writer.WriteLine("# Manual changes might be lost - proceed with caution!");
+            writer.WriteLine();
+            writer.WriteLine("__metadata:");
+            writer.WriteLine("  version: 4");
+            writer.WriteLine("  cacheKey: 7");
+
+            writer.WriteLine();
+
+            writer.WriteLine("block1:");
+            writer.WriteLine("  property: value");
+            writer.WriteLine("  childblock1:");
+            writer.WriteLine("    otherProperty: otherValue");
+
+            writer.WriteLine();
+
+            writer.WriteLine("block2:");
+            writer.WriteLine("  property: value");
+            writer.WriteLine("  childblock2:");
+            writer.WriteLine("    otherProperty: otherValue");
+
+            writer.WriteLine();
+
+            writer.WriteLine("block3:");
+            writer.WriteLine("  property: value");
+            writer.WriteLine("  childblock3:");
+            writer.WriteLine("    otherProperty: otherValue");
+
+            writer.Flush();
+            stream.Seek(0, SeekOrigin.Begin);
+
+            var file = await YarnBlockFile.CreateBlockFileAsync(stream);
+
+            Assert.AreEqual(3, file.Count());
+            Assert.AreEqual(yarnLockFileVersionString, file.VersionHeader);
+            Assert.AreEqual(YarnLockVersion.V2, file.YarnLockVersion);
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/YarnLockDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/YarnLockDetectorTests.cs
new file mode 100644
index 000000000..1c79e64a7
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/YarnLockDetectorTests.cs
@@ -0,0 +1,953 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Common;
+using Microsoft.ComponentDetection.Common.DependencyGraph;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.Internal;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Detectors.Tests.Utilities;
+using Microsoft.ComponentDetection.Detectors.Yarn;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Microsoft.ComponentDetection.TestsUtilities;
+using Newtonsoft.Json;
+
+using static Microsoft.ComponentDetection.Detectors.Tests.Utilities.TestUtilityExtensions;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class YarnLockDetectorTests
+    {
+        private Mock loggerMock;
+        private ComponentRecorder componentRecorder;
+        private DetectorTestUtility detectorTestUtility;
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            loggerMock = new Mock();
+            componentRecorder = new ComponentRecorder();
+            detectorTestUtility = DetectorTestUtilityCreator.Create();
+        }
+
+        [TestMethod]
+        public async Task WellFormedYarnLockV1WithZeroComponents_FindsNothing()
+        {
+            var yarnLock = YarnTestUtilities.GetWellFormedEmptyYarnV1LockFile();
+            var packageJson = NpmTestUtilities.GetPackageJsonNoDependencies();
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("yarn.lock", yarnLock)
+                                                    .WithFile("package.json", packageJson, new List { "package.json" })
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(0, componentRecorder.GetDetectedComponents().Count());
+        }
+
+        [TestMethod]
+        public async Task WellFormedYarnLockV2WithZeroComponents_FindsNothing()
+        {
+            var yarnLock = YarnTestUtilities.GetWellFormedEmptyYarnV2LockFile();
+            var packageJson = NpmTestUtilities.GetPackageJsonNoDependencies();
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("yarn.lock", yarnLock)
+                                                    .WithFile("package.json", packageJson, new List { "package.json" })
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(0, componentRecorder.GetDetectedComponents().Count());
+        }
+
+        [TestMethod]
+        public async Task MalformedYarnLockV1WithOneComponent_FindsNoComponent()
+        {
+            var componentName0 = Guid.NewGuid().ToString("N");
+            var version0 = NewRandomVersion();
+            var providedVersion0 = $"^{version0}";
+            var resolved0 = "https://resolved0/a/resolved";
+
+            var builder = new StringBuilder();
+
+            builder.AppendLine("# THIS IS A YARNFILE");
+            builder.AppendLine("# yarn lockfile v1");
+            builder.AppendLine();
+            builder.AppendLine($"{componentName0}@{providedVersion0}");
+            builder.AppendLine($"  version {version0}");
+            builder.AppendLine($"  resolved {resolved0}");
+
+            var yarnLock = builder.ToString();
+            var (packageJsonName, packageJsonContent, packageJsonPath) = NpmTestUtilities.GetPackageJsonOneRoot(componentName0, providedVersion0);
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("yarn.lock", yarnLock)
+                                                    .WithFile("package.json", packageJsonContent, new List { "package.json" })
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(0, componentRecorder.GetDetectedComponents().Count());
+        }
+
+        [TestMethod]
+        public async Task MalformedYarnLockV2WithOneComponent_FindsNoComponent()
+        {
+            var componentName0 = Guid.NewGuid().ToString("N");
+            var version0 = NewRandomVersion();
+            var providedVersion0 = $"^{version0}";
+            var resolved0 = "https://resolved0/a/resolved";
+
+            var builder = new StringBuilder();
+
+            builder.AppendLine(CreateYarnLockV2FileContent(new List()));
+            builder.AppendLine($"{componentName0}@{providedVersion0}");
+            builder.AppendLine($"  version {version0}");
+            builder.AppendLine($"  resolved {resolved0}");
+
+            var yarnLock = builder.ToString();
+            var (packageJsonName, packageJsonContent, packageJsonPath) = NpmTestUtilities.GetPackageJsonOneRoot(componentName0, providedVersion0);
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("yarn.lock", yarnLock)
+                                                    .WithFile("package.json", packageJsonContent, new List { "package.json" })
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(0, componentRecorder.GetDetectedComponents().Count());
+        }
+
+        [TestMethod]
+        public async Task WellFormedYarnLockV1WithOneComponent_FindsComponent()
+        {
+            var version0 = NewRandomVersion();
+            var componentA = new YarnTestComponentDefinition
+            {
+                ActualVersion = version0,
+                Name = Guid.NewGuid().ToString("N"),
+                RequestedVersion = $"^{version0}",
+                ResolvedVersion = "https://resolved0/a/resolved",
+            };
+
+            var yarnLock = CreateYarnLockV1FileContent(new List { componentA });
+            var (packageJsonName, packageJsonContent, packageJsonPath) = NpmTestUtilities.GetPackageJsonOneRoot(componentA.Name, componentA.RequestedVersion);
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("yarn.lock", yarnLock)
+                                                    .WithFile("package.json", packageJsonContent, new List { "package.json" })
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(1, detectedComponents.Count());
+            Assert.AreEqual(componentA.Name, ((NpmComponent)detectedComponents.Single().Component).Name);
+            Assert.AreEqual(version0, ((NpmComponent)detectedComponents.Single().Component).Version);
+
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                detectedComponents.Single().Component.Id,
+                parentComponent => parentComponent.Name == componentA.Name && parentComponent.Version == version0);
+        }
+
+        [TestMethod]
+        public async Task WellFormedYarnLockV2WithOneComponent_FindsComponent()
+        {
+            var version0 = NewRandomVersion();
+            var componentA = new YarnTestComponentDefinition
+            {
+                ActualVersion = version0,
+                Name = Guid.NewGuid().ToString("N"),
+                RequestedVersion = $"^{version0}",
+                ResolvedVersion = "https://resolved0/a/resolved",
+            };
+
+            var yarnLock = CreateYarnLockV2FileContent(new List { componentA });
+            var (packageJsonName, packageJsonContent, packageJsonPath) = NpmTestUtilities.GetPackageJsonOneRoot(componentA.Name, componentA.RequestedVersion);
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("yarn.lock", yarnLock)
+                                                    .WithFile("package.json", packageJsonContent, new List { "package.json" })
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(1, detectedComponents.Count());
+            Assert.AreEqual(componentA.Name, ((NpmComponent)detectedComponents.Single().Component).Name);
+            Assert.AreEqual(version0, ((NpmComponent)detectedComponents.Single().Component).Version);
+
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                detectedComponents.Single().Component.Id,
+                parentComponent => parentComponent.Name == componentA.Name && parentComponent.Version == version0);
+        }
+
+        [TestMethod]
+        public async Task WellFormedYarnLockV1WithWorkspace_FindsComponent()
+        {
+            var directory = new DirectoryInfo(Path.GetTempPath());
+
+            var version0 = NewRandomVersion();
+            var componentA = new YarnTestComponentDefinition
+            {
+                ActualVersion = version0,
+                RequestedVersion = $"^{version0}",
+                ResolvedVersion = "https://resolved0/a/resolved",
+                Name = Guid.NewGuid().ToString("N"),
+            };
+
+            var componentStream = YarnTestUtilities.GetMockedYarnLockStream("yarn.lock", CreateYarnLockV1FileContent(new List { componentA }));
+
+            var workspaceJson = new
+            {
+                name = "testworkspace",
+                version = "1.0.0",
+                @private = true,
+                workspaces = new[] { "workspace" },
+            };
+
+            var workspaceJsonComponentStream = new ComponentStream { Location = directory.ToString(), Pattern = "package.json", Stream = JsonConvert.SerializeObject(workspaceJson).ToStream() };
+
+            var packageStream = NpmTestUtilities.GetPackageJsonOneRootComponentStream(componentA.Name, componentA.RequestedVersion);
+
+            var detector = new YarnLockComponentDetector
+            {
+                Logger = loggerMock.Object,
+            };
+
+            var mock = new Mock();
+
+            // The initial call to get the yarn.lock file
+            mock.Setup(x => x.GetComponentStreams(It.IsAny(), It.Is>(f => f.Contains("yarn.lock")), It.IsAny(), It.IsAny())).Returns(new List { componentStream });
+
+            // The call to get the package.json that contains workspace information
+            mock.Setup(x => x.GetComponentStreams(It.IsAny(), It.Is>(f => f.Contains("package.json")), It.IsAny(), false)).Returns(new List { workspaceJsonComponentStream });
+
+            // The call to get the package.json that contains actual information
+            mock.Setup(x => x.GetComponentStreams(It.IsAny(), It.IsAny>(), It.IsAny(), true)).Returns(new List { packageStream });
+
+            var directoryWalkerMock = NpmTestUtilities.GetMockDirectoryWalker(new List { componentStream }, new List { packageStream }, directory.FullName, patterns: detector.SearchPatterns, componentRecorder: componentRecorder);
+
+            var (scanResult, _) = await detectorTestUtility
+                                                    .WithObservableDirectoryWalkerFactory(directoryWalkerMock)
+                                                    .WithComponentStreamEnumerableFactory(mock)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(1, detectedComponents.Count());
+            Assert.AreEqual(componentA.Name, ((NpmComponent)detectedComponents.Single().Component).Name);
+            Assert.AreEqual(version0, ((NpmComponent)detectedComponents.Single().Component).Version);
+
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                detectedComponents.Single().Component.Id,
+                parentComponent => parentComponent.Name == componentA.Name && parentComponent.Version == version0);
+        }
+
+        [TestMethod]
+        public async Task WellFormedYarnLockV2WithWorkspace_FindsComponent()
+        {
+            var directory = new DirectoryInfo(Path.GetTempPath());
+
+            var version0 = NewRandomVersion();
+            var componentA = new YarnTestComponentDefinition
+            {
+                ActualVersion = version0,
+                RequestedVersion = $"^{version0}",
+                ResolvedVersion = "https://resolved0/a/resolved",
+                Name = Guid.NewGuid().ToString("N"),
+            };
+
+            var componentStream = YarnTestUtilities.GetMockedYarnLockStream("yarn.lock", CreateYarnLockV2FileContent(new List { componentA }));
+
+            var workspaceJson = new
+            {
+                name = "testworkspace",
+                version = "1.0.0",
+                @private = true,
+                workspaces = new[] { "workspace" },
+            };
+
+            var workspaceJsonComponentStream = new ComponentStream { Location = directory.ToString(), Pattern = "package.json", Stream = JsonConvert.SerializeObject(workspaceJson).ToStream() };
+
+            var packageStream = NpmTestUtilities.GetPackageJsonOneRootComponentStream(componentA.Name, componentA.RequestedVersion);
+
+            var detector = new YarnLockComponentDetector
+            {
+                Logger = loggerMock.Object,
+            };
+
+            var mock = new Mock();
+
+            // The initial call to get the yarn.lock file
+            mock.Setup(x => x.GetComponentStreams(It.IsAny(), It.Is>(f => f.Contains("yarn.lock")), It.IsAny(), It.IsAny())).Returns(new List { componentStream });
+
+            // The call to get the package.json that contains workspace information
+            mock.Setup(x => x.GetComponentStreams(It.IsAny(), It.Is>(f => f.Contains("package.json")), It.IsAny(), false)).Returns(new List { workspaceJsonComponentStream });
+
+            // The call to get the package.json that contains actual information
+            mock.Setup(x => x.GetComponentStreams(It.IsAny(), It.IsAny>(), It.IsAny(), true)).Returns(new List { packageStream });
+
+            var directoryWalkerMock = NpmTestUtilities.GetMockDirectoryWalker(new List { componentStream }, new List { packageStream }, directory.FullName, patterns: detector.SearchPatterns, componentRecorder: componentRecorder);
+
+            var (scanResult, _) = await detectorTestUtility
+                                                    .WithObservableDirectoryWalkerFactory(directoryWalkerMock)
+                                                    .WithComponentStreamEnumerableFactory(mock)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(1, detectedComponents.Count());
+            Assert.AreEqual(componentA.Name, ((NpmComponent)detectedComponents.Single().Component).Name);
+            Assert.AreEqual(version0, ((NpmComponent)detectedComponents.Single().Component).Version);
+
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                detectedComponents.Single().Component.Id,
+                parentComponent => parentComponent.Name == componentA.Name && parentComponent.Version == version0);
+        }
+
+        [TestMethod]
+        public async Task WellFormedYarnLockV1WithWorkspaceAltForm_FindsComponent()
+        {
+            var directory = new DirectoryInfo(Path.GetTempPath());
+
+            var version0 = NewRandomVersion();
+            var componentA = new YarnTestComponentDefinition
+            {
+                ActualVersion = $"\"{version0}\"",
+                RequestedVersion = $"^{version0}",
+                ResolvedVersion = "\"https://resolved0/a/resolved\"",
+                Name = Guid.NewGuid().ToString("N"),
+            };
+
+            var componentStream = YarnTestUtilities.GetMockedYarnLockStream("yarn.lock", CreateYarnLockV1FileContent(new List { componentA }));
+
+            var workspaceJson = new
+            {
+                name = "testworkspace",
+                version = "1.0.0",
+                @private = true,
+                workspaces = new { packages = new[] { "workspace" } },
+            };
+
+            var workspaceJsonComponentStream = new ComponentStream { Location = directory.ToString(), Pattern = "package.json", Stream = JsonConvert.SerializeObject(workspaceJson).ToStream() };
+
+            var packageStream = NpmTestUtilities.GetPackageJsonOneRootComponentStream(componentA.Name, componentA.RequestedVersion);
+
+            var detector = new YarnLockComponentDetector
+            {
+                Logger = loggerMock.Object,
+            };
+
+            var mock = new Mock();
+
+            // The initial call to get the yarn.lock file
+            mock.Setup(x => x.GetComponentStreams(It.IsAny(), It.Is>(f => f.Contains("yarn.lock")), It.IsAny(), It.IsAny())).Returns(new List { componentStream });
+
+            // The call to get the package.json that contains workspace information
+            mock.Setup(x => x.GetComponentStreams(It.IsAny(), It.Is>(f => f.Contains("package.json")), It.IsAny(), false)).Returns(new List { workspaceJsonComponentStream });
+
+            // The call to get the package.json that contains actual information
+            mock.Setup(x => x.GetComponentStreams(It.IsAny(), It.IsAny>(), It.IsAny(), true)).Returns(new List { packageStream });
+
+            var directoryWalkerMock = new Mock();
+
+            directoryWalkerMock.Setup(walker => walker.GetFilteredComponentStreamObservable(It.IsAny(), It.IsAny>(), It.IsAny()))
+                .Returns(() => new IComponentStream[]
+                    {
+                        componentStream,
+                        workspaceJsonComponentStream,
+                        packageStream,
+                    }.Select(cs => new ProcessRequest
+                    {
+                        ComponentStream = cs,
+                        SingleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder(cs.Location),
+                    }).ToObservable());
+
+            var (scanResult, _) = await detectorTestUtility
+                                                    .WithObservableDirectoryWalkerFactory(directoryWalkerMock)
+                                                    .WithComponentStreamEnumerableFactory(mock)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(1, detectedComponents.Count());
+            Assert.AreEqual(componentA.Name, ((NpmComponent)detectedComponents.Single().Component).Name);
+            Assert.AreEqual(version0, ((NpmComponent)detectedComponents.Single().Component).Version);
+
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                detectedComponents.Single().Component.Id,
+                parentComponent => parentComponent.Name == componentA.Name && parentComponent.Version == version0);
+        }
+
+        [TestMethod]
+        public async Task WellFormedYarnLockV2WithWorkspaceAltForm_FindsComponent()
+        {
+            var directory = new DirectoryInfo(Path.GetTempPath());
+
+            var version0 = NewRandomVersion();
+            var componentA = new YarnTestComponentDefinition
+            {
+                ActualVersion = $"\"{version0}\"",
+                RequestedVersion = $"^{version0}",
+                ResolvedVersion = "\"https://resolved0/a/resolved\"",
+                Name = Guid.NewGuid().ToString("N"),
+            };
+
+            var componentStream = YarnTestUtilities.GetMockedYarnLockStream("yarn.lock", CreateYarnLockV2FileContent(new List { componentA }));
+
+            var workspaceJson = new
+            {
+                name = "testworkspace",
+                version = "1.0.0",
+                @private = true,
+                workspaces = new { packages = new[] { "workspace" } },
+            };
+
+            var workspaceJsonComponentStream = new ComponentStream { Location = directory.ToString(), Pattern = "package.json", Stream = JsonConvert.SerializeObject(workspaceJson).ToStream() };
+
+            var packageStream = NpmTestUtilities.GetPackageJsonOneRootComponentStream(componentA.Name, componentA.RequestedVersion);
+
+            var detector = new YarnLockComponentDetector
+            {
+                Logger = loggerMock.Object,
+            };
+
+            var mock = new Mock();
+
+            // The initial call to get the yarn.lock file
+            mock.Setup(x => x.GetComponentStreams(It.IsAny(), It.Is>(f => f.Contains("yarn.lock")), It.IsAny(), It.IsAny())).Returns(new List { componentStream });
+
+            // The call to get the package.json that contains workspace information
+            mock.Setup(x => x.GetComponentStreams(It.IsAny(), It.Is>(f => f.Contains("package.json")), It.IsAny(), false)).Returns(new List { workspaceJsonComponentStream });
+
+            // The call to get the package.json that contains actual information
+            mock.Setup(x => x.GetComponentStreams(It.IsAny(), It.IsAny>(), It.IsAny(), true)).Returns(new List { packageStream });
+
+            var directoryWalkerMock = new Mock();
+
+            directoryWalkerMock.Setup(walker => walker.GetFilteredComponentStreamObservable(It.IsAny(), It.IsAny>(), It.IsAny()))
+                .Returns(() => new IComponentStream[]
+                    {
+                        componentStream,
+                        workspaceJsonComponentStream,
+                        packageStream,
+                    }.Select(cs => new ProcessRequest
+                    {
+                        ComponentStream = cs,
+                        SingleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder(cs.Location),
+                    }).ToObservable());
+
+            var (scanResult, _) = await detectorTestUtility
+                                                    .WithObservableDirectoryWalkerFactory(directoryWalkerMock)
+                                                    .WithComponentStreamEnumerableFactory(mock)
+                                                    .ExecuteDetector();
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            Assert.AreEqual(1, detectedComponents.Count());
+            Assert.AreEqual(componentA.Name, ((NpmComponent)detectedComponents.Single().Component).Name);
+            Assert.AreEqual(version0, ((NpmComponent)detectedComponents.Single().Component).Version);
+
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                detectedComponents.Single().Component.Id,
+                parentComponent => parentComponent.Name == componentA.Name && parentComponent.Version == version0);
+        }
+
+        [TestMethod]
+        public async Task WellFormedYarnLockV1WithMoreThanOneComponent_FindsComponents()
+        {
+            var version0 = NewRandomVersion();
+            var componentA = new YarnTestComponentDefinition
+            {
+                ActualVersion = version0,
+                RequestedVersion = $"^{version0}",
+                ResolvedVersion = "https://resolved0/a/resolved",
+                Name = Guid.NewGuid().ToString("N"),
+            };
+
+            var version1 = NewRandomVersion();
+            var componentB = new YarnTestComponentDefinition
+            {
+                ActualVersion = version1,
+                RequestedVersion = version1,
+                ResolvedVersion = "https://resolved1/a/resolved",
+                Name = Guid.NewGuid().ToString("N"),
+            };
+
+            componentA.Dependencies = new List<(string, string)> { (componentB.Name, componentB.RequestedVersion) };
+
+            var yarnLock = CreateYarnLockV1FileContent(new List { componentA, componentB });
+            var (packageJsonName, packageJsonContent, packageJsonPath) = NpmTestUtilities.GetPackageJsonOneRoot(componentA.Name, componentA.RequestedVersion);
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("yarn.lock", yarnLock)
+                                                    .WithFile("package.json", packageJsonContent, new List { "package.json" })
+                                                    .ExecuteDetector();
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            var component0 = detectedComponents.Select(x => x.Component).Cast().Single(x => x.Name == componentA.Name);
+            var component1 = detectedComponents.Select(x => x.Component).Cast().Single(x => x.Name == componentB.Name);
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(2, detectedComponents.Count());
+
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                component0.Id,
+                parentComponent => parentComponent.Id == component0.Id);
+
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                component1.Id,
+                parentComponent => parentComponent.Id == component0.Id);
+        }
+
+        [TestMethod]
+        public async Task WellFormedYarnLockV2WithMoreThanOneComponent_FindsComponents()
+        {
+            var version0 = NewRandomVersion();
+            var componentA = new YarnTestComponentDefinition
+            {
+                ActualVersion = version0,
+                RequestedVersion = $"^{version0}",
+                ResolvedVersion = "https://resolved0/a/resolved",
+                Name = Guid.NewGuid().ToString("N"),
+            };
+
+            var version1 = NewRandomVersion();
+            var componentB = new YarnTestComponentDefinition
+            {
+                ActualVersion = version1,
+                RequestedVersion = version1,
+                ResolvedVersion = "https://resolved1/a/resolved",
+                Name = Guid.NewGuid().ToString("N"),
+            };
+
+            componentA.Dependencies = new List<(string, string)> { (componentB.Name, componentB.RequestedVersion) };
+
+            var yarnLock = CreateYarnLockV2FileContent(new List { componentA, componentB });
+            var (packageJsonName, packageJsonContent, packageJsonPath) = NpmTestUtilities.GetPackageJsonOneRoot(componentA.Name, componentA.RequestedVersion);
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("yarn.lock", yarnLock)
+                                                    .WithFile("package.json", packageJsonContent, new List { "package.json" })
+                                                    .ExecuteDetector();
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            var component0 = detectedComponents.Select(x => x.Component).Cast().Single(x => x.Name == componentA.Name);
+            var component1 = detectedComponents.Select(x => x.Component).Cast().Single(x => x.Name == componentB.Name);
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(2, detectedComponents.Count());
+
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                component0.Id,
+                parentComponent => parentComponent.Id == component0.Id);
+
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                component1.Id,
+                parentComponent => parentComponent.Id == component0.Id);
+        }
+
+        [TestMethod]
+        public async Task WellFormedYarnLockV1WithMultiRootedComponent_FindsAllRoots()
+        {
+            // This is a regression test for a bug where a dependency is both an explicitly referenced root and a transitive dependency.
+            //
+            // component A is a root dependency
+            // component B is a root devDependency
+            // component A depends on component B
+            //
+            // we expect A to be detected as a non-dev dependency with roots [A]
+            // we expect B to be detected as a non-dev dependency with roots [A, B]
+            var componentNameA = "component-a";
+            var actualVersionA = "1.1.1";
+            var requestedVersionA = $"^{actualVersionA}";
+            var resolvedA = "https://resolved0/a/resolved";
+
+            var componentNameB = "root-dev-dependency-component";
+            var actualVersionB = "2.2.2";
+            var resolvedB = "https://resolved1/a/resolved";
+            var requestedVersionB1 = $"^{actualVersionB}";
+            var requestedVersionB2 = $"~{actualVersionB}";
+
+            var packageJsonContent = $@"{{
+                ""name"": ""test"",
+                ""version"": ""0.0.0"",
+                ""dependencies"": {{
+                    ""{componentNameA}"": ""{requestedVersionA}""
+                }},
+                ""devDependencies"": {{
+                    ""{componentNameB}"": ""{requestedVersionB1}""
+                }}
+            }}";
+            var packageJson = packageJsonContent;
+
+            var builder = new StringBuilder();
+
+            builder.AppendLine("# THIS IS A YARNFILE");
+            builder.AppendLine("# yarn lockfile v1");
+            builder.AppendLine();
+            builder.AppendLine($"{componentNameA}@{requestedVersionA}:");
+            builder.AppendLine($"  version \"{actualVersionA}\"");
+            builder.AppendLine($"  resolved \"{resolvedA}\"");
+            builder.AppendLine($"  dependencies:");
+            builder.AppendLine($"    {componentNameB} \"{requestedVersionB2}\"");
+            builder.AppendLine();
+            builder.AppendLine($"{componentNameB}@{requestedVersionB1}, {componentNameB}@{requestedVersionB2}:");
+            builder.AppendLine($"  version \"{actualVersionB}\"");
+            builder.AppendLine($"  resolved \"{resolvedB}\"");
+            builder.AppendLine();
+
+            var yarnLock = builder.ToString();
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("yarn.lock", yarnLock)
+                                                    .WithFile("package.json", packageJsonContent, new List { "package.json" })
+                                                    .ExecuteDetector();
+
+            var detectedComponentes = componentRecorder.GetDetectedComponents();
+            var componentA = detectedComponentes.Single(x => ((NpmComponent)x.Component).Name == componentNameA);
+            var componentB = detectedComponentes.Single(x => ((NpmComponent)x.Component).Name == componentNameB);
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(2, detectedComponentes.Count());
+
+            // Component A
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                componentA.Component.Id,
+                parentComponent => parentComponent.Id == componentA.Component.Id);
+            Assert.AreEqual(false, componentRecorder.GetEffectiveDevDependencyValue(componentA.Component.Id));
+
+            // Component B
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                componentB.Component.Id,
+                parentComponent1 => parentComponent1.Id == componentA.Component.Id,
+                parentComponent2 => parentComponent2.Id == componentB.Component.Id);
+            Assert.AreEqual(false, componentRecorder.GetEffectiveDevDependencyValue(componentB.Component.Id));
+        }
+
+        [TestMethod]
+        public async Task WellFormedYarnLockV2WithMultiRootedComponent_FindsAllRoots()
+        {
+            // This is a regression test for a bug where a dependency is both an explicitly referenced root and a transitive dependency.
+            //
+            // component A is a root dependency
+            // component B is a root devDependency
+            // component A depends on component B
+            //
+            // we expect A to be detected as a non-dev dependency with roots [A]
+            // we expect B to be detected as a non-dev dependency with roots [A, B]
+            var componentNameA = "component-a";
+            var actualVersionA = "1.1.1";
+            var requestedVersionA = $"^{actualVersionA}";
+            var resolvedA = "https://resolved0/a/resolved";
+
+            var componentNameB = "root-dev-dependency-component";
+            var actualVersionB = "2.2.2";
+            var resolvedB = "https://resolved1/a/resolved";
+            var requestedVersionB1 = $"^{actualVersionB}";
+            var requestedVersionB2 = $"~{actualVersionB}";
+
+            var packageJsonContent = $@"{{
+                ""name"": ""test"",
+                ""version"": ""0.0.0"",
+                ""dependencies"": {{
+                    ""{componentNameA}"": ""{requestedVersionA}""
+                }},
+                ""devDependencies"": {{
+                    ""{componentNameB}"": ""{requestedVersionB1}""
+                }}
+            }}";
+            var packageJson = packageJsonContent;
+
+            var builder = new StringBuilder();
+
+            builder.AppendLine(CreateYarnLockV2FileContent(new List()));
+            builder.AppendLine($"{componentNameA}@{requestedVersionA}:");
+            builder.AppendLine($"  version: {actualVersionA}");
+            builder.AppendLine($"  resolved: {resolvedA}");
+            builder.AppendLine($"  dependencies:");
+            builder.AppendLine($"    {componentNameB}: {requestedVersionB2}");
+            builder.AppendLine();
+            builder.AppendLine($"{componentNameB}@{requestedVersionB1}, {componentNameB}@{requestedVersionB2}:");
+            builder.AppendLine($"  version: {actualVersionB}");
+            builder.AppendLine($"  resolved: {resolvedB}");
+            builder.AppendLine();
+
+            var yarnLock = builder.ToString();
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("yarn.lock", yarnLock)
+                                                    .WithFile("package.json", packageJsonContent, new List { "package.json" })
+                                                    .ExecuteDetector();
+
+            var detectedComponentes = componentRecorder.GetDetectedComponents();
+            var componentA = detectedComponentes.Single(x => ((NpmComponent)x.Component).Name == componentNameA);
+            var componentB = detectedComponentes.Single(x => ((NpmComponent)x.Component).Name == componentNameB);
+
+            Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
+            Assert.AreEqual(2, detectedComponentes.Count());
+
+            // Component A
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                componentA.Component.Id,
+                parentComponent => parentComponent.Id == componentA.Component.Id);
+            Assert.AreEqual(false, componentRecorder.GetEffectiveDevDependencyValue(componentA.Component.Id));
+
+            // Component B
+            componentRecorder.AssertAllExplicitlyReferencedComponents(
+                componentB.Component.Id,
+                parentComponent1 => parentComponent1.Id == componentA.Component.Id,
+                parentComponent2 => parentComponent2.Id == componentB.Component.Id);
+            Assert.AreEqual(false, componentRecorder.GetEffectiveDevDependencyValue(componentB.Component.Id));
+        }
+
+        [TestMethod]
+        public async Task DependencyGraphV1IsGeneratedCorrectly()
+        {
+            var componentA = new YarnTestComponentDefinition
+            {
+                Name = "component-a",
+                ActualVersion = "1.1.1",
+                RequestedVersion = "^1.1.1",
+                ResolvedVersion = "https://resolved0/b/resolved",
+            };
+
+            var componentB = new YarnTestComponentDefinition
+            {
+                Name = "component-b",
+                ActualVersion = "1.1.1",
+                RequestedVersion = "^1.1.1",
+                ResolvedVersion = "https://resolved0/c/resolved",
+            };
+
+            var componentC = new YarnTestComponentDefinition
+            {
+                Name = "component-c",
+                ActualVersion = "1.1.1",
+                RequestedVersion = "^1.1.1",
+                ResolvedVersion = "https://resolved0/d/resolved",
+            };
+
+            componentA.Dependencies = new List<(string, string)> { (componentB.Name, componentB.RequestedVersion) };
+            componentB.Dependencies = new List<(string, string)> { (componentC.Name, componentC.RequestedVersion) };
+
+            var yarnLockFileContent = CreateYarnLockV1FileContent(new List { componentA, componentB, componentC });
+            var packageJsonFileContent = CreatePackageJsonFileContent(new List { componentA, componentB, componentC });
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("yarn.lock", yarnLockFileContent)
+                                                    .WithFile("package.json", packageJsonFileContent, new List { "package.json" })
+                                                    .ExecuteDetector();
+
+            scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            var componentAId = detectedComponents.First(c => ((NpmComponent)c.Component).Name == componentA.Name).Component.Id;
+            var componentBId = detectedComponents.First(c => ((NpmComponent)c.Component).Name == componentB.Name).Component.Id;
+            var componentCId = detectedComponents.First(c => ((NpmComponent)c.Component).Name == componentC.Name).Component.Id;
+
+            var dependencyGraph = componentRecorder.GetDependencyGraphsByLocation().Values.First();
+
+            dependencyGraph.GetDependenciesForComponent(componentAId).Should().HaveCount(1);
+            dependencyGraph.GetDependenciesForComponent(componentAId).Should().Contain(componentBId);
+
+            dependencyGraph.GetDependenciesForComponent(componentBId).Should().HaveCount(1);
+            dependencyGraph.GetDependenciesForComponent(componentBId).Should().Contain(componentCId);
+
+            dependencyGraph.GetDependenciesForComponent(componentCId).Should().HaveCount(0);
+        }
+
+        [TestMethod]
+        public async Task DependencyGraphV2IsGeneratedCorrectly()
+        {
+            var componentA = new YarnTestComponentDefinition
+            {
+                Name = "component-a",
+                ActualVersion = "1.1.1",
+                RequestedVersion = "^1.1.1",
+                ResolvedVersion = "https://resolved0/b/resolved",
+            };
+
+            var componentB = new YarnTestComponentDefinition
+            {
+                Name = "component-b",
+                ActualVersion = "1.1.1",
+                RequestedVersion = "^1.1.1",
+                ResolvedVersion = "https://resolved0/c/resolved",
+            };
+
+            var componentC = new YarnTestComponentDefinition
+            {
+                Name = "component-c",
+                ActualVersion = "1.1.1",
+                RequestedVersion = "^1.1.1",
+                ResolvedVersion = "https://resolved0/d/resolved",
+            };
+
+            componentA.Dependencies = new List<(string, string)> { (componentB.Name, componentB.RequestedVersion) };
+            componentB.Dependencies = new List<(string, string)> { (componentC.Name, componentC.RequestedVersion) };
+
+            var yarnLockFileContent = CreateYarnLockV2FileContent(new List { componentA, componentB, componentC });
+            var packageJsonFileContent = CreatePackageJsonFileContent(new List { componentA, componentB, componentC });
+
+            var (scanResult, componentRecorder) = await detectorTestUtility
+                                                    .WithFile("yarn.lock", yarnLockFileContent)
+                                                    .WithFile("package.json", packageJsonFileContent, new List { "package.json" })
+                                                    .ExecuteDetector();
+
+            scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+
+            var detectedComponents = componentRecorder.GetDetectedComponents();
+            var componentAId = detectedComponents.First(c => ((NpmComponent)c.Component).Name == componentA.Name).Component.Id;
+            var componentBId = detectedComponents.First(c => ((NpmComponent)c.Component).Name == componentB.Name).Component.Id;
+            var componentCId = detectedComponents.First(c => ((NpmComponent)c.Component).Name == componentC.Name).Component.Id;
+
+            var dependencyGraph = componentRecorder.GetDependencyGraphsByLocation().Values.First();
+
+            dependencyGraph.GetDependenciesForComponent(componentAId).Should().HaveCount(1);
+            dependencyGraph.GetDependenciesForComponent(componentAId).Should().Contain(componentBId);
+
+            dependencyGraph.GetDependenciesForComponent(componentBId).Should().HaveCount(1);
+            dependencyGraph.GetDependenciesForComponent(componentBId).Should().Contain(componentCId);
+
+            dependencyGraph.GetDependenciesForComponent(componentCId).Should().HaveCount(0);
+        }
+
+        private string CreatePackageJsonFileContent(IList components)
+        {
+            var builder = new StringBuilder();
+            builder.Append("{");
+            builder.Append(@"""name"": ""test"",");
+            builder.Append(@"""version"": ""0.0.0"",");
+            builder.Append(@"""dependencies"": {");
+
+            var prodComponents = components.Where(c => !c.IsDevDependency).ToList();
+            for (var i = 0; i < prodComponents.Count(); i++)
+            {
+                if (i == prodComponents.Count() - 1)
+                {
+                    builder.Append($@"  ""{prodComponents[i].Name}"": ""{prodComponents[i].RequestedVersion}""");
+                }
+                else
+                {
+                    builder.Append($@"  ""{prodComponents[i].Name}"": ""{prodComponents[i].RequestedVersion}"",");
+                }
+            }
+
+            builder.Append("}");
+
+            if (components.Any(component => component.IsDevDependency))
+            {
+                builder.Append(",");
+                builder.Append(@"""devDependencies"": {");
+
+                var dependencyComponents = components.Where(c => c.IsDevDependency).ToList();
+
+                for (var i = 0; i < dependencyComponents.Count(); i++)
+                {
+                    if (i == dependencyComponents.Count() - 1)
+                    {
+                        builder.Append($@"  ""{dependencyComponents[i].Name}"": ""{dependencyComponents[i].RequestedVersion}""");
+                    }
+                    else
+                    {
+                        builder.Append($@"  ""{dependencyComponents[i].Name}"": ""{dependencyComponents[i].RequestedVersion}"",");
+                    }
+                }
+            }
+
+            builder.Append("}");
+            builder.Append("}");
+
+            return builder.ToString();
+        }
+
+        private string CreateYarnLockV1FileContent(IEnumerable components)
+        {
+            var builder = new StringBuilder();
+
+            builder.AppendLine("# yarn lockfile v1");
+            builder.AppendLine();
+
+            foreach (var component in components)
+            {
+                builder.AppendLine($"{component.Name}@{component.RequestedVersion}:");
+                builder.AppendLine($"  version \"{component.ActualVersion}\"");
+                builder.AppendLine($"  resolved \"{component.ResolvedVersion}\"");
+
+                if (component.Dependencies.Any())
+                {
+                    builder.AppendLine($"  dependencies:");
+                    foreach (var dependency in component.Dependencies)
+                    {
+                        builder.AppendLine($"    {dependency.Name} \"{dependency.RequestedVersion}\"");
+                    }
+                }
+
+                builder.AppendLine();
+            }
+
+            return builder.ToString();
+        }
+
+        private string CreateYarnLockV2FileContent(IEnumerable components)
+        {
+            var builder = new StringBuilder();
+
+            builder.AppendLine("# This file is generated by running \"yarn install\" inside your project.");
+            builder.AppendLine("# Manual changes might be lost - proceed with caution!");
+            builder.AppendLine();
+            builder.AppendLine("__metadata:");
+            builder.AppendLine("  version: 4");
+            builder.AppendLine("  cacheKey: 7");
+            builder.AppendLine();
+
+            foreach (var component in components)
+            {
+                builder.AppendLine($"{component.Name}@{component.RequestedVersion}:");
+                builder.AppendLine($"  version: {component.ActualVersion}");
+                builder.AppendLine($"  resolved: {component.ResolvedVersion}");
+
+                if (component.Dependencies.Any())
+                {
+                    builder.AppendLine($"  dependencies:");
+                    foreach (var dependency in component.Dependencies)
+                    {
+                        builder.AppendLine($"    {dependency.Name}: {dependency.RequestedVersion}");
+                    }
+                }
+
+                builder.AppendLine();
+            }
+
+            return builder.ToString();
+        }
+
+        private class YarnTestComponentDefinition
+        {
+            public string Name { get; set; }
+
+            public string RequestedVersion { get; set; }
+
+            public string ActualVersion { get; set; }
+
+            public string ResolvedVersion { get; set; }
+
+            public bool IsDevDependency { get; set; }
+
+            public IList<(string Name, string RequestedVersion)> Dependencies { get; set; } = new List<(string, string)>();
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/YarnParserTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/YarnParserTests.cs
new file mode 100644
index 000000000..a0039075a
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/YarnParserTests.cs
@@ -0,0 +1,189 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Detectors.Yarn;
+using Microsoft.ComponentDetection.Detectors.Yarn.Parsers;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class YarnParserTests
+    {
+        private Mock loggerMock;
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            loggerMock = new Mock();
+        }
+
+        [TestMethod]
+        public void YarnLockParserWithNullBlockFile_Fails()
+        {
+            var parser = new YarnLockParser();
+
+            Action action = () => parser.Parse(null, loggerMock.Object);
+
+            Assert.ThrowsException(action);
+        }
+
+        [TestMethod]
+        public void YarnLockParser_CanParseV1LockFiles()
+        {
+            var yarnLockFileVersion = YarnLockVersion.V1;
+
+            var parser = new YarnLockParser();
+
+            var blockFile = new Mock();
+            blockFile.Setup(x => x.YarnLockVersion).Returns(yarnLockFileVersion);
+
+            Assert.IsTrue(parser.CanParse(blockFile.Object.YarnLockVersion));
+        }
+
+        [TestMethod]
+        public void YarnLockParser_CanParseV2LockFiles()
+        {
+            var yarnLockFileVersion = YarnLockVersion.V2;
+
+            var parser = new YarnLockParser();
+
+            var blockFile = new Mock();
+            blockFile.Setup(x => x.YarnLockVersion).Returns(yarnLockFileVersion);
+
+            Assert.IsTrue(parser.CanParse(blockFile.Object.YarnLockVersion));
+        }
+
+        [TestMethod]
+        public void YarnLockParser_ParsesEmptyFile()
+        {
+            var yarnLockFileVersion = YarnLockVersion.V1;
+
+            var parser = new YarnLockParser();
+
+            var blocks = Enumerable.Empty();
+            var blockFile = new Mock();
+            blockFile.Setup(x => x.YarnLockVersion).Returns(yarnLockFileVersion);
+            blockFile.Setup(x => x.GetEnumerator()).Returns(blocks.GetEnumerator());
+
+            var file = parser.Parse(blockFile.Object, loggerMock.Object);
+
+            Assert.AreEqual(YarnLockVersion.V1, file.LockVersion);
+            Assert.AreEqual(0, file.Entries.Count());
+        }
+
+        [TestMethod]
+        public void YarnLockParser_ParsesBlocks()
+        {
+            var yarnLockFileVersion = YarnLockVersion.V1;
+
+            var parser = new YarnLockParser();
+
+            var blocks = new List
+            {
+                CreateBlock("a@^1.0.0", "1.0.0", "https://a", new List { CreateDependencyBlock(new Dictionary { { "xyz", "2" } }) }),
+                CreateBlock("b@2.4.6", "2.4.6", "https://b", new List { CreateDependencyBlock(new Dictionary { { "xyz", "2.4" }, { "a", "^1.0.0" } }) }),
+                CreateBlock("xyz@2, xyz@2.4", "2.4.3", "https://xyz", Enumerable.Empty()),
+            };
+
+            var blockFile = new Mock();
+            blockFile.Setup(x => x.YarnLockVersion).Returns(yarnLockFileVersion);
+            blockFile.Setup(x => x.GetEnumerator()).Returns(blocks.GetEnumerator());
+
+            var file = parser.Parse(blockFile.Object, loggerMock.Object);
+
+            Assert.AreEqual(YarnLockVersion.V1, file.LockVersion);
+            Assert.AreEqual(3, file.Entries.Count());
+
+            foreach (var entry in file.Entries)
+            {
+                YarnBlock block = blocks.Single(x => x.Values["resolved"] == entry.Resolved);
+
+                AssertBlockMatchesEntry(block, entry);
+            }
+        }
+
+        [TestMethod]
+        public void YarnLockParser_ParsesNoVersionInTitleBlock()
+        {
+            var yarnLockFileVersion = YarnLockVersion.V1;
+
+            var parser = new YarnLockParser();
+
+            var blocks = new List
+            {
+                CreateBlock("a", "1.0.0", "https://a", new List { CreateDependencyBlock(new Dictionary { { "xyz", "2" } }) }),
+                CreateBlock("b", "2.4.6", "https://b", new List { CreateDependencyBlock(new Dictionary { { "xyz", "2.4" }, { "a", "^1.0.0" } }) }),
+            };
+
+            var blockFile = new Mock();
+            blockFile.Setup(x => x.YarnLockVersion).Returns(yarnLockFileVersion);
+            blockFile.Setup(x => x.GetEnumerator()).Returns(blocks.GetEnumerator());
+
+            var file = parser.Parse(blockFile.Object, loggerMock.Object);
+
+            Assert.AreEqual(YarnLockVersion.V1, file.LockVersion);
+            Assert.AreEqual(2, file.Entries.Count());
+
+            Assert.IsNotNull(file.Entries.FirstOrDefault(x => x.LookupKey == "a@1.0.0"));
+            Assert.IsNotNull(file.Entries.FirstOrDefault(x => x.LookupKey == "b@2.4.6"));
+        }
+
+        private YarnBlock CreateDependencyBlock(IDictionary dependencies)
+        {
+            var block = new YarnBlock { Title = "dependencies" };
+
+            foreach (var item in dependencies)
+            {
+                var version = YarnLockParser.NormalizeVersion(item.Value);
+                block.Values[item.Key] = version;
+            }
+
+            return block;
+        }
+
+        private YarnBlock CreateBlock(string title, string version, string resolved, IEnumerable dependencies)
+        {
+            var block = new YarnBlock { Title = title };
+            block.Values["version"] = version;
+            block.Values["resolved"] = resolved;
+
+            foreach (var dependency in dependencies)
+            {
+                block.Children.Add(dependency);
+            }
+
+            return block;
+        }
+
+        private void AssertBlockMatchesEntry(YarnBlock block, YarnEntry entry)
+        {
+            var componentName = block.Title.Split(',').Select(x => x.Trim()).First().Split('@')[0];
+            var blockVersions = block.Title.Split(',').Select(x => x.Trim()).Select(x => x.Split('@')[1]);
+
+            Assert.AreEqual(componentName, entry.Name);
+
+            foreach (var version in blockVersions)
+            {
+                Assert.IsTrue(entry.Satisfied.Contains(YarnLockParser.NormalizeVersion(version)));
+            }
+
+            Assert.AreEqual(block.Values["version"], entry.Version);
+            Assert.AreEqual(block.Values["resolved"], entry.Resolved);
+
+            var dependencies = block.Children.SingleOrDefault(x => x.Title == "dependencies");
+
+            if (dependencies != null)
+            {
+                foreach (var dependency in dependencies.Values)
+                {
+                    Assert.IsNotNull(entry.Dependencies.SingleOrDefault(x => x.Name == dependency.Key && x.Version == dependency.Value));
+                }
+            }
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/YarnTestUtilities.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/YarnTestUtilities.cs
new file mode 100644
index 000000000..442405a35
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/YarnTestUtilities.cs
@@ -0,0 +1,46 @@
+using System.IO;
+using System.Text;
+using Microsoft.ComponentDetection.Contracts;
+using Moq;
+using Microsoft.ComponentDetection.TestsUtilities;
+
+namespace Microsoft.ComponentDetection.Detectors.Tests
+{
+    public static class YarnTestUtilities
+    {
+        public static string GetWellFormedEmptyYarnV1LockFile()
+        {
+            StringBuilder builder = new StringBuilder();
+
+            builder.AppendLine("# THIS IS A YARNFILE");
+            builder.AppendLine("# yarn lockfile v1");
+            builder.AppendLine();
+
+            return builder.ToString();
+        }
+
+        public static string GetWellFormedEmptyYarnV2LockFile()
+        {
+            StringBuilder builder = new StringBuilder();
+
+            builder.AppendLine("# THIS IS A YARNFILE");
+            builder.AppendLine();
+            builder.AppendLine("__metadata:");
+            builder.AppendLine("  version: 4");
+            builder.AppendLine("  cacheKey: 7");
+            builder.AppendLine();
+
+            return builder.ToString();
+        }
+
+        public static IComponentStream GetMockedYarnLockStream(string lockFileName, string content)
+        {
+            var packageLockMock = new Mock();
+            packageLockMock.SetupGet(x => x.Stream).Returns(content.ToString().ToStream());
+            packageLockMock.SetupGet(x => x.Pattern).Returns(lockFileName);
+            packageLockMock.SetupGet(x => x.Location).Returns(Path.Combine(Path.GetTempPath(), lockFileName));
+
+            return packageLockMock.Object;
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Orchestrator.Tests/GraphTranslationUtilityTests.cs b/test/Microsoft.ComponentDetection.Orchestrator.Tests/GraphTranslationUtilityTests.cs
new file mode 100644
index 000000000..ca725d839
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Orchestrator.Tests/GraphTranslationUtilityTests.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using System.Linq;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Common.DependencyGraph;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Orchestrator.Services.GraphTranslation;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Tests
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class GraphTranslationUtilityTests
+    {
+        [TestMethod]
+        public void Test_AccumulateAndConvertToContract()
+        {
+            var componentRecorder = new ComponentRecorder();
+
+            var file1 = componentRecorder.CreateSingleFileComponentRecorder("file1.json");
+            file1.RegisterUsage(new DetectedComponent(new NpmComponent("webpack", "1.0.0")));
+
+            var file2 = componentRecorder.CreateSingleFileComponentRecorder("file2.json");
+            file2.RegisterUsage(new DetectedComponent(new NpmComponent("webpack", "2.0.0")), isExplicitReferencedDependency: true);
+
+            var dependencyGraphs = new List>() { componentRecorder.GetDependencyGraphsByLocation() };
+
+            var convertedGraphContract = GraphTranslationUtility.AccumulateAndConvertToContract(dependencyGraphs);
+
+            convertedGraphContract.Count().Should().Be(2);
+            convertedGraphContract.Keys.Should().BeEquivalentTo(new List() { "file1.json", "file2.json" });
+
+            var graph1 = convertedGraphContract["file1.json"];
+            graph1.ExplicitlyReferencedComponentIds.Should().BeEmpty();
+            graph1.Graph.Keys.Should().BeEquivalentTo(new List() { "webpack 1.0.0 - Npm" });
+            graph1.DevelopmentDependencies.Should().BeEmpty();
+
+            var graph2 = convertedGraphContract["file2.json"];
+            graph2.ExplicitlyReferencedComponentIds.Should().BeEquivalentTo(new List() { "webpack 2.0.0 - Npm" });
+            graph2.Graph.Keys.Should().BeEquivalentTo(new List() { "webpack 2.0.0 - Npm" });
+            graph2.DevelopmentDependencies.Should().BeEmpty();
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Microsoft.ComponentDetection.Orchestrator.Tests.csproj b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Microsoft.ComponentDetection.Orchestrator.Tests.csproj
new file mode 100644
index 000000000..07f1cb5ff
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Microsoft.ComponentDetection.Orchestrator.Tests.csproj
@@ -0,0 +1,16 @@
+
+
+    
+      AnyCPU
+    
+
+    
+      AnyCPU
+    
+
+    
+        
+        
+    
+
+
diff --git a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/BcdeDevCommandServiceTests.cs b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/BcdeDevCommandServiceTests.cs
new file mode 100644
index 000000000..3e4455fb8
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/BcdeDevCommandServiceTests.cs
@@ -0,0 +1,65 @@
+using System.Collections.Generic;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.BcdeModels;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Orchestrator.ArgumentSets;
+using Microsoft.ComponentDetection.Orchestrator.Services;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Tests.Services
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class BcdeDevCommandServiceTests
+    {
+        private Mock scanExecutionServiceMock;
+
+        private BcdeDevCommandService serviceUnderTest;
+
+        private ScannedComponent[] scannedComponents;
+
+        [TestInitialize]
+        public void InitializeTest()
+        {
+            scanExecutionServiceMock = new Mock();
+            serviceUnderTest = new BcdeDevCommandService();
+
+            scannedComponents = new ScannedComponent[]
+            {
+                new ScannedComponent
+                {
+                    Component = new NpmComponent("some-npm-component", "1.2.3"),
+                    IsDevelopmentDependency = false,
+                },
+            };
+
+            var executeScanAsyncResult = new ScanResult
+            {
+                DetectorsInScan = new List(),
+                ComponentsFound = scannedComponents,
+                ContainerDetailsMap = new Dictionary(),
+                ResultCode = ProcessingResultCode.Success,
+            };
+
+            scanExecutionServiceMock.Setup(x => x.ExecuteScanAsync(It.IsAny()))
+                .ReturnsAsync(executeScanAsyncResult);
+        }
+
+        [TestMethod]
+        public void RunComponentDetection()
+        {
+            var args = new BcdeArguments();
+
+            serviceUnderTest = new BcdeDevCommandService
+            {
+                BcdeScanExecutionService = scanExecutionServiceMock.Object,
+            };
+
+            var result = serviceUnderTest.Handle(args);
+            result.Result.ResultCode.Should().Be(ProcessingResultCode.Success);
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/BcdeScanExecutionServiceTests.cs b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/BcdeScanExecutionServiceTests.cs
new file mode 100644
index 000000000..e8455a320
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/BcdeScanExecutionServiceTests.cs
@@ -0,0 +1,696 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Common.DependencyGraph;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.BcdeModels;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Detectors.Npm;
+using Microsoft.ComponentDetection.Detectors.Pip;
+using Microsoft.ComponentDetection.Orchestrator.ArgumentSets;
+using Microsoft.ComponentDetection.Orchestrator.Services;
+using Microsoft.ComponentDetection.Orchestrator.Services.GraphTranslation;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Tests.Services
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class BcdeScanExecutionServiceTests
+    {
+        private Mock loggerMock;
+        private Mock detectorProcessingServiceMock;
+        private Mock detectorRegistryServiceMock;
+        private Mock detectorRestrictionServiceMock;
+        private Mock componentDetector2Mock;
+        private Mock componentDetector3Mock;
+        private Mock versionedComponentDetector1Mock;
+
+        private DetectedComponent[] detectedComponents;
+        private ContainerDetails sampleContainerDetails;
+
+        private BcdeScanExecutionService serviceUnderTest;
+
+        private DirectoryInfo sourceDirectory;
+        private Dictionary contentByFileInfo;
+
+        [TestInitialize]
+        public void InitializeTest()
+        {
+            loggerMock = new Mock();
+            detectorProcessingServiceMock = new Mock();
+            detectorRegistryServiceMock = new Mock();
+            detectorRestrictionServiceMock = new Mock();
+            componentDetector2Mock = new Mock();
+            componentDetector3Mock = new Mock();
+            versionedComponentDetector1Mock = new Mock();
+            sampleContainerDetails = new ContainerDetails { Id = 1 };
+            var defaultGraphTranslationService = new DefaultGraphTranslationService
+            {
+                Logger = loggerMock.Object,
+            };
+
+            contentByFileInfo = new Dictionary();
+
+            detectedComponents = new[]
+            {
+                new DetectedComponent(new NpmComponent("some-npm-component", "1.2.3")),
+                new DetectedComponent(new NuGetComponent("SomeNugetComponent", "1.2.3.4")),
+            };
+
+            serviceUnderTest = new BcdeScanExecutionService
+            {
+                DetectorProcessingService = detectorProcessingServiceMock.Object,
+                DetectorRegistryService = detectorRegistryServiceMock.Object,
+                DetectorRestrictionService = detectorRestrictionServiceMock.Object,
+                Logger = loggerMock.Object,
+                GraphTranslationServices = new List>
+                {
+                    new Lazy(() => defaultGraphTranslationService, new GraphTranslationServiceMetadata()),
+                },
+            };
+
+            sourceDirectory = new DirectoryInfo(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()));
+
+            sourceDirectory.Create();
+        }
+
+        [TestCleanup]
+        public void CleanupTests()
+        {
+            detectorProcessingServiceMock.VerifyAll();
+            detectorRegistryServiceMock.VerifyAll();
+            detectorRestrictionServiceMock.VerifyAll();
+
+            try
+            {
+                sourceDirectory.Delete(true);
+            }
+            catch
+            {
+            }
+        }
+
+        [TestMethod]
+        public void DetectComponents_HappyPath()
+        {
+            var componentRecorder = new ComponentRecorder();
+            var singleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder(Path.Join(sourceDirectory.FullName, "/some/file/path"));
+
+            componentDetector2Mock.SetupGet(x => x.Id).Returns("Detector2");
+            componentDetector2Mock.SetupGet(x => x.Version).Returns(1);
+            componentDetector3Mock.SetupGet(x => x.Id).Returns("Detector3");
+            componentDetector3Mock.SetupGet(x => x.Version).Returns(10);
+
+            detectedComponents[0].DevelopmentDependency = true;
+            detectedComponents[0].ContainerDetailIds = new HashSet { sampleContainerDetails.Id };
+            singleFileComponentRecorder.RegisterUsage(detectedComponents[0], isDevelopmentDependency: true);
+
+            var parentPipComponent = new PipComponent("sample-root", "1.0");
+            detectedComponents[1].DependencyRoots = new HashSet(new[] { parentPipComponent });
+            detectedComponents[1].DevelopmentDependency = null;
+            singleFileComponentRecorder.RegisterUsage(new DetectedComponent(parentPipComponent, detector: new PipComponentDetector()), isExplicitReferencedDependency: true);
+            singleFileComponentRecorder.RegisterUsage(detectedComponents[1], parentComponentId: parentPipComponent.Id);
+
+            var args = new BcdeArguments
+            {
+                AdditionalPluginDirectories = Enumerable.Empty(),
+                SourceDirectory = sourceDirectory,
+            };
+            var result = DetectComponentsHappyPath(args, restrictions =>
+            {
+                restrictions.AllowedDetectorCategories.Should().BeNull();
+                restrictions.AllowedDetectorIds.Should().BeNull();
+            }, new List { componentRecorder });
+
+            result.Result.Should().Be(ProcessingResultCode.Success);
+            ValidateDetectedComponents(result.DetectedComponents);
+            result.DetectorsInRun.Count().Should().Be(2);
+            var detector2 = result.DetectorsInRun.Single(x => x.DetectorId == "Detector2");
+            detector2.Version.Should().Be(1);
+            var detector3 = result.DetectorsInRun.Single(x => x.DetectorId == "Detector3");
+            detector3.Version.Should().Be(10);
+
+            var npmComponent = result.DetectedComponents.Single(x => x.Component is NpmComponent);
+            npmComponent.LocationsFoundAt.Count().Should().Be(1);
+            npmComponent.LocationsFoundAt.First().Should().Be("/some/file/path");
+            npmComponent.IsDevelopmentDependency.Should().Be(true);
+            npmComponent.ContainerDetailIds.Contains(1).Should().Be(true);
+
+            var nugetComponent = result.DetectedComponents.Single(x => x.Component is NuGetComponent);
+            nugetComponent.TopLevelReferrers.Count().Should().Be(1);
+            (nugetComponent.TopLevelReferrers.First() as PipComponent).Name.Should().Be("sample-root");
+            nugetComponent.IsDevelopmentDependency.Should().Be(null);
+        }
+
+        [TestMethod]
+        public void DetectComponents_DetectOnlyWithIdAndCategoryRestrictions()
+        {
+            var args = new BcdeArguments
+            {
+                DetectorCategories = new[] { "Category1", "Category2" },
+                DetectorsFilter = new[] { "Detector1", "Detector2" },
+                AdditionalPluginDirectories = Enumerable.Empty(),
+                SourceDirectory = sourceDirectory,
+            };
+
+            var componentRecorder = new ComponentRecorder();
+            var singleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder("/location");
+            singleFileComponentRecorder.RegisterUsage(detectedComponents[0]);
+            singleFileComponentRecorder.RegisterUsage(detectedComponents[1]);
+
+            var result = DetectComponentsHappyPath(args, restrictions =>
+            {
+                restrictions.AllowedDetectorCategories.Should().Contain(args.DetectorCategories);
+                restrictions.AllowedDetectorIds.Should().Contain(args.DetectorsFilter);
+            }, new List { componentRecorder });
+
+            result.Result.Should().Be(ProcessingResultCode.Success);
+            ValidateDetectedComponents(result.DetectedComponents);
+        }
+
+        [TestMethod]
+        public void DetectComponents_DetectOnlyWithNoUrl()
+        {
+            var args = new BcdeArguments
+            {
+                AdditionalPluginDirectories = Enumerable.Empty(),
+                SourceDirectory = sourceDirectory,
+            };
+
+            var componentRecorder = new ComponentRecorder();
+            var singleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder("/location");
+            singleFileComponentRecorder.RegisterUsage(detectedComponents[0]);
+            singleFileComponentRecorder.RegisterUsage(detectedComponents[1]);
+
+            var result = DetectComponentsHappyPath(args, restrictions =>
+            {
+            }, new List { componentRecorder });
+
+            result.Result.Should().Be(ProcessingResultCode.Success);
+            ValidateDetectedComponents(result.DetectedComponents);
+        }
+
+        [TestMethod]
+        public void DetectComponents_ReturnsExperimentalDetectorInformation()
+        {
+            componentDetector2Mock.As();
+            componentDetector3Mock.As();
+
+            var args = new BcdeArguments
+            {
+                AdditionalPluginDirectories = Enumerable.Empty(),
+                SourceDirectory = sourceDirectory,
+            };
+
+            var componentRecorder = new ComponentRecorder();
+            var singleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder("/location");
+            singleFileComponentRecorder.RegisterUsage(detectedComponents[0]);
+            singleFileComponentRecorder.RegisterUsage(detectedComponents[1]);
+
+            var result = DetectComponentsHappyPath(args, restrictions => { }, new List { componentRecorder });
+
+            result.Result.Should().Be(ProcessingResultCode.Success);
+            ValidateDetectedComponents(result.DetectedComponents);
+            result.DetectorsInRun.All(x => x.IsExperimental).Should().BeTrue();
+        }
+
+        [TestMethod]
+        public void DetectComponents_Graph_Happy_Path()
+        {
+            string mockGraphLocation = "/some/dependency/graph";
+
+            var args = new BcdeArguments
+            {
+                AdditionalPluginDirectories = Enumerable.Empty(),
+                SourceDirectory = sourceDirectory,
+            };
+
+            var componentRecorder = new ComponentRecorder();
+            var singleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder(mockGraphLocation);
+            singleFileComponentRecorder.RegisterUsage(detectedComponents[0], isExplicitReferencedDependency: true, isDevelopmentDependency: true);
+            singleFileComponentRecorder.RegisterUsage(detectedComponents[1], isDevelopmentDependency: false, parentComponentId: detectedComponents[0].Component.Id);
+
+            Mock mockDependencyGraphA = new Mock();
+            mockDependencyGraphA.Setup(x => x.GetComponents()).Returns(new[] { detectedComponents[0].Component.Id, detectedComponents[1].Component.Id });
+            mockDependencyGraphA.Setup(x => x.GetDependenciesForComponent(detectedComponents[0].Component.Id))
+                .Returns(new[] { detectedComponents[1].Component.Id });
+            mockDependencyGraphA.Setup(x => x.IsComponentExplicitlyReferenced(detectedComponents[0].Component.Id)).Returns(true);
+            mockDependencyGraphA.Setup(x => x.IsDevelopmentDependency(detectedComponents[0].Component.Id)).Returns(true);
+            mockDependencyGraphA.Setup(x => x.IsDevelopmentDependency(detectedComponents[1].Component.Id)).Returns(false);
+
+            var result = DetectComponentsHappyPath(args, restrictions => { }, new List { componentRecorder });
+
+            result.Result.Should().Be(ProcessingResultCode.Success);
+            result.DependencyGraphs.Count().Should().Be(1);
+            var matchingGraph = result.DependencyGraphs.First();
+            matchingGraph.Key.Should().Be(mockGraphLocation);
+            var explicitlyReferencedComponents = matchingGraph.Value.ExplicitlyReferencedComponentIds;
+            explicitlyReferencedComponents.Count.Should().Be(1);
+            explicitlyReferencedComponents.Should().Contain(detectedComponents[0].Component.Id);
+
+            var actualGraph = matchingGraph.Value.Graph;
+            actualGraph.Keys.Count.Should().Be(2);
+            actualGraph[detectedComponents[0].Component.Id].Count().Should().Be(1);
+            actualGraph[detectedComponents[0].Component.Id].Should().Contain(detectedComponents[1].Component.Id);
+            actualGraph[detectedComponents[1].Component.Id].Should().BeNull();
+
+            matchingGraph.Value.DevelopmentDependencies.Should().Contain(detectedComponents[0].Component.Id);
+            matchingGraph.Value.DevelopmentDependencies.Should().NotContain(detectedComponents[1].Component.Id);
+
+            matchingGraph.Value.Dependencies.Should().Contain(detectedComponents[1].Component.Id);
+            matchingGraph.Value.Dependencies.Should().NotContain(detectedComponents[0].Component.Id);
+        }
+
+        [TestMethod]
+        public void DetectComponents_Graph_AccumulatesGraphsOnSameLocation()
+        {
+            string mockGraphLocation = "/some/dependency/graph";
+
+            var args = new BcdeArguments
+            {
+                AdditionalPluginDirectories = Enumerable.Empty(),
+                SourceDirectory = sourceDirectory,
+            };
+
+            var componentRecorder = new ComponentRecorder();
+
+            Mock mockDependencyGraphA = new Mock();
+            mockDependencyGraphA.Setup(x => x.GetComponents()).Returns(new[] { detectedComponents[0].Component.Id, detectedComponents[1].Component.Id });
+            mockDependencyGraphA.Setup(x => x.GetDependenciesForComponent(detectedComponents[0].Component.Id))
+                .Returns(new[] { detectedComponents[1].Component.Id });
+            mockDependencyGraphA.Setup(x => x.IsComponentExplicitlyReferenced(detectedComponents[0].Component.Id)).Returns(true);
+
+            var singleFileComponentRecorderA = componentRecorder.CreateSingleFileComponentRecorder(mockGraphLocation);
+            singleFileComponentRecorderA.RegisterUsage(detectedComponents[0], isExplicitReferencedDependency: true);
+            singleFileComponentRecorderA.RegisterUsage(detectedComponents[1], parentComponentId: detectedComponents[0].Component.Id);
+
+            Mock mockDependencyGraphB = new Mock();
+            mockDependencyGraphB.Setup(x => x.GetComponents()).Returns(new[] { detectedComponents[0].Component.Id, detectedComponents[1].Component.Id });
+            mockDependencyGraphB.Setup(x => x.GetDependenciesForComponent(detectedComponents[1].Component.Id))
+                .Returns(new[] { detectedComponents[0].Component.Id });
+            mockDependencyGraphB.Setup(x => x.IsComponentExplicitlyReferenced(detectedComponents[1].Component.Id)).Returns(true);
+
+            var singleFileComponentRecorderB = componentRecorder.CreateSingleFileComponentRecorder(mockGraphLocation);
+            singleFileComponentRecorderB.RegisterUsage(detectedComponents[1], isExplicitReferencedDependency: true);
+            singleFileComponentRecorderB.RegisterUsage(detectedComponents[0], parentComponentId: detectedComponents[1].Component.Id);
+
+            var result = DetectComponentsHappyPath(args, restrictions => { }, new List { componentRecorder });
+
+            result.Result.Should().Be(ProcessingResultCode.Success);
+            result.DependencyGraphs.Count().Should().Be(1);
+            var matchingGraph = result.DependencyGraphs.First();
+            matchingGraph.Key.Should().Be(mockGraphLocation);
+            var explicitlyReferencedComponents = matchingGraph.Value.ExplicitlyReferencedComponentIds;
+            explicitlyReferencedComponents.Count.Should().Be(2);
+            explicitlyReferencedComponents.Should().Contain(detectedComponents[0].Component.Id);
+            explicitlyReferencedComponents.Should().Contain(detectedComponents[1].Component.Id);
+
+            var actualGraph = matchingGraph.Value.Graph;
+            actualGraph.Keys.Count.Should().Be(2);
+            actualGraph[detectedComponents[0].Component.Id].Count().Should().Be(1);
+            actualGraph[detectedComponents[0].Component.Id].Should().Contain(detectedComponents[1].Component.Id);
+            actualGraph[detectedComponents[1].Component.Id].Count().Should().Be(1);
+            actualGraph[detectedComponents[1].Component.Id].Should().Contain(detectedComponents[0].Component.Id);
+        }
+
+        [TestMethod]
+        public void VerifyTranslation_ComponentsAreReturnedWithDevDependencyInfo()
+        {
+            var componentRecorder = new ComponentRecorder();
+            var npmDetector = new NpmComponentDetectorWithRoots();
+            var args = new BcdeArguments
+            {
+                AdditionalPluginDirectories = Enumerable.Empty(),
+                SourceDirectory = sourceDirectory,
+            };
+
+            var singleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder("location");
+            var detectedComponent1 = new DetectedComponent(new NpmComponent("test", "1.0.0"), detector: npmDetector);
+            var detectedComponent2 = new DetectedComponent(new NpmComponent("test", "2.0.0"), detector: npmDetector);
+            var detectedComponent3 = new DetectedComponent(new NpmComponent("test", "3.0.0"), detector: npmDetector);
+
+            singleFileComponentRecorder.RegisterUsage(detectedComponent1, isDevelopmentDependency: true);
+            singleFileComponentRecorder.RegisterUsage(detectedComponent2, isDevelopmentDependency: false);
+            singleFileComponentRecorder.RegisterUsage(detectedComponent3);
+
+            var results = SetupRecorderBasedScanning(args, new List { componentRecorder });
+
+            var detectedComponents = results.ComponentsFound;
+
+            var storedComponent1 = detectedComponents.First(dc => dc.Component.Id == detectedComponent1.Component.Id);
+            storedComponent1.IsDevelopmentDependency.Should().BeTrue();
+
+            var storedComponent2 = detectedComponents.First(dc => dc.Component.Id == detectedComponent2.Component.Id);
+            storedComponent2.IsDevelopmentDependency.Should().BeFalse();
+
+            var storedComponent3 = detectedComponents.First(dc => dc.Component.Id == detectedComponent3.Component.Id);
+            storedComponent3.IsDevelopmentDependency.Should().BeNull();
+        }
+
+        [TestMethod]
+        public void VerifyTranslation_RootsFromMultipleLocationsAreAgregated()
+        {
+            var componentRecorder = new ComponentRecorder();
+            var npmDetector = new NpmComponentDetectorWithRoots();
+            var args = new BcdeArguments
+            {
+                AdditionalPluginDirectories = Enumerable.Empty(),
+                SourceDirectory = sourceDirectory,
+            };
+
+            var singleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder("location1");
+            var detectedComponent1 = new DetectedComponent(new NpmComponent("test", "1.0.0"), detector: npmDetector);
+            var detectedComponent2 = new DetectedComponent(new NpmComponent("test", "2.0.0"), detector: npmDetector);
+
+            singleFileComponentRecorder.RegisterUsage(detectedComponent1, isExplicitReferencedDependency: true);
+            singleFileComponentRecorder.RegisterUsage(detectedComponent2, parentComponentId: detectedComponent1.Component.Id);
+
+            singleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder("location2");
+            var detectedComponent2NewLocation = new DetectedComponent(new NpmComponent("test", "2.0.0"), detector: npmDetector);
+            singleFileComponentRecorder.RegisterUsage(detectedComponent2NewLocation, isExplicitReferencedDependency: true);
+
+            var results = SetupRecorderBasedScanning(args, new List { componentRecorder });
+
+            var detectedComponents = results.ComponentsFound;
+
+            var storedComponent1 = detectedComponents.First(dc => dc.Component.Id == detectedComponent1.Component.Id);
+            storedComponent1.TopLevelReferrers.Should().HaveCount(1);
+            storedComponent1.TopLevelReferrers.Should().Contain(detectedComponent1.Component);
+
+            var storedComponent2 = detectedComponents.First(dc => dc.Component.Id == detectedComponent2.Component.Id);
+            storedComponent2.TopLevelReferrers.Should().HaveCount(2, "There 2 roots the component is root of itself in one location and other location the root is its parent");
+            storedComponent2.TopLevelReferrers.Should().Contain(detectedComponent1.Component);
+            storedComponent2.TopLevelReferrers.Should().Contain(detectedComponent2.Component);
+        }
+
+        [TestMethod]
+        public void VerifyTranslation_ComponentsAreReturnedWithRoots()
+        {
+            var componentRecorder = new ComponentRecorder();
+            var npmDetector = new NpmComponentDetectorWithRoots();
+            var args = new BcdeArguments
+            {
+                AdditionalPluginDirectories = Enumerable.Empty(),
+                SourceDirectory = sourceDirectory,
+            };
+
+            var singleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder("location");
+            var detectedComponent1 = new DetectedComponent(new NpmComponent("test", "1.0.0"), detector: npmDetector);
+            var detectedComponent2 = new DetectedComponent(new NpmComponent("test", "2.0.0"), detector: npmDetector);
+
+            singleFileComponentRecorder.RegisterUsage(detectedComponent1, isExplicitReferencedDependency: true);
+            singleFileComponentRecorder.RegisterUsage(detectedComponent2, parentComponentId: detectedComponent1.Component.Id);
+
+            var results = SetupRecorderBasedScanning(args, new List { componentRecorder });
+
+            var detectedComponents = results.ComponentsFound;
+
+            var storedComponent1 = detectedComponents.First(dc => dc.Component.Id == detectedComponent1.Component.Id);
+            storedComponent1.TopLevelReferrers.Should().HaveCount(1, "If a component is a root, then is root of itself");
+            storedComponent1.TopLevelReferrers.Should().Contain(detectedComponent1.Component);
+
+            var storedComponent2 = detectedComponents.First(dc => dc.Component.Id == detectedComponent2.Component.Id);
+            storedComponent2.TopLevelReferrers.Should().HaveCount(1);
+            storedComponent2.TopLevelReferrers.Should().Contain(detectedComponent1.Component);
+        }
+
+        [TestMethod]
+        public void VerifyTranslation_DevDependenciesAreMergedWhenSameComponentInDifferentFiles()
+        {
+            var componentRecorder = new ComponentRecorder();
+            var npmDetector = new NpmComponentDetectorWithRoots();
+            var args = new BcdeArguments
+            {
+                AdditionalPluginDirectories = Enumerable.Empty(),
+                SourceDirectory = sourceDirectory,
+            };
+
+            var firstRecorder = componentRecorder.CreateSingleFileComponentRecorder("FileA");
+            var secondRecorder = componentRecorder.CreateSingleFileComponentRecorder("FileB");
+
+            // These two merged should be true.
+            var componentAWithNoDevDep = new DetectedComponent(new NpmComponent("testA", "1.0.0"), detector: npmDetector);
+            var componentAWithDevDepTrue = new DetectedComponent(new NpmComponent("testA", "1.0.0"), detector: npmDetector);
+
+            // These two merged should be false.
+            var componentBWithNoDevDep = new DetectedComponent(new NpmComponent("testB", "1.0.0"), detector: npmDetector);
+            var componentBWithDevDepFalse = new DetectedComponent(new NpmComponent("testB", "1.0.0"), detector: npmDetector);
+
+            // These two merged should be false.
+            var componentCWithDevDepTrue = new DetectedComponent(new NpmComponent("testC", "1.0.0"), detector: npmDetector);
+            var componentCWithDevDepFalse = new DetectedComponent(new NpmComponent("testC", "1.0.0"), detector: npmDetector);
+
+            // These two merged should be true.
+            var componentDWithDevDepTrue = new DetectedComponent(new NpmComponent("testD", "1.0.0"), detector: npmDetector);
+            var componentDWithDevDepTrueCopy = new DetectedComponent(new NpmComponent("testD", "1.0.0"), detector: npmDetector);
+
+            // The hint for reading this test is to know that each "column" you see visually is what's being merged, so componentAWithNoDevDep is being merged "down" into componentAWithDevDepTrue.
+#pragma warning disable format
+            foreach ((DetectedComponent component, bool? isDevDep) component in new[] 
+            {
+                (componentAWithNoDevDep, (bool?)null), (componentAWithDevDepTrue, true),
+                (componentBWithNoDevDep, (bool?)null), (componentBWithDevDepFalse, false),
+                (componentCWithDevDepTrue, true), (componentCWithDevDepFalse, false),
+                (componentDWithDevDepTrue, true), (componentDWithDevDepTrueCopy, true),
+            })
+#pragma warning restore format
+            {
+                firstRecorder.RegisterUsage(component.component, isDevelopmentDependency: component.isDevDep);
+            }
+
+            var results = SetupRecorderBasedScanning(args, new List { componentRecorder });
+
+            var components = results.ComponentsFound;
+
+            components.Single(x => ((NpmComponent)x.Component).Name == "testA").IsDevelopmentDependency.Should().Be(true);
+            components.Single(x => ((NpmComponent)x.Component).Name == "testB").IsDevelopmentDependency.Should().Be(false);
+            components.Single(x => ((NpmComponent)x.Component).Name == "testC").IsDevelopmentDependency.Should().Be(false);
+            components.Single(x => ((NpmComponent)x.Component).Name == "testD").IsDevelopmentDependency.Should().Be(true);
+        }
+
+        [TestMethod]
+        public void VerifyTranslation_LocationsAreMergedWhenSameComponentInDifferentFiles()
+        {
+            var componentRecorder = new ComponentRecorder();
+            var npmDetector = new NpmComponentDetectorWithRoots();
+            var args = new BcdeArguments
+            {
+                AdditionalPluginDirectories = Enumerable.Empty(),
+                SourceDirectory = sourceDirectory,
+            };
+
+            var firstRecorder = componentRecorder.CreateSingleFileComponentRecorder(Path.Join(sourceDirectory.FullName, "/some/file/path"));
+            firstRecorder.AddAdditionalRelatedFile(Path.Join(sourceDirectory.FullName, "/some/related/file/1"));
+            var secondRecorder = componentRecorder.CreateSingleFileComponentRecorder(Path.Join(sourceDirectory.FullName, "/some/other/file/path"));
+            secondRecorder.AddAdditionalRelatedFile(Path.Join(sourceDirectory.FullName, "/some/related/file/2"));
+
+            var firstComponent = new DetectedComponent(new NpmComponent("test", "1.0.0"), detector: npmDetector);
+            var secondComponent = new DetectedComponent(new NpmComponent("test", "1.0.0"), detector: npmDetector);
+
+            firstRecorder.RegisterUsage(firstComponent);
+            secondRecorder.RegisterUsage(secondComponent);
+
+            var results = SetupRecorderBasedScanning(args, new List { componentRecorder });
+
+            var actualComponent = results.ComponentsFound.Single();
+
+            actualComponent.LocationsFoundAt.Count().Should().Be(4);
+            foreach (var path in new[]
+                                        {
+                                            "/some/file/path",
+                                            "/some/other/file/path",
+                                            "/some/related/file/1",
+                                            "/some/related/file/2",
+                                        })
+            {
+                actualComponent.LocationsFoundAt
+                    .FirstOrDefault(x => x == path)
+                    .Should().NotBeNull();
+            }
+        }
+
+        [TestMethod]
+        public void VerifyTranslation_RootsAreMergedWhenSameComponentInDifferentFiles()
+        {
+            var componentRecorder = new ComponentRecorder();
+            var npmDetector = new NpmComponentDetectorWithRoots();
+            var args = new BcdeArguments
+            {
+                AdditionalPluginDirectories = Enumerable.Empty(),
+                SourceDirectory = sourceDirectory,
+            };
+
+            var firstRecorder = componentRecorder.CreateSingleFileComponentRecorder("FileA");
+            var secondRecorder = componentRecorder.CreateSingleFileComponentRecorder("FileB");
+
+            var root1 = new DetectedComponent(new NpmComponent("test1", "2.0.0"), detector: npmDetector);
+            var firstComponent = new DetectedComponent(new NpmComponent("test", "1.0.0"), detector: npmDetector);
+
+            var root2 = new DetectedComponent(new NpmComponent("test2", "3.0.0"), detector: npmDetector);
+            var secondComponent = new DetectedComponent(new NpmComponent("test", "1.0.0"), detector: npmDetector);
+
+            firstRecorder.RegisterUsage(root1, isExplicitReferencedDependency: true);
+            firstRecorder.RegisterUsage(firstComponent, parentComponentId: root1.Component.Id);
+
+            secondRecorder.RegisterUsage(root2, isExplicitReferencedDependency: true);
+            secondRecorder.RegisterUsage(secondComponent, parentComponentId: root2.Component.Id);
+
+            var results = SetupRecorderBasedScanning(args, new List { componentRecorder });
+
+            var actualComponent = results.ComponentsFound.First(c => c.Component.Id == firstComponent.Component.Id);
+            actualComponent.TopLevelReferrers.Count().Should().Be(2);
+            actualComponent.TopLevelReferrers.OfType()
+                .FirstOrDefault(x => x.Name == "test1" && x.Version == "2.0.0")
+                .Should().NotBeNull();
+            actualComponent.TopLevelReferrers.OfType()
+                .FirstOrDefault(x => x.Name == "test2" && x.Version == "3.0.0")
+                .Should().NotBeNull();
+        }
+
+        [TestMethod]
+        public void VerifyTranslation_DetectedComponentExist_UpdateFunctionIsApplied()
+        {
+            var componentRecorder = new ComponentRecorder();
+            var npmDetector = new NpmComponentDetectorWithRoots();
+            var args = new BcdeArguments
+            {
+                AdditionalPluginDirectories = Enumerable.Empty(),
+                SourceDirectory = sourceDirectory,
+            };
+
+            var singleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder("location");
+            var detectedComponent = new DetectedComponent(new NpmComponent("test", "1.0.0"), detector: npmDetector);
+
+            singleFileComponentRecorder.RegisterUsage(detectedComponent, isDevelopmentDependency: true);
+
+            var results = SetupRecorderBasedScanning(args, new List { componentRecorder });
+            results.ComponentsFound.Where(component => component.Component.Id == detectedComponent.Component.Id).Single().IsDevelopmentDependency.Should().BeTrue();
+
+            singleFileComponentRecorder.RegisterUsage(detectedComponent, isDevelopmentDependency: false);
+
+            results = SetupRecorderBasedScanning(args, new List { componentRecorder });
+            results.ComponentsFound.Where(component => component.Component.Id == detectedComponent.Component.Id).Single().IsDevelopmentDependency.Should().BeFalse();
+        }
+
+        private TestOutput DetectComponentsHappyPath(
+            BcdeArguments args,
+            Action restrictionAsserter = null,
+            IEnumerable componentRecorders = null)
+        {
+            var registeredDetectors = new[] { componentDetector2Mock.Object, componentDetector3Mock.Object, versionedComponentDetector1Mock.Object };
+            var restrictedDetectors = new[] { componentDetector2Mock.Object, componentDetector3Mock.Object };
+            detectorRegistryServiceMock.Setup(x => x.GetDetectors(Enumerable.Empty(), It.IsAny>()))
+                .Returns(registeredDetectors);
+            detectorRestrictionServiceMock.Setup(
+                x => x.ApplyRestrictions(
+                    It.IsAny(),
+                    It.Is>(inputDetectors => registeredDetectors.Intersect(inputDetectors).Count() == registeredDetectors.Count())))
+                .Returns(restrictedDetectors)
+                .Callback>(
+                    (restrictions, detectors) => restrictionAsserter?.Invoke(restrictions));
+
+            // We initialize detected component's DetectedBy here because of a Moq constraint -- certain operations (Adding interfaces) have to happen before .Object
+            detectedComponents[0].DetectedBy = componentDetector2Mock.Object;
+            detectedComponents[1].DetectedBy = componentDetector3Mock.Object;
+
+            var processingResult = new DetectorProcessingResult
+            {
+                ResultCode = ProcessingResultCode.Success,
+                ContainersDetailsMap = new Dictionary
+                {
+                    { sampleContainerDetails.Id, sampleContainerDetails },
+                },
+                ComponentRecorders = componentRecorders.Select(componentRecorder => (componentDetector2Mock.Object, componentRecorder)),
+            };
+
+            detectorProcessingServiceMock.Setup(x =>
+                x.ProcessDetectorsAsync(
+                   args,
+                   It.Is>(inputDetectors => restrictedDetectors.Intersect(inputDetectors).Count() == restrictedDetectors.Count()),
+                   Match.Create(restriction => true)))
+                .ReturnsAsync(processingResult);
+
+            var result = serviceUnderTest.ExecuteScanAsync(args).Result;
+            result.ResultCode.Should().Be(ProcessingResultCode.Success);
+
+            var testOutput = new TestOutput((DefaultGraphScanResult)result);
+
+            return testOutput;
+        }
+
+        private ScanResult SetupRecorderBasedScanning(
+            BcdeArguments args,
+            IEnumerable componentRecorders)
+        {
+            var registeredDetectors = new[] { componentDetector2Mock.Object, componentDetector3Mock.Object, versionedComponentDetector1Mock.Object };
+            var restrictedDetectors = new[] { componentDetector2Mock.Object, componentDetector3Mock.Object };
+            detectorRegistryServiceMock.Setup(x => x.GetDetectors(Enumerable.Empty(), It.IsAny>()))
+                .Returns(registeredDetectors);
+            detectorRestrictionServiceMock.Setup(
+                x => x.ApplyRestrictions(
+                    It.IsAny(),
+                    It.Is>(inputDetectors => registeredDetectors.Intersect(inputDetectors).Count() == registeredDetectors.Count())))
+                .Returns(restrictedDetectors);
+
+            // We initialize detected component's DetectedBy here because of a Moq constraint -- certain operations (Adding interfaces) have to happen before .Object
+            detectedComponents[0].DetectedBy = componentDetector2Mock.Object;
+            detectedComponents[1].DetectedBy = componentDetector3Mock.Object;
+
+            var processingResult = new DetectorProcessingResult
+            {
+                ResultCode = ProcessingResultCode.Success,
+                ContainersDetailsMap = new Dictionary
+                {
+                    { sampleContainerDetails.Id, sampleContainerDetails },
+                },
+                ComponentRecorders = componentRecorders.Select(componentRecorder => (componentDetector2Mock.Object, componentRecorder)),
+            };
+
+            detectorProcessingServiceMock.Setup(x =>
+                x.ProcessDetectorsAsync(
+                   args,
+                   It.Is>(inputDetectors => restrictedDetectors.Intersect(inputDetectors).Count() == restrictedDetectors.Count()),
+                   Match.Create(restriction => true)))
+                .ReturnsAsync(processingResult);
+
+            var result = serviceUnderTest.ExecuteScanAsync(args).Result;
+            result.ResultCode.Should().Be(ProcessingResultCode.Success);
+
+            return result;
+        }
+
+        private void ValidateDetectedComponents(IEnumerable scannedComponents)
+        {
+            var npmComponent = scannedComponents.Where(x => x.Component.Type == ComponentType.Npm).Select(x => x.Component as NpmComponent).FirstOrDefault();
+            npmComponent.Should().NotBeNull();
+            npmComponent.Name.Should().Be(((NpmComponent)detectedComponents[0].Component).Name);
+            var nugetComponent = scannedComponents.Where(x => x.Component.Type == ComponentType.NuGet).Select(x => x.Component as NuGetComponent).FirstOrDefault();
+            nugetComponent.Should().NotBeNull();
+            nugetComponent.Name.Should().Be(((NuGetComponent)detectedComponents[1].Component).Name);
+        }
+
+        private class TestOutput
+        {
+            public TestOutput(DefaultGraphScanResult result)
+            {
+                Result = result.ResultCode;
+                DetectedComponents = result.ComponentsFound;
+                DetectorsInRun = result.DetectorsInScan;
+                DependencyGraphs = result.DependencyGraphs;
+            }
+
+            internal ProcessingResultCode Result { get; set; }
+
+            internal IEnumerable DetectedComponents { get; set; }
+
+            internal IEnumerable DetectorsInRun { get; set; }
+
+            internal DependencyGraphCollection DependencyGraphs { get; set; }
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/DetectorListingCommandServiceTests.cs b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/DetectorListingCommandServiceTests.cs
new file mode 100644
index 000000000..c5e3958b6
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/DetectorListingCommandServiceTests.cs
@@ -0,0 +1,75 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Orchestrator.ArgumentSets;
+using Microsoft.ComponentDetection.Orchestrator.Services;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Tests.Services
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class DetectorListingCommandServiceTests
+    {
+        private Mock loggerMock;
+        private Mock detectorRegistryServiceMock;
+        private Mock componentDetector2Mock;
+        private Mock componentDetector3Mock;
+        private Mock versionedComponentDetector1Mock;
+
+        private DetectorListingCommandService serviceUnderTest;
+
+        private List logOutput;
+
+        [TestInitialize]
+        public void InitializeTest()
+        {
+            loggerMock = new Mock();
+            detectorRegistryServiceMock = new Mock();
+            componentDetector2Mock = new Mock();
+            componentDetector3Mock = new Mock();
+            versionedComponentDetector1Mock = new Mock();
+
+            serviceUnderTest = new DetectorListingCommandService
+            {
+                DetectorRegistryService = detectorRegistryServiceMock.Object,
+                Logger = loggerMock.Object,
+            };
+
+            logOutput = new List();
+            loggerMock.Setup(x => x.LogInfo(It.IsAny())).Callback(loggedString =>
+            {
+                logOutput.Add(loggedString);
+            });
+
+            componentDetector2Mock.SetupGet(x => x.Id).Returns("ComponentDetector2");
+            componentDetector3Mock.SetupGet(x => x.Id).Returns("ComponentDetector3");
+            versionedComponentDetector1Mock.SetupGet(x => x.Id).Returns("VersionedComponentDetector");
+
+            var registeredDetectors = new[] { componentDetector2Mock.Object, componentDetector3Mock.Object, versionedComponentDetector1Mock.Object };
+            detectorRegistryServiceMock.Setup(x => x.GetDetectors(It.IsAny>(), It.IsAny>()))
+                .Returns(registeredDetectors);
+        }
+
+        [TestCleanup]
+        public void CleanupTests()
+        {
+            detectorRegistryServiceMock.VerifyAll();
+        }
+
+        [TestMethod]
+        public async Task DetectorListingCommandService_ListsDetectors()
+        {
+            var result = await serviceUnderTest.Handle(new ListDetectionArgs());
+            result.ResultCode.Should().Be(ProcessingResultCode.Success);
+
+            logOutput.Should().Contain("ComponentDetector2");
+            logOutput.Should().Contain("ComponentDetector3");
+            logOutput.Should().Contain("VersionedComponentDetector");
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/DetectorProcessingServiceTests.cs b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/DetectorProcessingServiceTests.cs
new file mode 100644
index 000000000..56966705f
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/DetectorProcessingServiceTests.cs
@@ -0,0 +1,539 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Common;
+using Microsoft.ComponentDetection.Common.Telemetry.Records;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Orchestrator.ArgumentSets;
+using Microsoft.ComponentDetection.Orchestrator.Services;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Newtonsoft.Json;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Tests.Services
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class DetectorProcessingServiceTests
+    {
+        private Mock loggerMock;
+        private DetectorProcessingService serviceUnderTest;
+        private FastDirectoryWalkerFactory directoryWalkerFactory;
+
+        private Mock firstFileComponentDetectorMock;
+        private Mock secondFileComponentDetectorMock;
+        private Mock firstCommandComponentDetectorMock;
+        private Mock secondCommandComponentDetectorMock;
+        private Mock experimentalFileComponentDetectorMock;
+
+        private IEnumerable detectorsToUse;
+
+        private readonly Dictionary componentDictionary = new Dictionary()
+        {
+            { "firstFileDetectorId", new DetectedComponent(new NpmComponent($"{Guid.NewGuid()}", "FileComponentVersion1")) },
+            { "secondFileDetectorId", new DetectedComponent(new NuGetComponent("FileComponentName2", "FileComponentVersion2")) },
+            { "firstCommandDetectorId", new DetectedComponent(new NpmComponent("CommandComponentName1", "CommandComponentVersion1")) },
+            { "secondCommandDetectorId",  new DetectedComponent(new NuGetComponent("CommandComponentName2", "CommandComponentVersion2")) },
+            { "experimentalFileDetectorId", new DetectedComponent(new NuGetComponent("experimentalDetectorName", "experimentalDetectorVersion")) },
+        };
+
+        private IndividualDetectorScanResult ExpectedResultForDetector(string detectorId)
+        {
+            return new IndividualDetectorScanResult
+            {
+                AdditionalTelemetryDetails = new Dictionary { { "detectorId", detectorId } },
+                ResultCode = ProcessingResultCode.Success,
+            };
+        }
+
+        private bool isWin;
+        private static DirectoryInfo defaultSourceDirectory = new DirectoryInfo(Path.Combine(Environment.CurrentDirectory, "SomeSource", "Directory"));
+        private static BcdeArguments defaultArgs = new BcdeArguments { SourceDirectory = defaultSourceDirectory, DetectorArgs = Enumerable.Empty() };
+
+        [TestInitialize]
+        public void TestInit()
+        {
+            loggerMock = new Mock();
+            serviceUnderTest = new DetectorProcessingService();
+            serviceUnderTest.Logger = loggerMock.Object;
+
+            directoryWalkerFactory = new FastDirectoryWalkerFactory()
+            {
+                Logger = loggerMock.Object,
+                PathUtilityService = new PathUtilityService(),
+            };
+
+            serviceUnderTest.Scanner = directoryWalkerFactory;
+
+            firstFileComponentDetectorMock = SetupFileDetectorMock("firstFileDetectorId");
+            secondFileComponentDetectorMock = SetupFileDetectorMock("secondFileDetectorId");
+            experimentalFileComponentDetectorMock = SetupFileDetectorMock("experimentalFileDetectorId");
+            experimentalFileComponentDetectorMock.As();
+
+            firstCommandComponentDetectorMock = SetupCommandDetectorMock("firstCommandDetectorId");
+            secondCommandComponentDetectorMock = SetupCommandDetectorMock("secondCommandDetectorId");
+
+            isWin = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+        }
+
+        private Mock SetupFileDetectorMock(string id)
+        {
+            var mockFileDetector = new Mock();
+            mockFileDetector.SetupAllProperties();
+            mockFileDetector.SetupGet(x => x.Id).Returns(id);
+
+            var sourceDirectory = new DirectoryInfo(Path.Combine(Environment.CurrentDirectory, "Some", "Source", "Directory"));
+            componentDictionary.Should().ContainKey(id, $"MockDetector id:{id}, should be in mock dictionary");
+
+            var expectedResult = ExpectedResultForDetector(id);
+
+            mockFileDetector.Setup(x => x.ExecuteDetectorAsync(It.Is(request => request.SourceDirectory == defaultArgs.SourceDirectory && request.ComponentRecorder != null))).ReturnsAsync(
+                (ScanRequest request) =>
+                {
+                    return mockFileDetector.Object.ExecuteDetectorAsync(request).Result;
+                }).Verifiable();
+            mockFileDetector.Setup(x => x.ExecuteDetectorAsync(It.Is(request => request.SourceDirectory == defaultArgs.SourceDirectory && request.ComponentRecorder != null))).ReturnsAsync(
+                (ScanRequest request) =>
+                {
+                    serviceUnderTest.Scanner.Initialize(request.SourceDirectory, request.DirectoryExclusionPredicate, 1);
+                    FillComponentRecorder(request.ComponentRecorder, id);
+                    return expectedResult;
+                }).Verifiable();
+
+            return mockFileDetector;
+        }
+
+        private IEnumerable GetDiscoveredComponentsFromDetectorProcessingResult(DetectorProcessingResult detectorProcessingResult)
+        {
+            return detectorProcessingResult
+                        .ComponentRecorders
+                        .Select(componentRecorder => componentRecorder.Item2.GetDetectedComponents())
+                        .SelectMany(x => x);
+        }
+
+        private void FillComponentRecorder(IComponentRecorder componentRecorder, string id)
+        {
+            var singleFileRecorder = componentRecorder.CreateSingleFileComponentRecorder("/mock/location");
+            singleFileRecorder.RegisterUsage(componentDictionary[id], false);
+        }
+
+        private void ValidateExpectedComponents(DetectorProcessingResult result, IEnumerable detectorsRan)
+        {
+            var shouldBePresent = detectorsRan.Where(detector => !(detector is IExperimentalDetector))
+                .Select(detector => componentDictionary[detector.Id]);
+            var isPresent = GetDiscoveredComponentsFromDetectorProcessingResult(result);
+
+            var check = isPresent.Select(i => i.GetType());
+
+            isPresent.All(discovered => shouldBePresent.Contains(discovered));
+            shouldBePresent.Should().HaveCount(isPresent.Count());
+        }
+
+        private Mock SetupCommandDetectorMock(string id)
+        {
+            var mockCommandDetector = new Mock();
+            mockCommandDetector.SetupAllProperties();
+            mockCommandDetector.SetupGet(x => x.Id).Returns(id);
+
+            componentDictionary.Should().ContainKey(id, $"MockDetector id:{id}, should be in mock dictionary");
+
+            mockCommandDetector.Setup(x => x.ExecuteDetectorAsync(It.Is(request => request.SourceDirectory == defaultArgs.SourceDirectory && !request.DetectorArgs.Any()))).ReturnsAsync(
+                (ScanRequest request) =>
+                {
+                    FillComponentRecorder(request.ComponentRecorder, id);
+                    return ExpectedResultForDetector(id);
+                }).Verifiable();
+
+            return mockCommandDetector;
+        }
+
+        [TestMethod]
+        public void ProcessDetectorsAsync_HappyPathReturnsDetectedComponents()
+        {
+            detectorsToUse = new[] { firstFileComponentDetectorMock.Object, secondFileComponentDetectorMock.Object };
+            var results = serviceUnderTest.ProcessDetectorsAsync(defaultArgs, detectorsToUse, new DetectorRestrictions()).Result;
+
+            firstFileComponentDetectorMock.Verify(x => x.ExecuteDetectorAsync(It.Is(request => request.SourceDirectory == defaultArgs.SourceDirectory)));
+            secondFileComponentDetectorMock.Verify(x => x.ExecuteDetectorAsync(It.Is(request => request.SourceDirectory == defaultArgs.SourceDirectory)));
+
+            ValidateExpectedComponents(results, detectorsToUse);
+            GetDiscoveredComponentsFromDetectorProcessingResult(results).FirstOrDefault(x => x.Component?.Type == ComponentType.Npm).Component
+                .Should().Be(componentDictionary[firstFileComponentDetectorMock.Object.Id].Component);
+            GetDiscoveredComponentsFromDetectorProcessingResult(results).FirstOrDefault(x => x.Component?.Type == ComponentType.NuGet).Component
+                .Should().Be(componentDictionary[secondFileComponentDetectorMock.Object.Id].Component);
+
+            results.ResultCode.Should().Be(ProcessingResultCode.Success);
+        }
+
+        [TestMethod]
+        public void ProcessDetectorsAsync_NullDetectedComponentsReturnIsCoalesced()
+        {
+            Mock mockComponentDetector = new Mock();
+            mockComponentDetector.Setup(d => d.Id).Returns("test");
+
+            mockComponentDetector.Setup(x => x.ExecuteDetectorAsync(It.IsAny()))
+                .ReturnsAsync(() =>
+                {
+                    return new IndividualDetectorScanResult
+                    {
+                        ResultCode = ProcessingResultCode.Success,
+                        ContainerDetails = null,
+                        AdditionalTelemetryDetails = null,
+                    };
+                });
+
+            detectorsToUse = new[] { mockComponentDetector.Object };
+            var results = serviceUnderTest.ProcessDetectorsAsync(defaultArgs, detectorsToUse, new DetectorRestrictions()).Result;
+
+            results.ResultCode.Should().Be(ProcessingResultCode.Success);
+        }
+
+        [TestMethod]
+        public void ProcessDetectorsAsync_HappyPathReturns_DependencyGraph()
+        {
+            detectorsToUse = new[] { firstFileComponentDetectorMock.Object, secondFileComponentDetectorMock.Object };
+            var results = serviceUnderTest.ProcessDetectorsAsync(defaultArgs, detectorsToUse, new DetectorRestrictions()).Result;
+
+            firstFileComponentDetectorMock.Verify(x => x.ExecuteDetectorAsync(It.Is(request => request.SourceDirectory == defaultArgs.SourceDirectory)));
+            secondFileComponentDetectorMock.Verify(x => x.ExecuteDetectorAsync(It.Is(request => request.SourceDirectory == defaultArgs.SourceDirectory)));
+
+            foreach (var discoveredComponent in GetDiscoveredComponentsFromDetectorProcessingResult(results))
+            {
+                var componentId = discoveredComponent.Component.Id;
+                bool isMatched = false;
+                foreach (var graph in results.ComponentRecorders.Select(componentRecorder => componentRecorder.Item2.GetDependencyGraphsByLocation()).SelectMany(x => x.Values))
+                {
+                    isMatched |= graph.GetComponents().Contains(componentId);
+                }
+
+                isMatched.Should().BeTrue();
+            }
+        }
+
+        [TestMethod]
+        public void ProcessDetectorsAsync_AdditionalTelemetryDetailsAreReturned()
+        {
+            detectorsToUse = new[] { firstFileComponentDetectorMock.Object, secondFileComponentDetectorMock.Object };
+            var records = TelemetryHelper.ExecuteWhileCapturingTelemetry(() =>
+            {
+                serviceUnderTest.ProcessDetectorsAsync(defaultArgs, detectorsToUse, new DetectorRestrictions()).Wait();
+            });
+
+            foreach (var record in records)
+            {
+                var additionalTelemetryDetails = JsonConvert.DeserializeObject>(record.AdditionalTelemetryDetails);
+                additionalTelemetryDetails["detectorId"].Should().Be(record.DetectorId);
+            }
+        }
+
+        [TestMethod]
+        public void ProcessDetectorsAsync_ExperimentalDetectorsDoNotReturnComponents()
+        {
+            detectorsToUse = new[] { firstFileComponentDetectorMock.Object, secondFileComponentDetectorMock.Object, experimentalFileComponentDetectorMock.Object };
+
+            DetectorProcessingResult results = null;
+            var records = TelemetryHelper.ExecuteWhileCapturingTelemetry(() =>
+            {
+                results = serviceUnderTest.ProcessDetectorsAsync(defaultArgs, detectorsToUse, new DetectorRestrictions()).Result;
+            });
+
+            var experimentalDetectorRecord = records.FirstOrDefault(x => x.DetectorId == experimentalFileComponentDetectorMock.Object.Id);
+            var experimentalComponent = componentDictionary[experimentalFileComponentDetectorMock.Object.Id].Component as NuGetComponent;
+
+            // protect against test code changes.
+            experimentalComponent.Name.Should().NotBeNullOrEmpty("Experimental component should be nuget and have a name");
+
+            experimentalDetectorRecord.Should().NotBeNull();
+            experimentalDetectorRecord.DetectedComponentCount.Should().Be(1);
+            experimentalDetectorRecord.IsExperimental.Should().BeTrue();
+
+            // We should have all components except the ones that came from our experimental detector
+            GetDiscoveredComponentsFromDetectorProcessingResult(results).Count().Should().Be(records.Sum(x => x.DetectedComponentCount) - experimentalDetectorRecord.DetectedComponentCount);
+            GetDiscoveredComponentsFromDetectorProcessingResult(results).All(x => (x.Component as NuGetComponent)?.Name != experimentalComponent.Name)
+                .Should().BeTrue("Experimental component should not be in component list");
+            results.ResultCode.Should().Be(ProcessingResultCode.Success);
+
+            firstFileComponentDetectorMock.Verify(x => x.ExecuteDetectorAsync(It.Is(request => request.SourceDirectory == defaultArgs.SourceDirectory)));
+            secondFileComponentDetectorMock.Verify(x => x.ExecuteDetectorAsync(It.Is(request => request.SourceDirectory == defaultArgs.SourceDirectory)));
+            experimentalFileComponentDetectorMock.Verify(x => x.ExecuteDetectorAsync(It.Is(request => request.SourceDirectory == defaultArgs.SourceDirectory)));
+        }
+
+        [TestMethod]
+        public void ProcessDetectorsAsync_ExperimentalDetectorsDoNormalStuffIfExplicitlyEnabled()
+        {
+            detectorsToUse = new[] { firstFileComponentDetectorMock.Object, secondFileComponentDetectorMock.Object, experimentalFileComponentDetectorMock.Object };
+            var experimentalDetectorId = experimentalFileComponentDetectorMock.Object.Id;
+
+            DetectorProcessingResult results = null;
+            var records = TelemetryHelper.ExecuteWhileCapturingTelemetry(() =>
+            {
+                results = serviceUnderTest.ProcessDetectorsAsync(defaultArgs, detectorsToUse, new DetectorRestrictions { ExplicitlyEnabledDetectorIds = new[] { experimentalDetectorId } }).Result;
+            });
+
+            // We should have all components except the ones that came from our experimental detector
+            GetDiscoveredComponentsFromDetectorProcessingResult(results).Count().Should().Be(records.Sum(x => x.DetectedComponentCount));
+            GetDiscoveredComponentsFromDetectorProcessingResult(results).FirstOrDefault(x => (x.Component as NuGetComponent)?.Name == (componentDictionary[experimentalDetectorId].Component as NuGetComponent).Name)
+                .Should().NotBeNull();
+            results.ResultCode.Should().Be(ProcessingResultCode.Success);
+
+            firstFileComponentDetectorMock.Verify(x => x.ExecuteDetectorAsync(It.Is(request => request.SourceDirectory == defaultArgs.SourceDirectory)));
+            secondFileComponentDetectorMock.Verify(x => x.ExecuteDetectorAsync(It.Is(request => request.SourceDirectory == defaultArgs.SourceDirectory)));
+            experimentalFileComponentDetectorMock.Verify(x => x.ExecuteDetectorAsync(It.Is(request => request.SourceDirectory == defaultArgs.SourceDirectory)));
+        }
+
+        [TestMethod]
+        public void ProcessDetectorsAsync_ExperimentalDetectorsThrowingDoesntKillDetection()
+        {
+            detectorsToUse = new[] { firstFileComponentDetectorMock.Object, secondFileComponentDetectorMock.Object, experimentalFileComponentDetectorMock.Object };
+
+            experimentalFileComponentDetectorMock.Setup(x => x.ExecuteDetectorAsync(It.Is(request => request.SourceDirectory == defaultArgs.SourceDirectory)))
+                .Throws(new InvalidOperationException("Simulated experimental failure"));
+
+            DetectorProcessingResult results = null;
+            var records = TelemetryHelper.ExecuteWhileCapturingTelemetry(() =>
+            {
+                results = serviceUnderTest.ProcessDetectorsAsync(defaultArgs, detectorsToUse, new DetectorRestrictions()).Result;
+            });
+
+            var experimentalDetectorRecord = records.FirstOrDefault(x => x.DetectorId == experimentalFileComponentDetectorMock.Object.Id);
+            experimentalDetectorRecord.Should().NotBeNull();
+            experimentalDetectorRecord.DetectedComponentCount.Should().Be(0);
+            experimentalDetectorRecord.IsExperimental.Should().BeTrue();
+            experimentalDetectorRecord.ReturnCode.Should().Be((int)ProcessingResultCode.InputError);
+            experimentalDetectorRecord.ExperimentalInformation.Contains("Simulated experimental failure");
+
+            // We should have all components except the ones that came from our experimental detector
+            GetDiscoveredComponentsFromDetectorProcessingResult(results).Count().Should().Be(records.Sum(x => x.DetectedComponentCount));
+            results.ResultCode.Should().Be(ProcessingResultCode.Success);
+
+            firstFileComponentDetectorMock.Verify(x => x.ExecuteDetectorAsync(It.Is(request => request.SourceDirectory == defaultArgs.SourceDirectory)));
+            secondFileComponentDetectorMock.Verify(x => x.ExecuteDetectorAsync(It.Is(request => request.SourceDirectory == defaultArgs.SourceDirectory)));
+        }
+
+        [TestMethod]
+        public void ProcessDetectorsAsync_DirectoryExclusionPredicateWorksAsExpected()
+        {
+            detectorsToUse = new[] { firstFileComponentDetectorMock.Object };
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                Assert.Inconclusive("Test is platform specific and fails on non-windows");
+            }
+
+            var d1 = new DirectoryInfo(Path.Combine(Environment.CurrentDirectory, "shouldExclude", "stuff"));
+            var d2 = new DirectoryInfo(Path.Combine(Environment.CurrentDirectory, "shouldNotExclude", "stuff"));
+
+            ScanRequest capturedRequest = null;
+            firstFileComponentDetectorMock.Setup(x => x.ExecuteDetectorAsync(It.IsAny()))
+                .ReturnsAsync(ExpectedResultForDetector(firstFileComponentDetectorMock.Object.Id))
+                .Callback(request => capturedRequest = request);
+
+            serviceUnderTest.ProcessDetectorsAsync(defaultArgs, detectorsToUse, new DetectorRestrictions()).Wait();
+
+            directoryWalkerFactory.Reset();
+
+            // Base case should match all directories.
+            capturedRequest.DirectoryExclusionPredicate(defaultArgs.SourceDirectory.Name, defaultArgs.SourceDirectory.Parent.Name).Should().BeFalse();
+            capturedRequest.DirectoryExclusionPredicate(d1.Name, d1.Parent.FullName).Should().BeFalse();
+
+            var argsWithExclusion = new BcdeArguments()
+            {
+                SourceDirectory = defaultSourceDirectory,
+                DetectorArgs = Enumerable.Empty(),
+                DirectoryExclusionList = new[] { Path.Combine("**", "SomeSource", "**"), Path.Combine("**", "shouldExclude", "**") },
+            };
+
+            // Now exercise the exclusion code
+            serviceUnderTest.ProcessDetectorsAsync(argsWithExclusion, detectorsToUse, new DetectorRestrictions()).Wait();
+
+            directoryWalkerFactory.Reset();
+
+            // Previous two tests should now exclude
+            capturedRequest.DirectoryExclusionPredicate(defaultArgs.SourceDirectory.Name, defaultArgs.SourceDirectory.Parent.FullName).Should().BeTrue();
+            capturedRequest.DirectoryExclusionPredicate(d1.Name, d1.Parent.FullName).Should().BeTrue();
+
+            // Some other directory should still match
+            capturedRequest.DirectoryExclusionPredicate(d2.Name, d2.Parent.FullName).Should().BeFalse();
+        }
+
+        [TestMethod]
+        public void GenerateDirectoryExclusionPredicate_IgnoreCaseAndAllowWindowsPathsWorksAsExpected()
+        {
+            /*
+            * We can't test a scenario like:
+            *
+            * SourceDirectory = /Some/Source/Directory
+            * DirectoryExclusionList = *Some/*
+            * allowWindowsPath = false
+            *
+            * and expect to exclude the directory, because when
+            * we pass the SourceDirectory path to DirectoryInfo and we are running the test on Windows,
+            * DirectoryInfo transalate it to C:\\Some\Source\Directory
+            * making the test fail
+            */
+
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                Assert.Inconclusive("Test is inconsistent for non-windows platforms");
+            }
+
+            // This unit test previously depended on defaultArgs.   But the author assumed that \Source\ was in the default source path, which may not be true on all developers machines.
+            // control this more explicitly.
+            BcdeArguments args = new BcdeArguments { SourceDirectory = new System.IO.DirectoryInfo(isWin ? @"C:\Some\Source\Directory" : "/c/Some/Source/Directory"), DetectorArgs = Enumerable.Empty() };
+
+            var dn = args.SourceDirectory.Name.AsSpan();
+            var dp = args.SourceDirectory.Parent.FullName.AsSpan();
+
+            // Exclusion predicate is case sensitive and allow windows path, the exclusion list follow the windows path structure and has a case mismatch with the directory path, should not exclude
+            args.DirectoryExclusionList = new[] { @"**\source\**" };
+            var exclusionPredicate = serviceUnderTest.GenerateDirectoryExclusionPredicate(@"C:\somefake\dir", args.DirectoryExclusionList, args.DirectoryExclusionListObsolete, allowWindowsPaths: true, ignoreCase: false);
+            Assert.IsFalse(exclusionPredicate(dn, dp));
+
+            // Exclusion predicate is case sensitive and allow windows path, the exclusion list follow the windows path structure and match directory path case, should exclude
+            args.DirectoryExclusionList = new[] { @"**\Source\**" };
+            exclusionPredicate = serviceUnderTest.GenerateDirectoryExclusionPredicate(@"C:\somefake\dir", args.DirectoryExclusionList, args.DirectoryExclusionListObsolete, allowWindowsPaths: true, ignoreCase: false);
+            Assert.IsTrue(exclusionPredicate(dn, dp));
+
+            // Exclusion predicate is not case sensitive and allow windows path, the exclusion list follow the windows path, should exclude
+            args.DirectoryExclusionList = new[] { @"**\sOuRce\**" };
+            exclusionPredicate = serviceUnderTest.GenerateDirectoryExclusionPredicate(@"C:\somefake\dir", args.DirectoryExclusionList, args.DirectoryExclusionListObsolete, allowWindowsPaths: true, ignoreCase: true);
+            Assert.IsTrue(exclusionPredicate(dn, dp));
+
+            // Exclusion predicate does not support windows path and the exclusion list define the path as a windows path, should not exclude
+            args.DirectoryExclusionList = new[] { @"**\Source\**" };
+            exclusionPredicate = serviceUnderTest.GenerateDirectoryExclusionPredicate(@"C:\somefake\dir", args.DirectoryExclusionList, args.DirectoryExclusionListObsolete, allowWindowsPaths: false, ignoreCase: true);
+            Assert.IsFalse(exclusionPredicate(dn, dp));
+
+            // Exclusion predicate support windows path and the exclusion list define the path as a windows path, should exclude
+            args.DirectoryExclusionList = new[] { @"**\Source\**" };
+            exclusionPredicate = serviceUnderTest.GenerateDirectoryExclusionPredicate(@"C:\somefake\dir", args.DirectoryExclusionList, args.DirectoryExclusionListObsolete, allowWindowsPaths: true, ignoreCase: true);
+            Assert.IsTrue(exclusionPredicate(dn, dp));
+
+            // Exclusion predicate support windows path and the exclusion list does not define a windows path, should exclude
+            args.DirectoryExclusionList = new[] { @"**/Source/**", @"**/Source\**" };
+            exclusionPredicate = serviceUnderTest.GenerateDirectoryExclusionPredicate(@"C:\somefake\dir", args.DirectoryExclusionList, args.DirectoryExclusionListObsolete, allowWindowsPaths: true, ignoreCase: true);
+            Assert.IsTrue(exclusionPredicate(dn, dp));
+        }
+
+        [TestMethod]
+        public void ProcessDetectorsAsync_DirectoryExclusionPredicateWorksAsExpectedForObsolete()
+        {
+            detectorsToUse = new[] { firstFileComponentDetectorMock.Object };
+            var sourceDirectory = defaultSourceDirectory;
+            var args = defaultArgs;
+            var d1 = new DirectoryInfo(Path.Combine(sourceDirectory.FullName, "Child"));
+            var d2 = new DirectoryInfo(Path.Combine(sourceDirectory.FullName, "..", "bin"));
+            var d3 = new DirectoryInfo(Path.Combine(sourceDirectory.FullName, "OtherChild"));
+
+            foreach (var di in new[] { sourceDirectory, d1, d2, d3 })
+            {
+                if (!di.Exists)
+                {
+                    di.Create();
+                }
+            }
+
+            Environment.CurrentDirectory = sourceDirectory.FullName;
+
+            ScanRequest capturedRequest = null;
+            firstFileComponentDetectorMock.Setup(x => x.ExecuteDetectorAsync(It.IsAny()))
+                .ReturnsAsync(ExpectedResultForDetector(firstFileComponentDetectorMock.Object.Id))
+                .Callback(request => capturedRequest = request);
+
+            serviceUnderTest.ProcessDetectorsAsync(args, detectorsToUse, new DetectorRestrictions()).Wait();
+
+            directoryWalkerFactory.Reset();
+
+            // Base case should match all directories.
+            capturedRequest.DirectoryExclusionPredicate(args.SourceDirectory.Name.AsSpan(), args.SourceDirectory.Parent.FullName.AsSpan()).Should().BeFalse();
+            capturedRequest.DirectoryExclusionPredicate(d1.Name.AsSpan(), d1.Parent.FullName.AsSpan()).Should().BeFalse();
+
+            // Now exercise the exclusion code
+            args.DirectoryExclusionListObsolete = new[] { Path.Combine("Child"), Path.Combine("..", "bin") };
+            serviceUnderTest.ProcessDetectorsAsync(args, new[] { firstFileComponentDetectorMock.Object }, new DetectorRestrictions()).Wait();
+
+            directoryWalkerFactory.Reset();
+
+            // Previous two tests should now exclude
+            capturedRequest.DirectoryExclusionPredicate(d1.Name.AsSpan(), d1.Parent.FullName.AsSpan()).Should().BeTrue();
+            capturedRequest.DirectoryExclusionPredicate(d2.Name.AsSpan(), d2.Parent.FullName.AsSpan()).Should().BeTrue();
+
+            // Some other directory should still match
+            capturedRequest.DirectoryExclusionPredicate(d3.Name.AsSpan(), d3.Parent.FullName.AsSpan()).Should().BeFalse();
+        }
+
+        [TestMethod]
+        public void ProcessDetectorsAsync_CapturesTelemetry()
+        {
+            BcdeArguments args = defaultArgs;
+            detectorsToUse = new[] { firstFileComponentDetectorMock.Object, secondFileComponentDetectorMock.Object };
+
+            var records = TelemetryHelper.ExecuteWhileCapturingTelemetry(() =>
+            {
+                serviceUnderTest.ProcessDetectorsAsync(args, detectorsToUse, new DetectorRestrictions()).Wait();
+            });
+
+            records.Should().Contain(x => x is DetectorExecutionTelemetryRecord);
+            records.Count(x => x is DetectorExecutionTelemetryRecord)
+                .Should().Be(2);
+            var firstDetectorRecord = records.FirstOrDefault(x => x.DetectorId == firstFileComponentDetectorMock.Object.Id);
+            firstDetectorRecord.Should().NotBeNull();
+            firstDetectorRecord.ExecutionTime.Should().BePositive();
+
+            var secondDetectorRecord = records.FirstOrDefault(x => x.DetectorId == secondFileComponentDetectorMock.Object.Id);
+            secondDetectorRecord.Should().NotBeNull();
+
+            firstFileComponentDetectorMock.Verify(x => x.ExecuteDetectorAsync(It.Is(request => request.SourceDirectory == defaultArgs.SourceDirectory)));
+            secondFileComponentDetectorMock.Verify(x => x.ExecuteDetectorAsync(It.Is(request => request.SourceDirectory == defaultArgs.SourceDirectory)));
+        }
+
+        [TestMethod]
+        public void ProcessDetectorsAsync_ExecutesMixedCommandAndFileDetectors()
+        {
+            detectorsToUse = new[] { firstFileComponentDetectorMock.Object, secondFileComponentDetectorMock.Object, firstCommandComponentDetectorMock.Object, secondCommandComponentDetectorMock.Object };
+
+            DetectorProcessingResult results = null;
+            var records = TelemetryHelper.ExecuteWhileCapturingTelemetry(() =>
+            {
+                results = serviceUnderTest.ProcessDetectorsAsync(defaultArgs, detectorsToUse, new DetectorRestrictions()).Result;
+            });
+
+            results.Should().NotBeNull("Detector processing failed");
+
+            records.Should().Contain(x => x is DetectorExecutionTelemetryRecord);
+
+            records.Count(x => x is DetectorExecutionTelemetryRecord)
+                .Should().Be(4);
+
+            ValidateExpectedComponents(results, detectorsToUse);
+
+            firstFileComponentDetectorMock.Verify(x => x.ExecuteDetectorAsync(It.Is(request => request.SourceDirectory == defaultArgs.SourceDirectory)));
+            secondFileComponentDetectorMock.Verify(x => x.ExecuteDetectorAsync(It.Is(request => request.SourceDirectory == defaultArgs.SourceDirectory)));
+            firstCommandComponentDetectorMock.Verify(x => x.ExecuteDetectorAsync(It.Is(request => request.SourceDirectory == defaultArgs.SourceDirectory)));
+            secondCommandComponentDetectorMock.Verify(x => x.ExecuteDetectorAsync(It.Is(request => request.SourceDirectory == defaultArgs.SourceDirectory)));
+        }
+
+        [TestMethod]
+        public void ProcessDetectorsAsync_HandlesDetectorArgs()
+        {
+            ScanRequest capturedRequest = null;
+            firstFileComponentDetectorMock.Setup(x => x.ExecuteDetectorAsync(It.IsAny()))
+                .ReturnsAsync(ExpectedResultForDetector(firstFileComponentDetectorMock.Object.Id))
+                .Callback(request => capturedRequest = request);
+
+            var args = defaultArgs;
+            args.DetectorArgs = new string[] { "arg1=val1", "arg2", "arg3=val3" };
+
+            serviceUnderTest.ProcessDetectorsAsync(defaultArgs, new[] { firstFileComponentDetectorMock.Object }, new DetectorRestrictions()).Wait();
+
+            capturedRequest.DetectorArgs
+                .Should().Contain("arg1", "val1")
+                .And.NotContainKey("arg2")
+                .And.Contain("arg3", "val3");
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/DetectorRestrictionServiceTests.cs b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/DetectorRestrictionServiceTests.cs
new file mode 100644
index 000000000..a8b4aea39
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Services/DetectorRestrictionServiceTests.cs
@@ -0,0 +1,178 @@
+using System;
+using System.Linq;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Common;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Orchestrator.Exceptions;
+using Microsoft.ComponentDetection.Orchestrator.Services;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Tests.Services
+{
+    [TestClass]
+    [TestCategory("Governance/All")]
+    [TestCategory("Governance/ComponentDetection")]
+    public class DetectorRestrictionServiceTests
+    {
+        private Mock logger;
+        private Mock firstDetectorMock;
+        private Mock secondDetectorMock;
+        private Mock thirdDetectorMock;
+        private Mock retiredNpmDetector;
+        private Mock newNpmDetector;
+        private IComponentDetector[] detectors;
+        private DetectorRestrictionService serviceUnderTest;
+
+        private Mock GenerateDetector(string detectorName, string[] categories = null)
+        {
+            var mockDetector = new Mock();
+            mockDetector.SetupGet(x => x.Id).Returns($"{detectorName}");
+            if (categories == null)
+            {
+                categories = new[] { $"{detectorName}Category", "AllCategory" };
+            }
+
+            mockDetector.SetupGet(x => x.Categories).Returns(categories);
+            return mockDetector;
+        }
+
+        [TestInitialize]
+        public void TestInitialize()
+        {
+            logger = new Mock();
+            firstDetectorMock = GenerateDetector("FirstDetector");
+            secondDetectorMock = GenerateDetector("SecondDetector");
+            thirdDetectorMock = GenerateDetector("ThirdDetector");
+            retiredNpmDetector = GenerateDetector("MSLicenseDevNpm");
+            newNpmDetector = GenerateDetector("NpmWithRoots");
+
+            detectors = new[] { firstDetectorMock.Object, secondDetectorMock.Object, thirdDetectorMock.Object, retiredNpmDetector.Object, newNpmDetector.Object };
+            serviceUnderTest = new DetectorRestrictionService() { Logger = logger.Object };
+        }
+
+        [TestMethod]
+        public void WithRestrictions_BaseCaseReturnsAll()
+        {
+            DetectorRestrictions r = new DetectorRestrictions();
+            var restrictedDetectors = serviceUnderTest.ApplyRestrictions(r, detectors);
+            restrictedDetectors
+                .Should().Contain(detectors);
+        }
+
+        [TestMethod]
+        public void WithRestrictions_RemovesDefaultOff()
+        {
+            DetectorRestrictions r = new DetectorRestrictions();
+            var detectorMock = GenerateDetector("defaultOffDetector");
+            var defaultOffDetectorMock = detectorMock.As();
+            detectors = detectors.Union(new[] { defaultOffDetectorMock.Object as IComponentDetector }).ToArray();
+            var restrictedDetectors = serviceUnderTest.ApplyRestrictions(r, detectors);
+            restrictedDetectors
+                .Should().NotContain(defaultOffDetectorMock.Object as IComponentDetector);
+        }
+
+        [TestMethod]
+        public void WithRestrictions_AllowsDefaultOffWithDetectorRestriction()
+        {
+            DetectorRestrictions r = new DetectorRestrictions();
+            var detectorMock = GenerateDetector("defaultOffDetector");
+            var defaultOffDetectorMock = detectorMock.As();
+            detectors = detectors.Union(new[] { defaultOffDetectorMock.Object as IComponentDetector }).ToArray();
+            r.ExplicitlyEnabledDetectorIds = new[] { "defaultOffDetector" };
+            var restrictedDetectors = serviceUnderTest.ApplyRestrictions(r, detectors);
+            restrictedDetectors
+                .Should().Contain(defaultOffDetectorMock.Object as IComponentDetector);
+        }
+
+        [TestMethod]
+        public void WithRestrictions_FiltersBasedOnDetectorId()
+        {
+            DetectorRestrictions r = new DetectorRestrictions();
+            r.AllowedDetectorIds = new[] { "FirstDetector", "SecondDetector" };
+            var restrictedDetectors = serviceUnderTest.ApplyRestrictions(r, detectors);
+            restrictedDetectors
+                .Should().Contain(firstDetectorMock.Object).And.Contain(secondDetectorMock.Object)
+                .And.NotContain(thirdDetectorMock.Object);
+
+            r.AllowedDetectorIds = new[] { "NotARealDetector" };
+            Action shouldThrow = () => serviceUnderTest.ApplyRestrictions(r, detectors);
+            shouldThrow.Should().Throw();
+        }
+
+        [TestMethod]
+        public void WithRestrictions_CorrectsRetiredDetector()
+        {
+            DetectorRestrictions r = new DetectorRestrictions();
+            r.AllowedDetectorIds = new[] { "MSLicenseDevNpm" };
+            var restrictedDetectors = serviceUnderTest.ApplyRestrictions(r, detectors);
+            restrictedDetectors
+                .Should().Contain(newNpmDetector.Object);
+
+            r.AllowedDetectorIds = new[] { "mslicensenpm" };
+            restrictedDetectors = serviceUnderTest.ApplyRestrictions(r, detectors);
+            restrictedDetectors
+                .Should().Contain(newNpmDetector.Object);
+
+            r.AllowedDetectorIds = new[] { "mslicensenpm", "NpmWithRoots" };
+            restrictedDetectors = serviceUnderTest.ApplyRestrictions(r, detectors);
+            restrictedDetectors
+                .Should().OnlyContain(item => item == newNpmDetector.Object);
+        }
+
+        [TestMethod]
+        public void WithRestrictions_FiltersBasedOnCategory()
+        {
+            DetectorRestrictions r = new DetectorRestrictions();
+            r.AllowedDetectorCategories = new[] { "FirstDetectorCategory", "ThirdDetectorCategory" };
+            var restrictedDetectors = serviceUnderTest.ApplyRestrictions(r, detectors);
+            restrictedDetectors
+                .Should().Contain(firstDetectorMock.Object).And.Contain(thirdDetectorMock.Object)
+                .And.NotContain(secondDetectorMock.Object);
+
+            r.AllowedDetectorCategories = new[] { "AllCategory" };
+            restrictedDetectors = serviceUnderTest.ApplyRestrictions(r, detectors);
+            restrictedDetectors
+                .Should().Contain(firstDetectorMock.Object)
+                .And.Contain(thirdDetectorMock.Object)
+                .And.Contain(secondDetectorMock.Object);
+
+            r.AllowedDetectorCategories = new[] { "NoCategory" };
+            Action shouldThrow = () => serviceUnderTest.ApplyRestrictions(r, detectors);
+            shouldThrow.Should().Throw();
+        }
+
+        [TestMethod]
+        public void WithRestrictions_AlwaysIncludesDetectorsThatSpecifyAllCategory()
+        {
+            var detectors = new[]
+            {
+                GenerateDetector("1", new[] { "Cat1" }).Object,
+                GenerateDetector("2", new[] { "Cat2" }).Object,
+                GenerateDetector("3", new[] { nameof(DetectorClass.All) }).Object,
+            };
+
+            DetectorRestrictions r = new DetectorRestrictions();
+            r.AllowedDetectorCategories = new[] { "ACategoryWhichDoesntMatch" };
+            var restrictedDetectors = serviceUnderTest.ApplyRestrictions(r, detectors);
+            restrictedDetectors
+                .Should().Contain(detectors[2])
+                .And.NotContain(detectors[0])
+                .And.NotContain(detectors[1]);
+
+            r.AllowedDetectorCategories = new[] { "Cat1" };
+            restrictedDetectors = serviceUnderTest.ApplyRestrictions(r, detectors);
+            restrictedDetectors
+                .Should().Contain(detectors[0])
+                .And.Contain(detectors[2])
+                .And.NotContain(detectors[1]);
+
+            r.AllowedDetectorCategories = null;
+            restrictedDetectors = serviceUnderTest.ApplyRestrictions(r, detectors);
+            restrictedDetectors
+                .Should().Contain(detectors[0])
+                .And.Contain(detectors[1])
+                .And.Contain(detectors[2]);
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.Orchestrator.Tests/TelemetryHelper.cs b/test/Microsoft.ComponentDetection.Orchestrator.Tests/TelemetryHelper.cs
new file mode 100644
index 000000000..ea8b2e45a
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Orchestrator.Tests/TelemetryHelper.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.ComponentDetection.Common.Telemetry;
+using Microsoft.ComponentDetection.Common.Telemetry.Records;
+using Moq;
+
+namespace Microsoft.ComponentDetection.Orchestrator.Tests
+{
+    public static class TelemetryHelper
+    {
+        public static IEnumerable ExecuteWhileCapturingTelemetry(Action codeToExecute)
+            where T : class, IDetectionTelemetryRecord
+        {
+            Mock telemetryServiceMock = new Mock();
+            ConcurrentBag records = new ConcurrentBag();
+            telemetryServiceMock.Setup(x => x.PostRecord(It.IsAny()))
+                .Callback(record =>
+                {
+                    if (record is T asT)
+                    {
+                        records.Add(asT);
+                    }
+                });
+            var oldServices = TelemetryRelay.TelemetryServices;
+            TelemetryRelay.TelemetryServices = new[] { telemetryServiceMock.Object };
+            try
+            {
+                codeToExecute();
+            }
+            finally
+            {
+                TelemetryRelay.TelemetryServices = oldServices;
+            }
+
+            return records.ToList();
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.TestsUtilities/Attributes/SkipTestIfNotWindowsAttribute.cs b/test/Microsoft.ComponentDetection.TestsUtilities/Attributes/SkipTestIfNotWindowsAttribute.cs
new file mode 100644
index 000000000..69b68f763
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.TestsUtilities/Attributes/SkipTestIfNotWindowsAttribute.cs
@@ -0,0 +1,25 @@
+using System.Runtime.InteropServices;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.ComponentDetection.TestsUtilities
+{
+    public class SkipTestIfNotWindowsAttribute : TestMethodAttribute
+    {
+        public override TestResult[] Execute(ITestMethod testMethod)
+        {
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                return new[]
+                {
+                    new TestResult
+                    {
+                        Outcome = UnitTestOutcome.Inconclusive,
+                        TestFailureException = new AssertInconclusiveException($"Skipped on {RuntimeInformation.OSDescription}."),
+                    },
+                };
+            }
+
+            return base.Execute(testMethod);
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.TestsUtilities/Attributes/SkipTestOnWindowsAttribute.cs b/test/Microsoft.ComponentDetection.TestsUtilities/Attributes/SkipTestOnWindowsAttribute.cs
new file mode 100644
index 000000000..b051b1ce3
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.TestsUtilities/Attributes/SkipTestOnWindowsAttribute.cs
@@ -0,0 +1,25 @@
+using System.Runtime.InteropServices;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.ComponentDetection.TestsUtilities
+{
+    public class SkipTestOnWindowsAttribute : TestMethodAttribute
+    {
+        public override TestResult[] Execute(ITestMethod testMethod)
+        {
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                return new[]
+                {
+                    new TestResult
+                    {
+                        Outcome = UnitTestOutcome.Inconclusive,
+                        TestFailureException = new AssertInconclusiveException("Skipped on Windows."),
+                    },
+                };
+            }
+
+            return base.Execute(testMethod);
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.TestsUtilities/DetectorTestUtility.cs b/test/Microsoft.ComponentDetection.TestsUtilities/DetectorTestUtility.cs
new file mode 100644
index 000000000..aa68f9682
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.TestsUtilities/DetectorTestUtility.cs
@@ -0,0 +1,220 @@
+using Moq;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Common;
+using Microsoft.ComponentDetection.Common.DependencyGraph;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.Internal;
+
+namespace Microsoft.ComponentDetection.TestsUtilities
+{
+    public class DetectorTestUtility
+        where T : FileComponentDetector, new()
+    {
+        private Mock mockLogger = new Mock();
+
+        private Mock mockComponentStreamEnumerableFactory;
+
+        private Mock mockObservableDirectoryWalkerFactory;
+
+        private IComponentRecorder componentRecorder = new ComponentRecorder();
+
+        private ScanRequest scanRequest;
+
+        private T detector;
+
+        private List<(string Name, Stream Contents, string Location, IEnumerable searchPatterns)> filesToAdd = new List<(string Name, Stream Contents, string Location, IEnumerable searchPatterns)>();
+
+        public async Task<(IndividualDetectorScanResult, IComponentRecorder)> ExecuteDetector()
+        {
+            if (scanRequest == null)
+            {
+                scanRequest = new ScanRequest(new DirectoryInfo(Path.GetTempPath()), null, null, new Dictionary(), null, componentRecorder);
+            }
+            else
+            {
+                componentRecorder = scanRequest.ComponentRecorder;
+            }
+
+            InitializeFileRelatedMocksUsingDefaultImplementationIfNecessary();
+
+            detector.Scanner = mockObservableDirectoryWalkerFactory.Object;
+            detector.ComponentStreamEnumerableFactory = mockComponentStreamEnumerableFactory.Object;
+            detector.Logger = mockLogger.Object;
+
+            var scanResult = await detector.ExecuteDetectorAsync(scanRequest);
+            return (scanResult, componentRecorder);
+        }
+
+        /// 
+        /// This is used to override specific services that certain detectors use which aren't common/shared between all detectors.
+        /// For example: CommandLineExecutionService for the LinuxDetector and PathUtilityService for NpmWithRoots Detector.
+        /// 
+        /// 
+        /// 
+        public DetectorTestUtility WithDetector(T detector)
+        {
+            this.detector = detector;
+            return this;
+        }
+
+        public DetectorTestUtility WithLogger(Mock mockLogger)
+        {
+            this.mockLogger = mockLogger;
+            return this;
+        }
+
+        public DetectorTestUtility WithScanRequest(ScanRequest scanRequest)
+        {
+            this.scanRequest = scanRequest;
+            return this;
+        }
+
+        public DetectorTestUtility WithFile(string fileName, string fileContents, IEnumerable searchPatterns = null, string fileLocation = null)
+        {
+            return WithFile(fileName, fileContents.ToStream(), searchPatterns, fileLocation);
+        }
+
+        public DetectorTestUtility WithFile(string fileName, Stream fileContents, IEnumerable searchPatterns = null, string fileLocation = null)
+        {
+            if (string.IsNullOrEmpty(fileLocation))
+            {
+                fileLocation = Path.Combine(Path.GetTempPath(), fileName);
+            }
+
+            if (searchPatterns == null || !searchPatterns.Any())
+            {
+                searchPatterns = detector.SearchPatterns;
+            }
+
+            filesToAdd.Add((fileName, fileContents, fileLocation, searchPatterns));
+
+            return this;
+        }
+
+        public DetectorTestUtility WithComponentStreamEnumerableFactory(Mock mock)
+        {
+            mockComponentStreamEnumerableFactory = mock;
+            return this;
+        }
+
+        public DetectorTestUtility WithObservableDirectoryWalkerFactory(Mock mock)
+        {
+            mockObservableDirectoryWalkerFactory = mock;
+            return this;
+        }
+
+        private ProcessRequest CreateProcessRequest(string pattern, string filePath, Stream content)
+        {
+            return new ProcessRequest
+            {
+                SingleFileComponentRecorder = componentRecorder.CreateSingleFileComponentRecorder(filePath),
+                ComponentStream = CreateComponentStreamForFile(pattern, filePath, content),
+            };
+        }
+
+        private static IComponentStream CreateComponentStreamForFile(string pattern, string filePath, Stream content)
+        {
+            var getFileMock = new Mock();
+            getFileMock.SetupGet(x => x.Stream).Returns(content);
+            getFileMock.SetupGet(x => x.Pattern).Returns(pattern);
+            getFileMock.SetupGet(x => x.Location).Returns(filePath);
+
+            return getFileMock.Object;
+        }
+
+        private void InitializeFileRelatedMocksUsingDefaultImplementationIfNecessary()
+        {
+            bool useDefaultObservableDirectoryWalkerFactory = false, useDefaultComponentStreamEnumerableFactory = false;
+
+            if (mockObservableDirectoryWalkerFactory == null)
+            {
+                useDefaultObservableDirectoryWalkerFactory = true;
+                mockObservableDirectoryWalkerFactory = new Mock();
+            }
+
+            if (mockComponentStreamEnumerableFactory == null)
+            {
+                useDefaultComponentStreamEnumerableFactory = true;
+                mockComponentStreamEnumerableFactory = new Mock();
+            }
+
+            if (!filesToAdd.Any() && useDefaultObservableDirectoryWalkerFactory)
+            {
+                mockObservableDirectoryWalkerFactory.Setup(x =>
+                x.GetFilteredComponentStreamObservable(It.IsAny(), detector.SearchPatterns, It.IsAny()))
+                    .Returns(Enumerable.Empty().ToObservable());
+            }
+
+            if (!filesToAdd.Any() && useDefaultComponentStreamEnumerableFactory)
+            {
+                mockComponentStreamEnumerableFactory.Setup(x =>
+                x.GetComponentStreams(It.IsAny(), detector.SearchPatterns, It.IsAny(), It.IsAny()))
+                    .Returns(Enumerable.Empty());
+            }
+
+            var filesGroupedBySearchPattern = filesToAdd.GroupBy(filesToAdd => filesToAdd.searchPatterns, new EnumerableStringComparer());
+            foreach (var group in filesGroupedBySearchPattern)
+            {
+                var searchPatterns = group.Key;
+                var filesToSend = group.Select(grouping => (grouping.Name, grouping.Contents, grouping.Location));
+
+                if (useDefaultObservableDirectoryWalkerFactory)
+                {
+                    mockObservableDirectoryWalkerFactory.Setup(x =>
+                    x.GetFilteredComponentStreamObservable(It.IsAny(), searchPatterns, It.IsAny()))
+                        .Returns, IComponentRecorder>((directoryInfo, searchPatterns, componentRecorder) =>
+                        {
+                            return filesToSend
+                                .Select(fileToSend => CreateProcessRequest(FindMatchingPattern(fileToSend.Name, searchPatterns), fileToSend.Location, fileToSend.Contents)).ToObservable();
+                        });
+                }
+
+                if (useDefaultComponentStreamEnumerableFactory)
+                {
+                    mockComponentStreamEnumerableFactory.Setup(x =>
+                    x.GetComponentStreams(It.IsAny(), searchPatterns, It.IsAny(), It.IsAny()))
+                        .Returns, ExcludeDirectoryPredicate, bool>((directoryInfo, searchPatterns, excludeDirectoryPredicate, recurse) =>
+                        {
+                            if (recurse)
+                            {
+                                return filesToSend
+                                    .Select(fileToSend => CreateProcessRequest(FindMatchingPattern(fileToSend.Name, searchPatterns), fileToSend.Location, fileToSend.Contents)).Select(pr => pr.ComponentStream);
+                            }
+                            else
+                            {
+                                return filesToSend
+                                    .Where(fileToSend => Directory.GetParent(fileToSend.Location).FullName == directoryInfo.FullName)
+                                    .Select(fileToSend => CreateProcessRequest(FindMatchingPattern(fileToSend.Name, searchPatterns), fileToSend.Location, fileToSend.Contents)).Select(pr => pr.ComponentStream);
+                            } 
+                        });
+                }
+            }
+
+            var providedDetectorSearchPatterns = filesGroupedBySearchPattern.Any(group => group.Key.SequenceEqual(detector.SearchPatterns));
+            if (!providedDetectorSearchPatterns && useDefaultObservableDirectoryWalkerFactory)
+            {
+                mockObservableDirectoryWalkerFactory.Setup(x =>
+                    x.GetFilteredComponentStreamObservable(It.IsAny(), detector.SearchPatterns, It.IsAny()))
+                    .Returns(new List().ToObservable());
+            }
+
+            if (!providedDetectorSearchPatterns && useDefaultComponentStreamEnumerableFactory)
+            {
+                mockComponentStreamEnumerableFactory.Setup(x =>
+                    x.GetComponentStreams(It.IsAny(), detector.SearchPatterns, It.IsAny(), It.IsAny()))
+                    .Returns(new List());
+            }
+        }
+
+        private string FindMatchingPattern(string fileName, IEnumerable searchPatterns)
+        {
+            var foundPattern = searchPatterns.Where(searchPattern => new PathUtilityService().MatchesPattern(searchPattern, fileName)).FirstOrDefault();
+
+            return foundPattern != default(string) ? foundPattern : fileName;
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.TestsUtilities/DetectorTestUtilityCreator.cs b/test/Microsoft.ComponentDetection.TestsUtilities/DetectorTestUtilityCreator.cs
new file mode 100644
index 000000000..1597ea70e
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.TestsUtilities/DetectorTestUtilityCreator.cs
@@ -0,0 +1,13 @@
+using Microsoft.ComponentDetection.Contracts;
+
+namespace Microsoft.ComponentDetection.TestsUtilities
+{
+    public class DetectorTestUtilityCreator
+    {
+        public static DetectorTestUtility Create()
+            where T : FileComponentDetector, new()
+        {
+            return new DetectorTestUtility().WithDetector(new T());
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.TestsUtilities/EnumerableStringComparer.cs b/test/Microsoft.ComponentDetection.TestsUtilities/EnumerableStringComparer.cs
new file mode 100644
index 000000000..1ab3b85ed
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.TestsUtilities/EnumerableStringComparer.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace Microsoft.ComponentDetection.TestsUtilities
+{
+    // https://stackoverflow.com/questions/35128996/groupby-on-complex-object-e-g-listt
+    // This is used as the comparator for the detector utility when doing GroupBy on a List>
+    // Normally, it compares by object reference but we need to compare by the data in the lists
+    public class EnumerableStringComparer : IEqualityComparer>
+    {
+        public bool Equals([AllowNull] IEnumerable x, [AllowNull] IEnumerable y)
+        {
+            return x.SequenceEqual(y);
+        }
+
+        public int GetHashCode([DisallowNull] IEnumerable obj)
+        {
+            return obj.Aggregate(0, (a, y) => a ^ y.GetHashCode());
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.TestsUtilities/ExtensionMethods.cs b/test/Microsoft.ComponentDetection.TestsUtilities/ExtensionMethods.cs
new file mode 100644
index 000000000..d72d97763
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.TestsUtilities/ExtensionMethods.cs
@@ -0,0 +1,17 @@
+using System.IO;
+using System.Text;
+
+namespace Microsoft.ComponentDetection.TestsUtilities
+{
+    public static class ExtensionMethods
+    {
+        public static Stream ToStream(this string input)
+        {
+            var stream = new MemoryStream(Encoding.UTF8.GetBytes(input));
+
+            stream.Seek(0, SeekOrigin.Begin);
+
+            return stream;
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.TestsUtilities/Microsoft.ComponentDetection.TestsUtilities.csproj b/test/Microsoft.ComponentDetection.TestsUtilities/Microsoft.ComponentDetection.TestsUtilities.csproj
new file mode 100644
index 000000000..b8312e018
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.TestsUtilities/Microsoft.ComponentDetection.TestsUtilities.csproj
@@ -0,0 +1,10 @@
+
+
+    
+        
+        
+    
+	
+		true
+	
+
diff --git a/test/Microsoft.ComponentDetection.VerificationTests/ComponentDetectionIntegrationTests.cs b/test/Microsoft.ComponentDetection.VerificationTests/ComponentDetectionIntegrationTests.cs
new file mode 100644
index 000000000..5bf8146d9
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.VerificationTests/ComponentDetectionIntegrationTests.cs
@@ -0,0 +1,273 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using FluentAssertions;
+using FluentAssertions.Execution;
+using Microsoft.ComponentDetection.Contracts.BcdeModels;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Newtonsoft.Json;
+
+namespace Microsoft.ComponentDetection.VerificationTests
+{
+    [TestClass]
+    public class ComponentDetectionIntegrationTests 
+    {
+        private string oldLogFileContents;
+        private string newLogFileContents;
+        private DefaultGraphScanResult oldScanResult;
+        private DefaultGraphScanResult newScanResult;
+        private List bumpedDetectorVersions;
+        private double allowedTimeDriftRatio;
+
+        [TestInitialize]
+        public void GatherResources()
+        {
+            var oldGithubArtifactsDir = Environment.GetEnvironmentVariable("GITHUB_OLD_ARTIFACTS_DIR");
+            var newGithubArtifactsDir = Environment.GetEnvironmentVariable("GITHUB_NEW_ARTIFACTS_DIR");
+            var allowedTimeDriftRatioString = Environment.GetEnvironmentVariable("ALLOWED_TIME_DRIFT_RATIO");
+            allowedTimeDriftRatio = string.IsNullOrEmpty(allowedTimeDriftRatioString) ? .1 : double.Parse(allowedTimeDriftRatioString);
+
+            SetupGithub(oldGithubArtifactsDir, newGithubArtifactsDir);
+        }
+
+        [TestMethod]
+        public void LogFileHasNoErrors()
+        {
+            // make sure the new log does not contain any error messages.
+            int errorIndex = newLogFileContents.IndexOf("[ERROR]");
+            if (errorIndex >= 0)
+            {
+                // prints out the line that the error occured.
+                string errorMessage = $"An Error was found: {newLogFileContents.Substring(errorIndex, 200)}";
+                throw new Exception(errorMessage);
+            }
+        }
+
+        [TestMethod]
+        public void CheckManifestFiles_ExcludingExperimentalDetectors()
+        {
+            // can't just compare contents since the order of detectors is non deterministic.
+            // Parse out array of components
+            // make sure each component id has identical fields.
+            // if any are lost, error, new ones should come with a bumped detector version, which is checked during the detectors counts test.
+            var experimentalDetectorsId = GetExperimentalDetectorsId(newScanResult.DetectorsInScan, oldScanResult.DetectorsInScan);
+
+            var newComponents = newScanResult.ComponentsFound.Where(c => !experimentalDetectorsId.Contains(c.DetectorId));
+            var oldComponents = oldScanResult.ComponentsFound.Where(c => !experimentalDetectorsId.Contains(c.DetectorId));
+
+            Dictionary newComponentDictionary = GetComponentDictionary(newComponents);
+            Dictionary oldComponentDictionary = GetComponentDictionary(oldComponents);
+            using (new AssertionScope())
+            {
+                CompareDetectedComponents(oldComponents, newComponentDictionary, "new");
+                CompareDetectedComponents(newComponents, oldComponentDictionary, "old");
+                DependencyGraphCollection oldGraphs = oldScanResult.DependencyGraphs;
+                DependencyGraphCollection newGraphs = newScanResult.DependencyGraphs;
+                CompareGraphs(oldGraphs, newGraphs, "old", "new");
+                CompareGraphs(newGraphs, oldGraphs, "new", "old");
+            }
+        }
+
+        private ISet GetExperimentalDetectorsId(IEnumerable newScanDetectors, IEnumerable oldScanDetectors)
+        {
+            var experimentalDetectorsId = new HashSet();
+
+            foreach (var detector in newScanDetectors)
+            {
+                if (detector.IsExperimental)
+                {
+                    experimentalDetectorsId.Add(detector.DetectorId);
+                }
+            }
+
+            foreach (var detector in oldScanDetectors)
+            {
+                if (detector.IsExperimental)
+                {
+                    experimentalDetectorsId.Add(detector.DetectorId);
+                }
+            }
+
+            return experimentalDetectorsId;
+        }
+
+        private void CompareDetectedComponents(IEnumerable leftComponents, Dictionary rightComponentDictionary, string rightFileName)
+        {
+            foreach (var leftComponent in leftComponents)
+            {
+                var foundComponent = rightComponentDictionary.TryGetValue(GetKey(leftComponent), out var rightComponent);
+                if (!foundComponent)
+                {
+                    foundComponent.Should().BeTrue($"The component for {GetKey(leftComponent)} was not present in the {rightFileName} manifest file. Verify this is expected behavior before proceeding");
+                }
+
+                if (leftComponent.IsDevelopmentDependency != null)
+                {
+                    leftComponent.IsDevelopmentDependency.Should().Be(rightComponent.IsDevelopmentDependency, $"Component: {GetKey(rightComponent)} has a different \"DevelopmentDependency\".");
+                }
+            }
+        }
+
+        private void CompareGraphs(DependencyGraphCollection leftGraphs, DependencyGraphCollection newGraphs, string leftGraphName, string rightGraphName)
+        {
+            foreach (var leftGraph in leftGraphs)
+            {
+                newGraphs.TryGetValue(leftGraph.Key, out var rightGraph).Should().BeTrue($"File {leftGraph.Key} is in the {leftGraphName} dependency graph, but not in the {rightGraphName} one.");
+
+                if (rightGraph == null)
+                {
+                    // the rest of test depends on rightDependencies, if it is null a 
+                    // NullReferenceException is going to be thrown stopping the verification process
+                    // the previous test that validate its existance is going to include a meaningfull message
+                    // in the test summary
+                    continue;
+                }
+
+                foreach (var leftComponent in leftGraph.Value.ExplicitlyReferencedComponentIds)
+                {
+                    rightGraph.ExplicitlyReferencedComponentIds.Should().Contain(leftComponent, $"Component {leftComponent} was explicitly referenced in the {leftGraphName} dependency graph, but is not in the {rightGraphName} one.");
+                }
+
+                foreach (var leftComponent in leftGraph.Value.Graph)
+                {
+                    rightGraph.Graph.TryGetValue(leftComponent.Key, out var rightDependencies).Should().BeTrue($"Component {leftComponent} was in the {leftGraphName} dependency graph, but is not in the {rightGraphName} one.");
+
+                    if (rightDependencies == null)
+                    {
+                        // the rest of test depends on rightDependencies, if it is null a 
+                        // NullReferenceException is going to be thrown stopping the verification process
+                        continue;
+                    }
+
+                    var leftDependenciesGraph = leftGraph.Value.Graph[leftComponent.Key];
+                    if (leftDependenciesGraph != null)
+                    {
+                        var leftDependencies = leftGraph.Value.Graph[leftComponent.Key];
+                        foreach (var leftDependency in leftDependencies)
+                        {
+                            rightDependencies.Should().Contain(leftDependency, $"Component dependency {leftDependency} for component {leftComponent} was not in the {rightGraphName} dependency graph.");
+                        }
+                        
+                        leftDependencies.Should().BeEquivalentTo(rightDependencies, $"{rightGraphName} has the following components that were not found in {leftGraphName}, please verify this is expected behavior. {JsonConvert.SerializeObject(rightDependencies.Except(leftDependencies))}");
+                    }
+                }
+            }
+        }
+
+        private Dictionary GetComponentDictionary(IEnumerable scannedComponents)
+        {
+            // The Maven detector currently returns duplicate components in some cases, so we do this to insulate.
+            var grouping = scannedComponents.GroupBy(x => GetKey(x));
+            return grouping.ToDictionary(x => x.Key, x => x.First());
+        }
+
+        private string GetKey(ScannedComponent component)
+        {
+            return $"{component.DetectorId}-{component.Component.Id}";
+        }
+
+        [TestMethod]
+        public void CheckDetectorsRunTimesAndCounts()
+        {
+            // makes sure that all detectors have the same number of components found.
+            // if some are lost, error. 
+            // if some are new, check if version of detector is updated. if it isn't error 
+            // Run times should be fairly close to identical. errors if there is an increase of more than 5%
+            using (new AssertionScope())
+            {
+                ProcessDetectorVersions();
+                string regexPattern = @"Detection time: (\w+\.\w+) seconds. |(\w+ *[\w()]+) *\|(\w+\.*\w*) seconds *\|(\d+)";
+                var oldMatches = Regex.Matches(oldLogFileContents, regexPattern);
+                var newMatches = Regex.Matches(newLogFileContents, regexPattern);
+
+                newMatches.Should().HaveCountGreaterOrEqualTo(oldMatches.Count, "A detector was lost, make sure this was intentional.");
+
+                var detectorTimes = new Dictionary();
+                var detectorCounts = new Dictionary();
+                foreach (Match match in oldMatches)
+                {
+                    if (!match.Groups[2].Success)
+                    {
+                        detectorTimes.Add("TotalTime", float.Parse(match.Groups[1].Value));
+                    }
+                    else
+                    {
+                        string detectorId = match.Groups[2].Value;
+                        detectorTimes.Add(detectorId, float.Parse(match.Groups[3].Value));
+                        detectorCounts.Add(detectorId, int.Parse(match.Groups[4].Value));
+                    }
+                }
+
+                // fail at the end to gather all failures instead of just the first.
+                foreach (Match match in newMatches)
+                {
+                    // for each detector and overall, make sure the time doesn't increase by more than 10%
+                    // for each detector make sure component counts do not change. if they increase, make sure the version of the detector was bumped.
+                    if (!match.Groups[2].Success)
+                    {
+                        detectorTimes.TryGetValue("TotalTime", out var oldTime);
+                        float newTime = float.Parse(match.Groups[1].Value);
+
+                        float maxTimeThreshold = (float)(oldTime + Math.Max(5, oldTime * allowedTimeDriftRatio));
+                        newTime.Should().BeLessOrEqualTo(maxTimeThreshold, $"Total Time take increased by a large amount. Please verify before continuing. old time: {oldTime}, new time: {newTime}");
+                    }
+                    else
+                    {
+                        string detectorId = match.Groups[2].Value;
+                        int newCount = int.Parse(match.Groups[4].Value);
+                        if (detectorCounts.TryGetValue(detectorId, out var oldCount))
+                        {
+                            newCount.Should().BeGreaterOrEqualTo(oldCount, $"{oldCount - newCount} Components were lost for detector {detectorId}. Verify this is expected behavior. \n Old Count: {oldCount}, PPE Count: {newCount}");
+
+                            (newCount > oldCount && !bumpedDetectorVersions.Contains(detectorId)).Should().BeFalse($"{newCount - oldCount} New Components were found for detector {detectorId}, but the detector version was not updated.");
+                        }
+                    }
+                }
+            }
+        }
+
+        private void ProcessDetectorVersions()
+        {
+            var oldDetectors = oldScanResult.DetectorsInScan;
+            var newDetectors = newScanResult.DetectorsInScan;
+            bumpedDetectorVersions = new List();
+            foreach (var cd in oldDetectors)
+            {
+                var newDetector = newDetectors.FirstOrDefault(det => det.DetectorId == cd.DetectorId);
+
+                if (newDetector == null)
+                {
+                    newDetector.Should().NotBeNull($"the detector {cd.DetectorId} was lost, verify this is expected behavior");
+                    continue;
+                }
+
+                newDetector.Version.Should().BeGreaterOrEqualTo(cd.Version, $"the version for detector {cd.DetectorId} was unexpectedly reduced. please check all detector versions and verify this behavior.");
+
+                if (newDetector.Version > cd.Version)
+                {
+                    bumpedDetectorVersions.Add(cd.DetectorId);
+                }
+
+                cd.SupportedComponentTypes.Should().OnlyContain(type => newDetector.SupportedComponentTypes.Contains(type), "the detector {cd.DetectorId} has lost suppported component types. Verify this is expected behavior.");
+            }
+        }
+
+        private void SetupGithub(string oldGithubArtifactsDir, string newGithubArtifactsDir)
+        {
+            var oldGithubDirectory = new DirectoryInfo(oldGithubArtifactsDir);
+            oldLogFileContents = GetFileTextWithPattern("GovCompDisc_Log*.log", oldGithubDirectory);
+            oldScanResult = JsonConvert.DeserializeObject(GetFileTextWithPattern("ScanManifest*.json", oldGithubDirectory));
+
+            var newGithubDirectory = new DirectoryInfo(newGithubArtifactsDir);
+            newLogFileContents = GetFileTextWithPattern("GovCompDisc_Log*.log", newGithubDirectory);
+            newScanResult = JsonConvert.DeserializeObject(GetFileTextWithPattern("ScanManifest*.json", newGithubDirectory));
+        }
+
+        private string GetFileTextWithPattern(string pattern, DirectoryInfo directory)
+        {
+            return directory.GetFiles(pattern).Single().OpenText().ReadToEnd();
+        }
+    }
+}
diff --git a/test/Microsoft.ComponentDetection.VerificationTests/Microsoft.DependencyDetective.VerificationTests.csproj b/test/Microsoft.ComponentDetection.VerificationTests/Microsoft.DependencyDetective.VerificationTests.csproj
new file mode 100644
index 000000000..1a8cfaca8
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.VerificationTests/Microsoft.DependencyDetective.VerificationTests.csproj
@@ -0,0 +1,28 @@
+
+
+    
+        true
+        UnitTest
+        {95c8e34e-5895-4c7e-8c7e-c300a114a1e5}
+        Library
+        ./bin
+        Microsoft.ComponentDetection.VerificationTests
+        Microsoft.ComponentDetection.VerificationTests
+    
+
+    
+        
+        
+        
+        
+        
+    
+
+    
+        
+    
+
+
\ No newline at end of file