diff --git a/src/Shared/PackageManagers/BaseProjectManager.cs b/src/Shared/PackageManagers/BaseProjectManager.cs index 5cd460fd..3282b8b3 100644 --- a/src/Shared/PackageManagers/BaseProjectManager.cs +++ b/src/Shared/PackageManagers/BaseProjectManager.cs @@ -362,6 +362,17 @@ public virtual async Task PackageVersionExistsAsync(PackageURL purl, bool return (await EnumerateVersionsAsync(purl, useCache)).Contains(purl.Version); } + + /// + /// Check if the package version was pulled from the repository. + /// + /// The PackageURL to check. + /// If the cache should be checked for the existence of this package. + /// True if the package was pulled from the repository. False otherwise. + public virtual Task PackageVersionPulled(PackageURL purl, bool useCache = true) + { + throw new NotImplementedException("BaseProjectManager does not implement PackageVersionPulled."); + } /// /// Static overload for getting the latest version. diff --git a/src/Shared/PackageManagers/NPMProjectManager.cs b/src/Shared/PackageManagers/NPMProjectManager.cs index 834b246a..0818db54 100644 --- a/src/Shared/PackageManagers/NPMProjectManager.cs +++ b/src/Shared/PackageManagers/NPMProjectManager.cs @@ -31,6 +31,8 @@ public class NPMProjectManager : TypedManager? actions = null, @@ -467,6 +469,37 @@ public override List GetVersions(JsonDocument? contentJSON) return allVersions; } + public override async Task PackageVersionPulled(PackageURL purl, bool useCache = true) + { + string? content = await GetMetadataAsync(purl, useCache); + if (string.IsNullOrEmpty(content)) { return false; } + + JsonDocument contentJSON = JsonDocument.Parse(content); + JsonElement root = contentJSON.RootElement; + if (root.TryGetProperty("time", out JsonElement time)) + { + if (time.TryGetProperty("unpublished", out JsonElement unpublished)) + { + List? versions = OssUtilities.ConvertJSONToList(OssUtilities.GetJSONPropertyIfExists(unpublished, "versions")); + return versions?.Contains(purl.Version) ?? false; + } + } + return false; + } + + /// + /// Check to see if the package only has one version, and if that version is a NPM security holding package. + /// + /// The to check. + /// If the cache should be checked for the existence of this package. + /// True if this package is a NPM security holding package. False otherwise. + public async Task PackageSecurityHolding(PackageURL purl, bool useCache = true) + { + List versions = (await this.EnumerateVersionsAsync(purl, useCache)).ToList(); + + return versions.Count == 1 && versions[0].Equals(NPM_SECURITY_HOLDING_VERSION); + } + /// /// Searches the package manager metadata to figure out the source code repository /// @@ -474,7 +507,6 @@ public override List GetVersions(JsonDocument? contentJSON) /// /// A dictionary, mapping each possible repo source entry to its probability/empty dictionary /// - protected override async Task> SearchRepoUrlsInPackageMetadata(PackageURL purl, string metadata) { diff --git a/src/oss-tests/ProjectManagerTests/NPMProjectManagerTests.cs b/src/oss-tests/ProjectManagerTests/NPMProjectManagerTests.cs index 7a0da3cf..c2ebf0b0 100644 --- a/src/oss-tests/ProjectManagerTests/NPMProjectManagerTests.cs +++ b/src/oss-tests/ProjectManagerTests/NPMProjectManagerTests.cs @@ -26,6 +26,8 @@ public class NPMProjectManagerTests private readonly IDictionary _packages = new Dictionary() { { "https://registry.npmjs.org/lodash", Resources.lodash_json }, + { "https://registry.npmjs.org/lodash.js", Resources.lodashjs_json }, + { "https://registry.npmjs.org/%40somosme/webflowutils", Resources.unpublishedpackage_json }, { "https://registry.npmjs.org/%40angular/core", Resources.angular_core_json }, { "https://registry.npmjs.org/ds-modal", Resources.ds_modal_json }, { "https://registry.npmjs.org/monorepolint", Resources.monorepolint_json }, @@ -94,6 +96,7 @@ public async Task EnumerateVersionsSucceeds(string purlString, int count, string [DataRow("pkg:npm/monorepolint@0.4.0")] [DataRow("pkg:npm/example@0.0.0")] [DataRow("pkg:npm/rly-cli@0.0.2")] + [DataRow("pkg:npm/lodash.js@0.0.1-security")] public async Task PackageVersionExistsAsyncSucceeds(string purlString) { PackageURL purl = new(purlString); @@ -111,6 +114,27 @@ public async Task PackageVersionDoesntExistsAsyncSucceeds(string purlString) Assert.IsFalse(await _projectManager.PackageVersionExistsAsync(purl, useCache: false)); } + [DataTestMethod] + [DataRow("pkg:npm/%40somosme/webflowutils@1.0.0")] + [DataRow("pkg:npm/%40somosme/webflowutils@1.2.3", false)] + public async Task PackageVersionPulledAsync(string purlString, bool expectedPulled = true) + { + PackageURL purl = new(purlString); + + Assert.AreEqual(expectedPulled, await _projectManager.PackageVersionPulled(purl, useCache: false)); + } + + [DataTestMethod] + [DataRow("pkg:npm/lodash.js")] + [DataRow("pkg:npm/lodash.js@1.0.0")] + [DataRow("pkg:npm/lodash", false)] + public async Task PackageSecurityHoldingAsync(string purlString, bool expectedToHaveSecurityHolding = true) + { + PackageURL purl = new(purlString); + + Assert.AreEqual(expectedToHaveSecurityHolding, await _projectManager.PackageSecurityHolding(purl, useCache: false)); + } + [DataTestMethod] [DataRow("pkg:npm/lodash@4.17.15", "2019-07-19T02:28:46.584Z")] [DataRow("pkg:npm/%40angular/core@13.2.5", "2022-03-02T18:25:31.169Z")] diff --git a/src/oss-tests/Properties/Resources.Designer.cs b/src/oss-tests/Properties/Resources.Designer.cs index f7e7d34b..7545228d 100644 --- a/src/oss-tests/Properties/Resources.Designer.cs +++ b/src/oss-tests/Properties/Resources.Designer.cs @@ -96,6 +96,15 @@ internal class Resources { } } + /// + /// Looks up a localized string similar to {"_id":"lodash.js","_rev":"7-b033eedf4d1b14d686a8c537585bb116","time":{"4.17.16":"2019-11-25T16:45:29.666Z","created":"2019-11-25T18:50:27.244Z","0.0.1-security":"2019-11-25T18:50:27.445Z","modified":"2022-05-08T08:11:30.890Z"},"name":"lodash.js","dist-tags":{"latest":"0.0.1-security"},"versions":{"0.0.1-security":{"name":"lodash.js","version":"0.0.1-security","description":"security holding package","repository":{"type":"git","url":"git+https://github.com/npm/security-holder.git"},"gitHead":"ac50be87aafecb [rest of string was truncated]";. + /// + internal static string lodashjs_json { + get { + return ResourceManager.GetString("lodashjs.json", resourceCulture); + } + } + /// /// Looks up a localized string similar to {"_id":"example","name":"example","dist-tags":{"latest":"0.0.0"},"versions":{"0.0.0":{"name":"example","version":"0.0.0","_id":"example@0.0.0","_npmVersion":"1.0.0","_npmUser":{"name":"test","email":"test@microsoft.com"}}}}. /// @@ -214,5 +223,14 @@ internal class Resources { return ResourceManager.GetString("rly-cli.json", resourceCulture); } } + + /// + /// Looks up a localized string similar to {"_id":"@somosme/webflowutils","_rev":"3-9be941baa0509bbc96a1349f8464fd6d","name":"@somosme/webflowutils","time":{"created":"2022-08-10T19:49:56.913Z","1.0.0":"2022-08-10T19:49:57.198Z","modified":"2022-08-10T21:31:52.293Z","unpublished":{"time":"2022-08-10T21:31:32.856Z","versions":["1.0.0"]}},"maintainers":[{"email":"antonio@somos.me","name":"antoniomb"}]}. + /// + internal static string unpublishedpackage_json { + get { + return ResourceManager.GetString("unpublishedpackage.json", resourceCulture); + } + } } } diff --git a/src/oss-tests/Properties/Resources.resx b/src/oss-tests/Properties/Resources.resx index 2c5a9a0e..269eb3a7 100644 --- a/src/oss-tests/Properties/Resources.resx +++ b/src/oss-tests/Properties/Resources.resx @@ -27,6 +27,9 @@ ..\TestData\NPM\lodash.json;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\TestData\NPM\lodashjs.json;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + ..\TestData\NPM\monorepolint.json;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 @@ -36,6 +39,9 @@ ..\TestData\NPM\minimum_json.json;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\TestData\NPM\unpublishedpackage.json;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + ..\TestData\NuGet\razorengine\index.json;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 diff --git a/src/oss-tests/TestData/NPM/lodashjs.json b/src/oss-tests/TestData/NPM/lodashjs.json new file mode 100644 index 00000000..bf25e575 --- /dev/null +++ b/src/oss-tests/TestData/NPM/lodashjs.json @@ -0,0 +1 @@ +{"_id":"lodash.js","_rev":"7-b033eedf4d1b14d686a8c537585bb116","time":{"4.17.16":"2019-11-25T16:45:29.666Z","created":"2019-11-25T18:50:27.244Z","0.0.1-security":"2019-11-25T18:50:27.445Z","modified":"2022-05-08T08:11:30.890Z"},"name":"lodash.js","dist-tags":{"latest":"0.0.1-security"},"versions":{"0.0.1-security":{"name":"lodash.js","version":"0.0.1-security","description":"security holding package","repository":{"type":"git","url":"git+https://github.com/npm/security-holder.git"},"gitHead":"ac50be87aafecba67fcacca3b32bf36a1f8f7a71","bugs":{"url":"https://github.com/npm/security-holder/issues"},"homepage":"https://github.com/npm/security-holder#readme","_id":"lodash.js@0.0.1-security","_nodeVersion":"11.14.0","_npmVersion":"6.13.0","dist":{"integrity":"sha512-YHdFXeKSXAHuY5pusUO4sltubTuLrTqhatFgdKuiSisSuQ/UdrQNlCtgB+CDnzgsNp/Y3Mdfa3K4rInyEDHugg==","shasum":"9f4bddb7df0b7e36b101c2054549300d38b6f518","tarball":"https://registry.npmjs.org/lodash.js/-/lodash.js-0.0.1-security.tgz","fileCount":2,"unpackedSize":469,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJd3CJzCRA9TVsSAnZWagAAN7kP/inmZqIyY4YHkc+HuG/4\n4zZTOiweO+duew6VZbIT7/fvPydBFuPuTBjmBC9D/JdDG10dgFxmxOf9RMDI\nwW4B1XQn8bc/6BNkaWXDFkJXlOsu60X08wvntRPRr+FNRjwyMvfyzrg3ad44\nnalTEr2PQpNyP+sDhamEgUXjIqGXM7wNCrcI0kf8gB0aqR/WH8fKYKFtVhHs\nF4kL0sUwiYzhwdImgc1ZmYEV1d9Gcnam+OiLImL7HCljG3SXmWNlpKOuRmUp\n3G2bz6mz9bs+QsmPDFXh4rg44ErplTuRXd4rUGbLFSyHqz7k8oBdmzzUQmu4\nKcvQmTC9UPrq989s8QEnnmlx48OcH99Ig6cMtIj7ZkoscpH/KSH4Bvc65CtQ\nFzqsSLv3jlU/TgRjTxNwMOurjNE4FbQOKb9ICC1OYVGv3cIG31rrzcwGzrX6\ndhsinq47C7AdKYcmTOUqhCSQwqonbrDUO/nKtIpQ1t3shxx3KKUef4DOmYcN\nu36+h8mNR8De6fqLxBNRhazV5YG4Sfab+CQ+r7KvEusrT//lJpnUw2rqGHW3\naqSypAfwjvodnr0Mwh6pNhB3eUAO9on9t/MfB39NVh5kpa3mW+pT4Q558Mnh\ncsGKmTygH+fWmzA1ty/VGRUSHqrMwOAdEV81xo0UXIennkKwfB/WBu7/EjCm\nuqJ+\r\n=yUZ9\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIHsnZDHbTNbN1PrubcwvLrhEqTjvHi0ym/5AxK5Fpr7cAiEAnzTEI7y2HFivNLF+5L9OBGlf0/5mmDwOrFO0hkf0JqY="}]},"maintainers":[{"name":"andreeleuterio","email":"andre@npmjs.com"}],"_npmUser":{"name":"andreeleuterio","email":"andre@npmjs.com"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/lodash.js_0.0.1-security_1574707827244_0.7707519668716685"},"_hasShrinkwrap":false}},"maintainers":[{"email":"npm@npmjs.com","name":"npm"},{"email":"andre@npmjs.com","name":"andreeleuterio"}],"description":"security holding package","homepage":"https://github.com/npm/security-holder#readme","repository":{"type":"git","url":"git+https://github.com/npm/security-holder.git"},"bugs":{"url":"https://github.com/npm/security-holder/issues"},"readme":"# Security holding package\n\nThis package name is not currently in use, but was formerly occupied\nby another package. To avoid malicious use, npm is hanging on to the\npackage name, but loosely, and we'll probably give it to you if you\nwant it.\n\nYou may adopt this package by contacting support@npmjs.com and\nrequesting the name.\n","readmeFilename":"README.md"} \ No newline at end of file diff --git a/src/oss-tests/TestData/NPM/unpublishedpackage.json b/src/oss-tests/TestData/NPM/unpublishedpackage.json new file mode 100644 index 00000000..ab4cb39d --- /dev/null +++ b/src/oss-tests/TestData/NPM/unpublishedpackage.json @@ -0,0 +1 @@ +{"_id":"@somosme/webflowutils","_rev":"3-9be941baa0509bbc96a1349f8464fd6d","name":"@somosme/webflowutils","time":{"created":"2022-08-10T19:49:56.913Z","1.0.0":"2022-08-10T19:49:57.198Z","modified":"2022-08-10T21:31:52.293Z","unpublished":{"time":"2022-08-10T21:31:32.856Z","versions":["1.0.0"]}},"maintainers":[{"email":"antonio@somos.me","name":"antoniomb"}]} \ No newline at end of file