diff --git a/src/main/java/org/dependencytrack/model/Repository.java b/src/main/java/org/dependencytrack/model/Repository.java index 3cd7b31cb5..a8ed870aa5 100644 --- a/src/main/java/org/dependencytrack/model/Repository.java +++ b/src/main/java/org/dependencytrack/model/Repository.java @@ -86,6 +86,11 @@ public class Repository implements Serializable { @NotNull private Boolean internal; // New column, must allow nulls on existing databases + //New column to determine if authentication is required for a repository + @Persistent + @Column(name = "AUTHENTICATIONREQUIRED", allowsNull = "true") + private Boolean authenticationRequired; + @Persistent @Column(name = "USERNAME") @JsonDeserialize(using = TrimmedStringDeserializer.class) @@ -97,7 +102,8 @@ public class Repository implements Serializable { @Persistent(customValueStrategy = "uuid") @Index(name = "REPOSITORY_UUID_IDX") // Cannot be @Unique. Microsoft SQL Server throws an exception - @Column(name = "UUID", jdbcType = "VARCHAR", length = 36, allowsNull = "true") // New column, must allow nulls on existing databases + @Column(name = "UUID", jdbcType = "VARCHAR", length = 36, allowsNull = "true") + // New column, must allow nulls on existing databases @NotNull private UUID uuid; @@ -157,6 +163,14 @@ public void setInternal(Boolean internal) { this.internal = internal; } + public Boolean isAuthenticationRequired() { + return authenticationRequired; + } + + public void setAuthenticationRequired(Boolean authenticationRequired) { + this.authenticationRequired = authenticationRequired; + } + public String getUsername() { return username; } diff --git a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java index 7bb482ce60..d78875b22e 100644 --- a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java +++ b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java @@ -18,11 +18,11 @@ */ package org.dependencytrack.persistence; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; +import alpine.common.logging.Logger; +import alpine.model.ManagedUser; +import alpine.model.Permission; +import alpine.model.Team; +import alpine.server.auth.PasswordService; import org.dependencytrack.RequirementsVerifier; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.ConfigPropertyConstants; @@ -32,11 +32,12 @@ import org.dependencytrack.parser.spdx.json.SpdxLicenseDetailParser; import org.dependencytrack.persistence.defaults.DefaultLicenseGroupImporter; import org.dependencytrack.util.NotificationUtil; -import alpine.common.logging.Logger; -import alpine.model.ManagedUser; -import alpine.model.Permission; -import alpine.model.Team; -import alpine.server.auth.PasswordService; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; /** * Creates default objects on an empty database. @@ -110,7 +111,7 @@ private void loadDefaultLicenses() { private void loadDefaultLicenseGroups() { try (QueryManager qm = new QueryManager()) { final DefaultLicenseGroupImporter importer = new DefaultLicenseGroupImporter(qm); - if (! importer.shouldImport()) { + if (!importer.shouldImport()) { return; } LOGGER.info("Adding default license group definitions to datastore"); @@ -180,7 +181,7 @@ private void loadDefaultPersonas() { private List getPortfolioManagersPermissions(final List fullList) { final List permissions = new ArrayList<>(); - for (final Permission permission: fullList) { + for (final Permission permission : fullList) { if (permission.getName().equals(Permissions.Constants.VIEW_PORTFOLIO) || permission.getName().equals(Permissions.Constants.PORTFOLIO_MANAGEMENT)) { permissions.add(permission); @@ -191,7 +192,7 @@ private List getPortfolioManagersPermissions(final List private List getAutomationPermissions(final List fullList) { final List permissions = new ArrayList<>(); - for (final Permission permission: fullList) { + for (final Permission permission : fullList) { if (permission.getName().equals(Permissions.Constants.VIEW_PORTFOLIO) || permission.getName().equals(Permissions.Constants.BOM_UPLOAD)) { permissions.add(permission); @@ -206,21 +207,21 @@ private List getAutomationPermissions(final List fullLis private void loadDefaultRepositories() { try (QueryManager qm = new QueryManager()) { LOGGER.info("Synchronizing default repositories to datastore"); - qm.createRepository(RepositoryType.CPAN, "cpan-public-registry", "https://fastapi.metacpan.org/v1/", true, false); - qm.createRepository(RepositoryType.GEM, "rubygems.org", "https://rubygems.org/", true, false); - qm.createRepository(RepositoryType.HEX, "hex.pm", "https://hex.pm/", true, false); - qm.createRepository(RepositoryType.MAVEN, "central", "https://repo1.maven.org/maven2/", true, false); - qm.createRepository(RepositoryType.MAVEN, "atlassian-public", "https://packages.atlassian.com/content/repositories/atlassian-public/", true, false); - qm.createRepository(RepositoryType.MAVEN, "jboss-releases", "https://repository.jboss.org/nexus/content/repositories/releases/", true, false); - qm.createRepository(RepositoryType.MAVEN, "clojars", "https://repo.clojars.org/", true, false); - qm.createRepository(RepositoryType.MAVEN, "google-android", "https://maven.google.com/", true, false); - qm.createRepository(RepositoryType.NPM, "npm-public-registry", "https://registry.npmjs.org/", true, false); - qm.createRepository(RepositoryType.PYPI, "pypi.org", "https://pypi.org/", true, false); - qm.createRepository(RepositoryType.NUGET, "nuget-gallery", "https://api.nuget.org/", true, false); - qm.createRepository(RepositoryType.COMPOSER, "packagist", "https://repo.packagist.org/", true, false); - qm.createRepository(RepositoryType.CARGO, "crates.io", "https://crates.io", true, false); - qm.createRepository(RepositoryType.GO_MODULES, "proxy.golang.org", "https://proxy.golang.org", true, false); - qm.createRepository(RepositoryType.GITHUB, "github.com", "https://github.com", true, false); + qm.createRepository(RepositoryType.CPAN, "cpan-public-registry", "https://fastapi.metacpan.org/v1/", true, false, false, null, null); + qm.createRepository(RepositoryType.GEM, "rubygems.org", "https://rubygems.org/", true, false, false, null, null); + qm.createRepository(RepositoryType.HEX, "hex.pm", "https://hex.pm/", true, false, false, null, null); + qm.createRepository(RepositoryType.MAVEN, "central", "https://repo1.maven.org/maven2/", true, false, false, null, null); + qm.createRepository(RepositoryType.MAVEN, "atlassian-public", "https://packages.atlassian.com/content/repositories/atlassian-public/", true, false, false, null, null); + qm.createRepository(RepositoryType.MAVEN, "jboss-releases", "https://repository.jboss.org/nexus/content/repositories/releases/", true, false, false, null, null); + qm.createRepository(RepositoryType.MAVEN, "clojars", "https://repo.clojars.org/", true, false, false, null, null); + qm.createRepository(RepositoryType.MAVEN, "google-android", "https://maven.google.com/", true, false, false, null, null); + qm.createRepository(RepositoryType.NPM, "npm-public-registry", "https://registry.npmjs.org/", true, false, false, null, null); + qm.createRepository(RepositoryType.PYPI, "pypi.org", "https://pypi.org/", true, false, false, null, null); + qm.createRepository(RepositoryType.NUGET, "nuget-gallery", "https://api.nuget.org/", true, false, false, null, null); + qm.createRepository(RepositoryType.COMPOSER, "packagist", "https://repo.packagist.org/", true, false, false, null, null); + qm.createRepository(RepositoryType.CARGO, "crates.io", "https://crates.io", true, false, false, null, null); + qm.createRepository(RepositoryType.GO_MODULES, "proxy.golang.org", "https://proxy.golang.org", true, false, false, null, null); + qm.createRepository(RepositoryType.GITHUB, "github.com", "https://github.com", true, false, false, null, null); } } diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 64155b0f1f..6524a339c3 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -651,7 +651,7 @@ public ViolationAnalysis getViolationAnalysis(Component component, PolicyViolati } public ViolationAnalysis makeViolationAnalysis(Component component, PolicyViolation policyViolation, - ViolationAnalysisState violationAnalysisState, Boolean isSuppressed) { + ViolationAnalysisState violationAnalysisState, Boolean isSuppressed) { return getPolicyQueryManager().makeViolationAnalysis(component, policyViolation, violationAnalysisState, isSuppressed); } @@ -781,14 +781,14 @@ public AffectedVersionAttribution getAffectedVersionAttribution(Vulnerability vu } public void updateAffectedVersionAttributions(final Vulnerability vulnerability, - final List vsList, - final Vulnerability.Source source) { + final List vsList, + final Vulnerability.Source source) { getVulnerabilityQueryManager().updateAffectedVersionAttributions(vulnerability, vsList, source); } public void updateAffectedVersionAttribution(final Vulnerability vulnerability, - final VulnerableSoftware vulnerableSoftware, - final Vulnerability.Source source) { + final VulnerableSoftware vulnerableSoftware, + final Vulnerability.Source source) { getVulnerabilityQueryManager().updateAffectedVersionAttribution(vulnerability, vulnerableSoftware, source); } @@ -833,8 +833,8 @@ public List getAllVulnerableSoftwareByCpe(final String cpeSt } public VulnerableSoftware getVulnerableSoftwareByPurl(String purlType, String purlNamespace, String purlName, - String versionEndExcluding, String versionEndIncluding, - String versionStartExcluding, String versionStartIncluding) { + String versionEndExcluding, String versionEndIncluding, + String versionStartExcluding, String versionStartIncluding) { return getVulnerableSoftwareQueryManager().getVulnerableSoftwareByPurl(purlType, purlNamespace, purlName, versionEndExcluding, versionEndIncluding, versionStartExcluding, versionStartIncluding); } @@ -1120,12 +1120,12 @@ public boolean repositoryExist(RepositoryType type, String identifier) { return getRepositoryQueryManager().repositoryExist(type, identifier); } - public Repository createRepository(RepositoryType type, String identifier, String url, boolean enabled, boolean internal) { - return getRepositoryQueryManager().createRepository(type, identifier, url, enabled, internal); + public Repository createRepository(RepositoryType type, String identifier, String url, boolean enabled, boolean internal, boolean isAuthenticationRequired, String username, String password) { + return getRepositoryQueryManager().createRepository(type, identifier, url, enabled, internal, isAuthenticationRequired, username, password); } - public Repository updateRepository(UUID uuid, String identifier, String url, boolean internal, String username, String password, boolean enabled) { - return getRepositoryQueryManager().updateRepository(uuid, identifier, url, internal, username, password, enabled); + public Repository updateRepository(UUID uuid, String identifier, String url, boolean internal, boolean authenticationRequired, String username, String password, boolean enabled) { + return getRepositoryQueryManager().updateRepository(uuid, identifier, url, internal, authenticationRequired, username, password, enabled); } public RepositoryMetaComponent getRepositoryMetaComponent(RepositoryType repositoryType, String namespace, String name) { @@ -1468,4 +1468,4 @@ public List getRepositoryMetaComponentsBatch(final List return results; } -} +} \ No newline at end of file diff --git a/src/main/java/org/dependencytrack/persistence/RepositoryQueryManager.java b/src/main/java/org/dependencytrack/persistence/RepositoryQueryManager.java index c9b9861e44..5ff60e7b5e 100644 --- a/src/main/java/org/dependencytrack/persistence/RepositoryQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/RepositoryQueryManager.java @@ -18,8 +18,11 @@ */ package org.dependencytrack.persistence; +import alpine.common.logging.Logger; import alpine.persistence.PaginatedResult; import alpine.resources.AlpineRequest; +import alpine.security.crypto.DataEncryption; +import org.apache.commons.lang3.StringUtils; import org.dependencytrack.model.Repository; import org.dependencytrack.model.RepositoryMetaComponent; import org.dependencytrack.model.RepositoryType; @@ -34,10 +37,12 @@ import java.util.UUID; public class RepositoryQueryManager extends QueryManager implements IQueryManager { + private static final Logger LOGGER = Logger.getLogger(RepositoryQueryManager.class); /** * Constructs a new QueryManager. + * * @param pm a PersistenceManager object */ RepositoryQueryManager(final PersistenceManager pm) { @@ -46,7 +51,8 @@ public class RepositoryQueryManager extends QueryManager implements IQueryManage /** * Constructs a new QueryManager. - * @param pm a PersistenceManager object + * + * @param pm a PersistenceManager object * @param request an AlpineRequest object */ RepositoryQueryManager(final PersistenceManager pm, final AlpineRequest request) { @@ -56,6 +62,7 @@ public class RepositoryQueryManager extends QueryManager implements IQueryManage /** * Returns a list of all repositories. + * * @return a List of Repositories */ public PaginatedResult getRepositories() { @@ -74,6 +81,7 @@ public PaginatedResult getRepositories() { /** * Returns a list of all repositories * This method if designed NOT to provide paginated results. + * * @return a List of Repositories */ public List getAllRepositories() { @@ -84,6 +92,7 @@ public List getAllRepositories() { /** * Returns a list of repositories by it's type. + * * @param type the type of repository (required) * @return a List of Repository objects */ @@ -98,6 +107,7 @@ public PaginatedResult getRepositories(RepositoryType type) { /** * Returns a list of repositories by it's type in the order in which the repository should be used in resolution. * This method if designed NOT to provide paginated results. + * * @param type the type of repository (required) * @return a List of Repository objects */ @@ -105,12 +115,13 @@ public PaginatedResult getRepositories(RepositoryType type) { public List getAllRepositoriesOrdered(RepositoryType type) { final Query query = pm.newQuery(Repository.class, "type == :type"); query.setOrdering("resolutionOrder asc"); - return (List)query.execute(type); + return (List) query.execute(type); } /** * Determines if the repository exists in the database. - * @param type the type of repository (required) + * + * @param type the type of repository (required) * @param identifier the repository identifier * @return true if object exists, false if not */ @@ -122,19 +133,24 @@ public boolean repositoryExist(RepositoryType type, String identifier) { /** * Creates a new Repository. - * @param type the type of repository - * @param identifier a unique (to the type) identifier for the repo - * @param url the URL to the repository - * @param enabled if the repo is enabled or not + * + * @param type the type of repository + * @param identifier a unique (to the type) identifier for the repo + * @param url the URL to the repository + * @param enabled if the repo is enabled or not + * @param internal if the repo is internal or not + * @param isAuthenticationRequired if the repository needs authentication or not + * @param username the username to access the (authenticated) repository with + * @param password the password to access the (authenticated) repository with * @return the created Repository */ - public Repository createRepository(RepositoryType type, String identifier, String url, boolean enabled, boolean internal) { + public Repository createRepository(RepositoryType type, String identifier, String url, boolean enabled, boolean internal, boolean isAuthenticationRequired, String username, String password) { if (repositoryExist(type, identifier)) { return null; } int order = 0; final List existingRepos = getAllRepositoriesOrdered(type); - if (existingRepos != null) { + if (existingRepos != null && !existingRepos.isEmpty()) { for (final Repository existing : existingRepos) { if (existing.getResolutionOrder() > order) { order = existing.getResolutionOrder(); @@ -148,27 +164,40 @@ public Repository createRepository(RepositoryType type, String identifier, Strin repo.setResolutionOrder(order + 1); repo.setEnabled(enabled); repo.setInternal(internal); + repo.setAuthenticationRequired(isAuthenticationRequired); + if (Boolean.TRUE.equals(isAuthenticationRequired) && (username != null || password != null)) { + repo.setUsername(StringUtils.trimToNull(username)); + try { + if (password != null) { + repo.setPassword(DataEncryption.encryptAsString(password)); + } + } catch (Exception e) { + LOGGER.error("An error occurred while saving password in encrypted state"); + } + } return persist(repo); } /** * Updates an existing Repository. - * @param uuid the uuid of the repository to update - * @param identifier the identifier of the repository - * @param url a url of the repository - * @param internal specifies if the repository is internal - * @param username the username to access the (internal) repository with - * @param password the password to access the (internal) repository with - * @param enabled specifies if the repository is enabled + * + * @param uuid the uuid of the repository to update + * @param identifier the identifier of the repository + * @param url a url of the repository + * @param internal specifies if the repository is internal + * @param authenticationRequired if the repository needs authentication or not + * @param username the username to access the (authenticated) repository with + * @param password the password to access the (authenticated) repository with + * @param enabled specifies if the repository is enabled * @return the updated Repository */ - public Repository updateRepository(UUID uuid, String identifier, String url, boolean internal, String username, String password, boolean enabled) { + public Repository updateRepository(UUID uuid, String identifier, String url, boolean internal, boolean authenticationRequired, String username, String password, boolean enabled) { final Repository repository = getObjectByUuid(Repository.class, uuid); repository.setIdentifier(identifier); repository.setUrl(url); repository.setInternal(internal); - - if (!internal) { + repository.setAuthenticationRequired(authenticationRequired); + if (!authenticationRequired) { repository.setUsername(null); repository.setPassword(null); } else { @@ -182,12 +211,14 @@ public Repository updateRepository(UUID uuid, String identifier, String url, boo /** * Returns a RepositoryMetaComponent object from the specified type, group, and name. + * * @param repositoryType the type of repository - * @param namespace the Package URL namespace of the meta component - * @param name the Package URL name of the meta component + * @param namespace the Package URL namespace of the meta component + * @param name the Package URL name of the meta component * @return a RepositoryMetaComponent object, or null if not found */ - public RepositoryMetaComponent getRepositoryMetaComponent(RepositoryType repositoryType, String namespace, String name) { + public RepositoryMetaComponent getRepositoryMetaComponent(RepositoryType repositoryType, String + namespace, String name) { final Query query = pm.newQuery(RepositoryMetaComponent.class); query.setFilter("repositoryType == :repositoryType && namespace == :namespace && name == :name"); query.setRange(0, 1); @@ -196,10 +227,12 @@ public RepositoryMetaComponent getRepositoryMetaComponent(RepositoryType reposit /** * Synchronizes a RepositoryMetaComponent, updating it if it needs updating, or creating it if it doesn't exist. + * * @param transientRepositoryMetaComponent the RepositoryMetaComponent object to synchronize * @return a synchronized RepositoryMetaComponent object */ - public synchronized RepositoryMetaComponent synchronizeRepositoryMetaComponent(final RepositoryMetaComponent transientRepositoryMetaComponent) { + public synchronized RepositoryMetaComponent synchronizeRepositoryMetaComponent( + final RepositoryMetaComponent transientRepositoryMetaComponent) { final RepositoryMetaComponent metaComponent = getRepositoryMetaComponent(transientRepositoryMetaComponent.getRepositoryType(), transientRepositoryMetaComponent.getNamespace(), transientRepositoryMetaComponent.getName()); if (metaComponent != null) { @@ -217,6 +250,7 @@ public synchronized RepositoryMetaComponent synchronizeRepositoryMetaComponent(f /** * Returns a list of {@link RepositoryMetaComponent} objects from the specified type, group, and name. + * * @param list a list of {@link RepositoryQueryManager.RepositoryMetaComponentSearch} used to filter * @return a List of {@link RepositoryMetaComponent} objects * @since 4.9.0 @@ -225,7 +259,7 @@ public List getRepositoryMetaComponents(final List query = pm.newQuery(RepositoryMetaComponent.class); // Dynamically build the filter string and populate the parameters - final String filterTemplate = "(repositoryType == :%s && name == :%s && namespace == :%s)"; + final String filterTemplate = "(repositoryType == :%s && name == :%s && namespace == :%s)"; // List with all the filters final List filters = new ArrayList<>(list.size()); @@ -269,5 +303,7 @@ public List getRepositoryMetaComponents(final List * @since 4.9.0 */ - public record RepositoryMetaComponentSearch(RepositoryType type, String namespace, String name) implements Serializable { } + public record RepositoryMetaComponentSearch(RepositoryType type, String namespace, + String name) implements Serializable { + } } diff --git a/src/main/java/org/dependencytrack/resources/v1/RepositoryResource.java b/src/main/java/org/dependencytrack/resources/v1/RepositoryResource.java index 4a955fcf82..0f1219af4a 100644 --- a/src/main/java/org/dependencytrack/resources/v1/RepositoryResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/RepositoryResource.java @@ -51,6 +51,8 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import static org.dependencytrack.resources.v1.AbstractConfigPropertyResource.ENCRYPTED_PLACEHOLDER; + /** * JAX-RS resources for processing repositories. * @@ -96,7 +98,7 @@ public Response getRepositories() { @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response getRepositoriesByType( @ApiParam(value = "The type of repositories to retrieve", required = true) - @PathParam("type")RepositoryType type) { + @PathParam("type") RepositoryType type) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final PaginatedResult result = qm.getRepositories(type); return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); @@ -162,24 +164,15 @@ public Response createRepository(Repository jsonRepository) { try (QueryManager qm = new QueryManager()) { final boolean exists = qm.repositoryExist(jsonRepository.getType(), StringUtils.trimToNull(jsonRepository.getIdentifier())); - if (! exists) { + if (!exists) { final Repository repository = qm.createRepository( jsonRepository.getType(), StringUtils.trimToNull(jsonRepository.getIdentifier()), StringUtils.trimToNull(jsonRepository.getUrl()), jsonRepository.isEnabled(), - jsonRepository.isInternal()); - - if (Boolean.TRUE.equals(jsonRepository.isInternal()) && (jsonRepository.getUsername() != null || jsonRepository.getPassword() != null)) { - repository.setUsername(StringUtils.trimToNull(jsonRepository.getUsername())); - try { - if (jsonRepository.getPassword() != null) { - repository.setPassword(DataEncryption.encryptAsString(jsonRepository.getPassword())); - } - } catch (Exception e) { - return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("The specified repository password could not be encrypted.").build(); - } - } + jsonRepository.isInternal(), + jsonRepository.isAuthenticationRequired(), + jsonRepository.getUsername(), jsonRepository.getPassword()); return Response.status(Response.Status.CREATED).entity(repository).build(); } else { @@ -202,7 +195,7 @@ public Response createRepository(Repository jsonRepository) { @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response updateRepository(Repository jsonRepository) { final Validator validator = super.getValidator(); - failOnValidationError( + failOnValidationError(validator.validateProperty(jsonRepository, "identifier"), validator.validateProperty(jsonRepository, "url") ); @@ -211,13 +204,13 @@ public Response updateRepository(Repository jsonRepository) { if (repository != null) { final String url = StringUtils.trimToNull(jsonRepository.getUrl()); try { - // The password is not passed to the front-end, so it should only be overwritten if it is not null. - final String updatedPassword = jsonRepository.getPassword() != null + // The password is not passed to the front-end, so it should only be overwritten if it is not null or not set to default value coming from ui + final String updatedPassword = jsonRepository.getPassword()!=null && !jsonRepository.getPassword().equals(ENCRYPTED_PLACEHOLDER) ? DataEncryption.encryptAsString(jsonRepository.getPassword()) : repository.getPassword(); repository = qm.updateRepository(jsonRepository.getUuid(), repository.getIdentifier(), url, - jsonRepository.isInternal(), jsonRepository.getUsername(), updatedPassword, jsonRepository.isEnabled()); + jsonRepository.isInternal(), jsonRepository.isAuthenticationRequired(), jsonRepository.getUsername(), updatedPassword, jsonRepository.isEnabled()); return Response.ok(repository).build(); } catch (Exception e) { return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("The specified repository password could not be encrypted.").build(); diff --git a/src/main/java/org/dependencytrack/tasks/repositories/RepositoryMetaAnalyzerTask.java b/src/main/java/org/dependencytrack/tasks/repositories/RepositoryMetaAnalyzerTask.java index e7500b101a..49553a13f2 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/RepositoryMetaAnalyzerTask.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/RepositoryMetaAnalyzerTask.java @@ -86,7 +86,7 @@ public void inform(final Event e) { final List components = event.getComponents().get(); // Refreshing the object by querying for it again is preventative LOGGER.info("Performing component repository metadata analysis against " + components.size() + " components"); - for (final Component component: components) { + for (final Component component : components) { qm.ensureNoActiveTransaction(); // Workaround for https://github.com/DependencyTrack/dependency-track/issues/2677 analyze(qm, qm.getObjectById(Component.class, component.getId())); } @@ -96,10 +96,10 @@ public void inform(final Event e) { LOGGER.info("Analyzing portfolio component repository metadata"); try (final QueryManager qm = new QueryManager()) { final List projects = qm.getAllProjects(true); - for (final Project project: projects) { + for (final Project project : projects) { final List components = qm.getAllComponents(project); LOGGER.debug("Performing component repository metadata analysis against " + components.size() + " components in project: " + project.getUuid()); - for (final Component component: components) { + for (final Component component : components) { qm.ensureNoActiveTransaction(); // Workaround for https://github.com/DependencyTrack/dependency-track/issues/2677 analyze(qm, component); } @@ -116,19 +116,19 @@ public void inform(final Event e) { private void analyze(final QueryManager qm, final Component component) { LOGGER.debug("Analyzing component: " + component.getUuid()); final IMetaAnalyzer analyzer = IMetaAnalyzer.build(component); - if(RepositoryType.UNSUPPORTED != analyzer.supportedRepositoryType() && !isRepositoryMetaComponentStillValid(qm, analyzer.supportedRepositoryType(), component.getPurl().getNamespace(), component.getPurl().getName())) { + if (RepositoryType.UNSUPPORTED != analyzer.supportedRepositoryType() && !isRepositoryMetaComponentStillValid(qm, analyzer.supportedRepositoryType(), component.getPurl().getNamespace(), component.getPurl().getName())) { Callable cacheLoader = () -> { analyze(qm, component, analyzer); return null; }; boolean cacheStampedeBlockerEnabled = Config.getInstance().getPropertyAsBoolean(ConfigKey.REPO_META_ANALYZER_CACHE_STAMPEDE_BLOCKER_ENABLED); - if(cacheStampedeBlockerEnabled) { + if (cacheStampedeBlockerEnabled) { cacheStampedeBlocker.readThroughOrPopulateCache(PurlUtil.silentPurlCoordinatesOnly(component.getPurl()).toString(), cacheLoader); } else { try { cacheLoader.call(); } catch (Exception e) { - LOGGER.warn("Error while fetching component meta model for component(id="+component.getId()+"; purl="+component.getPurl()+") : "+e.getMessage(), e); + LOGGER.warn("Error while fetching component meta model for component(id=" + component.getId() + "; purl=" + component.getPurl() + ") : " + e.getMessage(), e); } } } @@ -142,7 +142,7 @@ private void analyze(final QueryManager qm, final Component component, final IMe if (cacList != null && cacList.size() > 0) { cacList.stream().forEach(cac -> cacByHost.put(cac.getTargetHost(), cac)); } - for (final Repository repository: qm.getAllRepositoriesOrdered(analyzer.supportedRepositoryType())) { + for (final Repository repository : qm.getAllRepositoriesOrdered(analyzer.supportedRepositoryType())) { // Moved the identification of internal components from the isApplicable() method from the Meta Analyzers // themselves (which was introduced in https://github.com/DependencyTrack/dependency-track/pull/512) // and made a global decision here instead. Internal components should only be analyzed using internal @@ -155,21 +155,24 @@ private void analyze(final QueryManager qm, final Component component, final IMe ComponentAnalysisCache cac = cacByHost.get(repository.getUrl()); MetaModel model = new MetaModel(component); if (cac != null && isCacheCurrent(cac, component.getPurl().toString())) { - LOGGER.debug("Building repository Metamodel from cache for "+purl); + LOGGER.debug("Building repository Metamodel from cache for " + purl); model.setLatestVersion(StringUtils.trimToNull(cac.getResult().getString(LATEST_VERSION))); model.setPublishedTimestamp(Date.from(Instant.ofEpochMilli(cac.getResult().getJsonNumber(PUBLISHED_TIMESTAMP).longValue()))); } else { LOGGER.debug("Analyzing component: " + component.getUuid() + " using repository: " + repository.getIdentifier() + " (" + repository.getType() + ")"); - if (repository.isInternal()) { + if (Boolean.TRUE.equals(repository.isInternal()) || Boolean.TRUE.equals(repository.isAuthenticationRequired())) { try { - analyzer.setRepositoryUsernameAndPassword(repository.getUsername(), DataEncryption.decryptAsString(repository.getPassword())); + String encryptedPassword = null; + if (repository.getPassword() != null) { + encryptedPassword = DataEncryption.decryptAsString(repository.getPassword()); + } + analyzer.setRepositoryUsernameAndPassword(repository.getUsername(), encryptedPassword); } catch (Exception e) { LOGGER.error("Failed decrypting password for repository: " + repository.getIdentifier(), e); } } - analyzer.setRepositoryBaseUrl(repository.getUrl()); model = analyzer.analyze(component); qm.updateComponentAnalysisCache(ComponentAnalysisCache.CacheType.REPOSITORY, repository.getUrl(), repository.getType().name(), PurlUtil.silentPurlCoordinatesOnly(component.getPurl()).toString(), new Date(), buildRepositoryComponentAnalysisCacheResult(model)); @@ -221,9 +224,9 @@ protected boolean isRepositoryMetaComponentStillValid(final QueryManager qm, fin } } if (isRepositoryMetaComponentStillValid) { - LOGGER.debug("RepositoryMetaComponent has been checked in the last "+cacheValidityPeriod+" ms (precisely "+delta+" ms ago). Skipping analysis. (source: " + repositoryType.name() + " / Namespace: " + namespace + " / Name: " + name + ")"); + LOGGER.debug("RepositoryMetaComponent has been checked in the last " + cacheValidityPeriod + " ms (precisely " + delta + " ms ago). Skipping analysis. (source: " + repositoryType.name() + " / Namespace: " + namespace + " / Name: " + name + ")"); } else { - LOGGER.debug("RepositoryMetaComponent has not been checked since "+cacheValidityPeriod+" ms. Analysis should be performed (source: " + repositoryType.name() + " / Namespace: " + namespace + " / Name: " + name + ")"); + LOGGER.debug("RepositoryMetaComponent has not been checked since " + cacheValidityPeriod + " ms. Analysis should be performed (source: " + repositoryType.name() + " / Namespace: " + namespace + " / Name: " + name + ")"); } return isRepositoryMetaComponentStillValid; } @@ -242,9 +245,9 @@ protected boolean isCacheCurrent(ComponentAnalysisCache cac, String target) { } } if (isCacheCurrent) { - LOGGER.debug("Cache is current. External repository call was made in the last "+cacheValidityPeriod+" ms (precisely "+delta+" ms ago). Skipping analysis. (target: " + target + ")"); + LOGGER.debug("Cache is current. External repository call was made in the last " + cacheValidityPeriod + " ms (precisely " + delta + " ms ago). Skipping analysis. (target: " + target + ")"); } else { - LOGGER.debug("Cache is not current. External repository call was not made in the last "+cacheValidityPeriod+" ms. Analysis should be performed (target: " + target + ")"); + LOGGER.debug("Cache is not current. External repository call was not made in the last " + cacheValidityPeriod + " ms. Analysis should be performed (target: " + target + ")"); } return isCacheCurrent; } diff --git a/src/test/java/org/dependencytrack/model/RepositoryTest.java b/src/test/java/org/dependencytrack/model/RepositoryTest.java index 9ae07a3270..9c60fd1f50 100644 --- a/src/test/java/org/dependencytrack/model/RepositoryTest.java +++ b/src/test/java/org/dependencytrack/model/RepositoryTest.java @@ -16,13 +16,13 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright (c) Steve Springett. All Rights Reserved. */ -package org.dependencytrack.model; +package org.dependencytrack.model; import org.junit.Assert; import org.junit.Test; public class RepositoryTest { - + @Test public void testId() { Repository repo = new Repository(); @@ -35,7 +35,7 @@ public void testType() { Repository repo = new Repository(); repo.setType(RepositoryType.MAVEN); Assert.assertEquals(RepositoryType.MAVEN, repo.getType()); - } + } @Test public void testIdentifier() { @@ -49,14 +49,14 @@ public void testUrl() { Repository repo = new Repository(); repo.setUrl("https://repo.maven.apache.org/maven2"); Assert.assertEquals("https://repo.maven.apache.org/maven2", repo.getUrl()); - } + } @Test public void testResolutionOrder() { Repository repo = new Repository(); repo.setResolutionOrder(5); Assert.assertEquals(5, repo.getResolutionOrder()); - } + } @Test public void testEnabled() { @@ -64,4 +64,18 @@ public void testEnabled() { repo.setEnabled(true); Assert.assertTrue(repo.isEnabled()); } + + @Test + public void testAuthenticationRequiredTrue() { + Repository repo = new Repository(); + repo.setAuthenticationRequired(true); + Assert.assertTrue(repo.isAuthenticationRequired()); + } + + @Test + public void testAuthenticationRequiredFalse() { + Repository repo = new Repository(); + repo.setAuthenticationRequired(false); + Assert.assertFalse(repo.isAuthenticationRequired()); + } } diff --git a/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java index 3c709ac4ef..dd4f09ae6e 100644 --- a/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java @@ -18,14 +18,14 @@ */ package org.dependencytrack.resources.v1; -import java.util.Date; -import javax.json.JsonArray; -import javax.json.JsonObject; -import javax.ws.rs.core.Response; +import alpine.server.filters.ApiFilter; +import alpine.server.filters.AuthenticationFilter; import org.dependencytrack.ResourceTest; +import org.dependencytrack.model.Repository; import org.dependencytrack.model.RepositoryMetaComponent; import org.dependencytrack.model.RepositoryType; import org.dependencytrack.persistence.DefaultObjectGenerator; +import org.dependencytrack.persistence.QueryManager; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.servlet.ServletContainer; import org.glassfish.jersey.test.DeploymentContext; @@ -33,17 +33,23 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import alpine.server.filters.ApiFilter; -import alpine.server.filters.AuthenticationFilter; + +import javax.json.JsonArray; +import javax.json.JsonObject; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.Date; +import java.util.List; public class RepositoryResourceTest extends ResourceTest { @Override protected DeploymentContext configureDeployment() { return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(RepositoryResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))) + new ResourceConfig(RepositoryResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class))) .build(); } @@ -64,7 +70,7 @@ public void getRepositoriesTest() { JsonArray json = parseJsonArray(response); Assert.assertNotNull(json); Assert.assertEquals(15, json.size()); - for (int i=0; i 0); + Assert.assertTrue(json.getJsonObject(12).getBoolean("authenticationRequired")); + Assert.assertEquals("testuser", json.getJsonObject(12).getString("username")); + Assert.assertTrue(json.getJsonObject(12).getBoolean("enabled")); + } + + @Test + public void createNonInternalRepositoryTest() { + Repository repository = new Repository(); + repository.setAuthenticationRequired(true); + repository.setEnabled(true); + repository.setUsername("testuser"); + repository.setPassword("testPassword"); + repository.setInternal(false); + repository.setIdentifier("test"); + repository.setUrl("www.foobar.com"); + repository.setType(RepositoryType.MAVEN); + RepositoryResource repositoryResource = new RepositoryResource(); + Response response = target(V1_REPOSITORY).request().header(X_API_KEY, apiKey) + .put(Entity.entity(repository, MediaType.APPLICATION_JSON)); + Assert.assertEquals(201, response.getStatus()); + + + response = target(V1_REPOSITORY).request().header(X_API_KEY, apiKey).get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals(String.valueOf(16), response.getHeaderString(TOTAL_COUNT_HEADER)); + JsonArray json = parseJsonArray(response); + Assert.assertNotNull(json); + Assert.assertEquals(16, json.size()); + Assert.assertEquals("MAVEN", json.getJsonObject(12).getString("type")); + Assert.assertEquals("test", json.getJsonObject(12).getString("identifier")); + Assert.assertEquals("www.foobar.com", json.getJsonObject(12).getString("url")); + Assert.assertTrue(json.getJsonObject(12).getInt("resolutionOrder") > 0); + Assert.assertTrue(json.getJsonObject(12).getBoolean("authenticationRequired")); + Assert.assertFalse(json.getJsonObject(12).getBoolean("internal")); + Assert.assertEquals("testuser", json.getJsonObject(12).getString("username")); + Assert.assertTrue(json.getJsonObject(12).getBoolean("enabled")); + } + + @Test + public void createRepositoryAuthFalseTest() { + Repository repository = new Repository(); + repository.setAuthenticationRequired(false); + repository.setEnabled(true); + repository.setInternal(true); + repository.setIdentifier("test"); + repository.setUrl("www.foobar.com"); + repository.setType(RepositoryType.MAVEN); + Response response = target(V1_REPOSITORY).request().header(X_API_KEY, apiKey) + .put(Entity.entity(repository, MediaType.APPLICATION_JSON)); + Assert.assertEquals(201, response.getStatus()); + + + response = target(V1_REPOSITORY).request().header(X_API_KEY, apiKey).get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals(String.valueOf(16), response.getHeaderString(TOTAL_COUNT_HEADER)); + JsonArray json = parseJsonArray(response); + Assert.assertNotNull(json); + Assert.assertEquals(16, json.size()); + Assert.assertEquals("MAVEN", json.getJsonObject(12).getString("type")); + Assert.assertEquals("test", json.getJsonObject(12).getString("identifier")); + Assert.assertEquals("www.foobar.com", json.getJsonObject(12).getString("url")); + Assert.assertTrue(json.getJsonObject(12).getInt("resolutionOrder") > 0); + Assert.assertFalse(json.getJsonObject(12).getBoolean("authenticationRequired")); + Assert.assertTrue(json.getJsonObject(12).getBoolean("enabled")); + + } + + @Test + public void updateRepositoryTest() throws Exception { + Repository repository = new Repository(); + repository.setAuthenticationRequired(true); + repository.setEnabled(true); + repository.setUsername("testuser"); + repository.setPassword("testPassword"); + repository.setInternal(true); + repository.setIdentifier("test"); + repository.setUrl("www.foobar.com"); + repository.setType(RepositoryType.MAVEN); + Response response = target(V1_REPOSITORY).request().header(X_API_KEY, apiKey) + .put(Entity.entity(repository, MediaType.APPLICATION_JSON)); + Assert.assertEquals(201, response.getStatus()); + try (QueryManager qm = new QueryManager()) { + List repositoryList = qm.getRepositories(RepositoryType.MAVEN).getList(Repository.class); + for (Repository repository1 : repositoryList) { + if (repository1.getIdentifier().equals("test")) { + repository1.setAuthenticationRequired(false); + response = target(V1_REPOSITORY).request().header(X_API_KEY, apiKey) + .post(Entity.entity(repository1, MediaType.APPLICATION_JSON)); + Assert.assertEquals(200, response.getStatus()); + break; + } + } + repositoryList = qm.getRepositories(RepositoryType.MAVEN).getList(Repository.class); + for (Repository repository1 : repositoryList) { + if (repository1.getIdentifier().equals("test")) { + Assert.assertEquals(false, repository1.isAuthenticationRequired()); + break; + } + } + } + + } } diff --git a/src/test/java/org/dependencytrack/tasks/RepoMetaAnalysisTaskTest.java b/src/test/java/org/dependencytrack/tasks/RepoMetaAnalysisTaskTest.java new file mode 100644 index 0000000000..b33ba4d4d8 --- /dev/null +++ b/src/test/java/org/dependencytrack/tasks/RepoMetaAnalysisTaskTest.java @@ -0,0 +1,232 @@ +package org.dependencytrack.tasks; + +import alpine.event.framework.EventService; +import com.github.packageurl.PackageURL; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.http.Body; +import com.github.tomakehurst.wiremock.http.ContentTypeHeader; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import org.dependencytrack.PersistenceCapableTest; +import org.dependencytrack.event.RepositoryMetaEvent; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.ConfigPropertyConstants; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.RepositoryMetaComponent; +import org.dependencytrack.model.RepositoryType; +import org.dependencytrack.tasks.repositories.RepositoryMetaAnalyzerTask; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import javax.ws.rs.core.MediaType; +import java.util.List; + +import static com.github.tomakehurst.wiremock.client.WireMock.containing; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.assertj.core.api.Assertions.assertThat; + +public class RepoMetaAnalysisTaskTest extends PersistenceCapableTest { + + @Rule + public WireMockRule wireMockRule = new WireMockRule(options().port(2345)); + + @Before + public void setUp() { + qm.createConfigProperty(ConfigPropertyConstants.SCANNER_ANALYSIS_CACHE_VALIDITY_PERIOD.getGroupName(), + ConfigPropertyConstants.SCANNER_ANALYSIS_CACHE_VALIDITY_PERIOD.getPropertyName(), "43200000", + ConfigPropertyConstants.SCANNER_ANALYSIS_CACHE_VALIDITY_PERIOD.getPropertyType(), + ConfigPropertyConstants.SCANNER_ANALYSIS_CACHE_VALIDITY_PERIOD.getDescription()); + wireMockRule.start(); + } + + @After + public void tearDown() { + EventService.getInstance().unsubscribe(RepositoryMetaAnalyzerTask.class); + wireMockRule.stop(); + } + + @Test + public void informTestNullPassword() throws Exception { + WireMock.stubFor(WireMock.get(WireMock.anyUrl()).withHeader("Authorization", containing("Basic")) + .willReturn(WireMock.aResponse() + .withStatus(200) + .withResponseBody(Body.ofBinaryOrText(""" + + junit + junit + + 4.13.2 + 4.13.2 + + 4.13-beta-1 + 4.13-beta-2 + 4.13-beta-3 + 4.13-rc-1 + 4.13-rc-2 + 4.13 + 4.13.1 + 4.13.2 + + 20210213164433 + + + """.getBytes(), new ContentTypeHeader(MediaType.APPLICATION_JSON)) + ) + .withHeader("X-CheckSum-MD5", "md5hash") + .withHeader("X-Checksum-SHA1", "sha1hash") + .withHeader("X-Checksum-SHA512", "sha512hash") + .withHeader("X-Checksum-SHA256", "sha256hash") + .withHeader("Last-Modified", "Thu, 07 Jul 2022 14:00:00 GMT"))); + EventService.getInstance().subscribe(RepositoryMetaEvent.class, RepositoryMetaAnalyzerTask.class); + Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); + Component component = new Component(); + component.setProject(project); + component.setName("junit"); + component.setPurl(new PackageURL("pkg:maven/junit/junit@4.12")); + qm.createComponent(component, false); + qm.createRepository(RepositoryType.MAVEN, "test", wireMockRule.baseUrl(), true, false, true, "testuser", null); + new RepositoryMetaAnalyzerTask().inform(new RepositoryMetaEvent(List.of(component))); + RepositoryMetaComponent metaComponent = qm.getRepositoryMetaComponent(RepositoryType.MAVEN, "junit", "junit"); + qm.getPersistenceManager().refresh(metaComponent); + assertThat(metaComponent.getLatestVersion()).isEqualTo("4.13.2"); + } + + @Test + public void informTestNullUserName() throws Exception { + WireMock.stubFor(WireMock.get(WireMock.anyUrl()).withHeader("Authorization", containing("Basic")) + .willReturn(WireMock.aResponse() + .withStatus(200) + .withResponseBody(Body.ofBinaryOrText(""" + + test1 + test1 + + 1.7.0 + 1.7.0 + + 1.2.0 + 1.3.0 + 1.4.0 + 1.5.0 + 1.5-rc-1 + 1.6.0 + 1.6.3 + 1.7.0 + + 20210213164433 + + + """.getBytes(), new ContentTypeHeader(MediaType.APPLICATION_JSON)) + ) + .withHeader("X-CheckSum-MD5", "md5hash") + .withHeader("X-Checksum-SHA1", "sha1hash") + .withHeader("X-Checksum-SHA512", "sha512hash") + .withHeader("X-Checksum-SHA256", "sha256hash") + .withHeader("Last-Modified", "Thu, 07 Jul 2022 14:00:00 GMT"))); + EventService.getInstance().subscribe(RepositoryMetaEvent.class, RepositoryMetaAnalyzerTask.class); + Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); + Component component = new Component(); + component.setProject(project); + component.setName("test1"); + component.setPurl(new PackageURL("pkg:maven/test1/test1@1.2.0")); + qm.createComponent(component, false); + qm.createRepository(RepositoryType.MAVEN, "test", wireMockRule.baseUrl(), true, false, true, null, "testPassword"); + new RepositoryMetaAnalyzerTask().inform(new RepositoryMetaEvent(List.of(component))); + RepositoryMetaComponent metaComponent = qm.getRepositoryMetaComponent(RepositoryType.MAVEN, "test1", "test1"); + qm.getPersistenceManager().refresh(metaComponent); + assertThat(metaComponent.getLatestVersion()).isEqualTo("1.7.0"); + } + + @Test + public void informTestNullUserNameAndPassword() throws Exception { + WireMock.stubFor(WireMock.get(WireMock.anyUrl()) + .willReturn(WireMock.aResponse() + .withStatus(200) + .withResponseBody(Body.ofBinaryOrText(""" + + test2 + test2 + + 4.13.2 + 4.13.2 + + 4.13-beta-1 + 4.13-beta-2 + 4.13-beta-3 + 4.13-rc-1 + 4.13-rc-2 + 4.13 + 4.13.1 + 4.13.2 + + 20210213164433 + + + """.getBytes(), new ContentTypeHeader(MediaType.APPLICATION_JSON)) + ) + .withHeader("X-CheckSum-MD5", "md5hash") + .withHeader("X-Checksum-SHA1", "sha1hash") + .withHeader("X-Checksum-SHA512", "sha512hash") + .withHeader("X-Checksum-SHA256", "sha256hash") + .withHeader("Last-Modified", "Thu, 07 Jul 2022 14:00:00 GMT"))); + EventService.getInstance().subscribe(RepositoryMetaEvent.class, RepositoryMetaAnalyzerTask.class); + Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); + Component component = new Component(); + component.setProject(project); + component.setName("junit"); + component.setPurl(new PackageURL("pkg:maven/test2/test2@4.12")); + qm.createComponent(component, false); + qm.createRepository(RepositoryType.MAVEN, "test", wireMockRule.baseUrl(), true, false, false, null, null); + new RepositoryMetaAnalyzerTask().inform(new RepositoryMetaEvent(List.of(component))); + RepositoryMetaComponent metaComponent = qm.getRepositoryMetaComponent(RepositoryType.MAVEN, "test2", "test2"); + qm.getPersistenceManager().refresh(metaComponent); + assertThat(metaComponent.getLatestVersion()).isEqualTo("4.13.2"); + } + + @Test + public void informTestUserNameAndPassword() throws Exception { + WireMock.stubFor(WireMock.get(WireMock.anyUrl()) + .willReturn(WireMock.aResponse() + .withStatus(200) + .withResponseBody(Body.ofBinaryOrText(""" + + test3 + test3 + + 4.13.2 + 4.13.2 + + 4.13-beta-1 + 4.13-beta-2 + 4.13-beta-3 + 4.13-rc-1 + 4.13-rc-2 + 4.13 + 4.13.1 + 4.13.2 + + 20210213164433 + + + """.getBytes(), new ContentTypeHeader(MediaType.APPLICATION_JSON)) + ) + .withHeader("X-CheckSum-MD5", "md5hash") + .withHeader("X-Checksum-SHA1", "sha1hash") + .withHeader("X-Checksum-SHA512", "sha512hash") + .withHeader("X-Checksum-SHA256", "sha256hash") + .withHeader("Last-Modified", "Thu, 07 Jul 2022 14:00:00 GMT"))); + EventService.getInstance().subscribe(RepositoryMetaEvent.class, RepositoryMetaAnalyzerTask.class); + Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); + Component component = new Component(); + component.setProject(project); + component.setName("test3"); + component.setPurl(new PackageURL("pkg:maven/test3/test3@4.12")); + qm.createComponent(component, false); + qm.createRepository(RepositoryType.MAVEN, "test", wireMockRule.baseUrl(), true, false, true, "testUser", "testPassword"); + new RepositoryMetaAnalyzerTask().inform(new RepositoryMetaEvent(List.of(component))); + RepositoryMetaComponent metaComponent = qm.getRepositoryMetaComponent(RepositoryType.MAVEN, "test3", "test3"); + qm.getPersistenceManager().refresh(metaComponent); + assertThat(metaComponent.getLatestVersion()).isEqualTo("4.13.2"); + } +}