Skip to content

Commit 4bea950

Browse files
authored
Merge pull request #5993 from nscuro/ecosystem-aware-latest-version-detection
Use ecosystem-aware version comparison for latest version detection
2 parents efc8733 + d89365f commit 4bea950

File tree

2 files changed

+100
-68
lines changed

2 files changed

+100
-68
lines changed

src/main/java/org/dependencytrack/tasks/repositories/ComposerMetaAnalyzer.java

Lines changed: 34 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@
2222
import com.github.benmanes.caffeine.cache.Cache;
2323
import com.github.benmanes.caffeine.cache.Caffeine;
2424
import com.github.packageurl.PackageURL;
25+
import io.github.nscuro.versatile.VersionFactory;
26+
import io.github.nscuro.versatile.spi.InvalidVersionException;
27+
import io.github.nscuro.versatile.spi.Version;
28+
import io.github.nscuro.versatile.version.KnownVersioningSchemes;
2529
import jakarta.ws.rs.core.UriBuilder;
2630
import org.apache.http.HttpStatus;
2731
import org.apache.http.client.methods.CloseableHttpResponse;
2832
import org.apache.http.util.EntityUtils;
29-
import org.apache.maven.artifact.versioning.ComparableVersion;
3033
import org.dependencytrack.exception.MetaAnalyzerException;
3134
import org.dependencytrack.model.Component;
3235
import org.dependencytrack.model.RepositoryType;
@@ -52,9 +55,9 @@ public class ComposerMetaAnalyzer extends AbstractMetaAnalyzer {
5255

5356
/**
5457
* @see <a href="https://packagist.org/apidoc#get-package-data">Packagist's API
55-
* doc for
56-
* "Getting package data - Using the Composer v1 metadata (DEPRECATED)"</a>
57-
* Example: https://repo.packagist.org/p/monolog/monolog.json
58+
* doc for
59+
* "Getting package data - Using the Composer v1 metadata (DEPRECATED)"</a>
60+
* Example: https://repo.packagist.org/p/monolog/monolog.json
5861
*/
5962
private static final String PACKAGE_META_DATA_PATH_PATTERN_V1 = "/p/%package%.json";
6063

@@ -66,10 +69,10 @@ public class ComposerMetaAnalyzer extends AbstractMetaAnalyzer {
6669
* Some of the properties of the root package.json are documented at
6770
* https://github.com/composer/composer/blob/main/doc/05-repositories.md
6871
* Properties to investigate / implement:
69-
*
72+
* <p>
7073
* - security-advisories: very relevant, but only in a VulnerabilityAnalyzer (or
7174
* mirrored VulnerabilitySource) context
72-
*
75+
* <p>
7376
* - providers-lazy-url: old v1 construct for which I haven't seen any example,
7477
* in v2 the metadata-url is used for this. seems like it's not relevant for DT
7578
* - list: returns only package names, seems like repo.packagist.org (and .com?)
@@ -273,7 +276,7 @@ private void loadIncludedPackages(final JSONObject repoRoot, final JSONObject da
273276
}
274277

275278
private MetaModel analyzeFromMetadataUrl(final MetaModel meta, final Component component,
276-
final String packageMetaDataPathPattern) {
279+
final String packageMetaDataPathPattern) {
277280
final String composerPackageMetadataFilename = packageMetaDataPathPattern.replaceAll("%package%",
278281
getComposerPackageName(component));
279282
final String url;
@@ -307,7 +310,7 @@ private MetaModel analyzeFromMetadataUrl(final MetaModel meta, final Component c
307310
if (!responsePackages.has(expectedResponsePackage)) {
308311
// the package no longer exists - for v2 there's no example (yet), v1 example
309312
// https://repo.packagist.org/p/magento/adobe-ims.json
310-
LOGGER.debug("%s: Package no longer exists in repository %s.". formatted(component.getPurl(), this.repositoryId));
313+
LOGGER.debug("%s: Package no longer exists in repository %s.".formatted(component.getPurl(), this.repositoryId));
311314
return meta;
312315
}
313316

@@ -353,38 +356,40 @@ private JSONObject expandPackages(JSONObject packages) {
353356
}
354357

355358
private MetaModel analyzePackageVersions(final MetaModel meta, Component component, JSONObject packageVersions) {
356-
final ComparableVersion latestVersion = new ComparableVersion(stripLeadingV(component.getPurl().getVersion()));
359+
Version latestVersion = null;
357360
final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
358361

359362
LOGGER.debug("%s: analyzing package versions in %s: ".formatted(component.getPurl(), this.repositoryId));
360-
packageVersions.keySet().forEach(item -> {
361-
JSONObject packageVersion = packageVersions.getJSONObject((String) item);
363+
for (final String item : packageVersions.keySet()) {
364+
final JSONObject packageVersion = packageVersions.getJSONObject(item);
362365
// Sometimes the JSON key differs from the the version inside the JSON value. The latter is leading.
363-
String version = packageVersion.getString("version");
364-
if (version.startsWith("dev-") || version.endsWith("-dev")) {
365-
// dev versions are excluded, since they are not pinned but a VCS-branch.
366-
// this case doesn't seem to happen anymore with V2, as dev (untagged) releases
367-
// are not part of the response anymore
368-
return;
369-
}
366+
final String version = packageVersion.getString("version");
370367

371368
// Some (old?) repositories like composer.amasty.com/enterprise do not include a
372-
// 'version_normalized' field
373-
// TODO Should we attempt to normalize ourselves? The PHP code uses something
374-
// that results in 4 parts instead of 3, i.e. 2.3.8.0 instead of 2.3.8. Not sure
375-
// if that works with Semver4j
376-
String version_normalized = packageVersion.getString("version");
369+
// 'version_normalized' field. versatile handles both 3- and 4-part version
370+
// strings (e.g. 2.3.8 and 2.3.8.0) natively, so falling back to the raw version
371+
// is safe.
372+
String version_normalized = version;
377373
if (packageVersion.has("version_normalized")) {
378374
version_normalized = packageVersion.getString("version_normalized");
379375
}
380376

381-
ComparableVersion currentComparableVersion = new ComparableVersion(version_normalized);
382-
if (currentComparableVersion.compareTo(latestVersion) < 0) {
383-
// smaller version can be skipped
384-
return;
377+
final Version currentVersion;
378+
try {
379+
currentVersion = VersionFactory.forScheme(KnownVersioningSchemes.SCHEME_COMPOSER, version_normalized);
380+
} catch (InvalidVersionException e) {
381+
LOGGER.debug("%s: Skipping unparseable Composer version %s in repository %s".formatted(component.getPurl(), version_normalized, this.repositoryId), e);
382+
continue;
383+
}
384+
if (!currentVersion.isStable()) {
385+
continue;
386+
}
387+
388+
if (latestVersion != null && currentVersion.compareTo(latestVersion) < 0) {
389+
continue;
385390
}
386391

387-
latestVersion.parseVersion(stripLeadingV(version_normalized));
392+
latestVersion = currentVersion;
388393
meta.setLatestVersion(version);
389394

390395
if (packageVersion.has("time")) {
@@ -401,16 +406,10 @@ private MetaModel analyzePackageVersions(final MetaModel meta, Component compone
401406
// do not include the name field for a version, so print purl
402407
LOGGER.warn("%s: Field 'time' not present in metadata in repository %s".formatted(component.getPurl(), this.repositoryId));
403408
}
404-
});
409+
}
405410
return meta;
406411
}
407412

408-
private static String stripLeadingV(String s) {
409-
return s.startsWith("v") || s.startsWith("V")
410-
? s.substring(1)
411-
: s;
412-
}
413-
414413
private static boolean isMinified(JSONObject data) {
415414
if (data.has("minified") && "composer/2.0".equals(data.getString("minified"))) {
416415
return true;

0 commit comments

Comments
 (0)