diff options
author | Havoc Pennington <hp@pobox.com> | 2025-02-03 15:49:29 -0500 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2025-02-19 20:03:12 +0000 |
commit | b39b71107d70faa478402a85d5f71ea2e1312a97 (patch) | |
tree | 26e1ca9c8f1d1d4f39b109e153e0750b46699f57 /server/sonar-db-dao | |
parent | 933fcdba3b22e7048af11385c343eb2a414567fc (diff) | |
download | sonarqube-b39b71107d70faa478402a85d5f71ea2e1312a97.tar.gz sonarqube-b39b71107d70faa478402a85d5f71ea2e1312a97.zip |
SQRP-138 Create the sca_dependencies database table
Diffstat (limited to 'server/sonar-db-dao')
16 files changed, 888 insertions, 0 deletions
diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/sca/ScaDependenciesDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/sca/ScaDependenciesDaoIT.java new file mode 100644 index 00000000000..99d7289dc07 --- /dev/null +++ b/server/sonar-db-dao/src/it/java/org/sonar/db/sca/ScaDependenciesDaoIT.java @@ -0,0 +1,198 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db.sca; + +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.sonar.api.utils.System2; +import org.sonar.db.DbTester; +import org.sonar.db.Pagination; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ProjectData; + +import static org.assertj.core.api.Assertions.assertThat; + +class ScaDependenciesDaoIT { + + private static final String PROJECT_BRANCH_UUID = "branchUuid"; + + @RegisterExtension + private final DbTester db = DbTester.create(System2.INSTANCE); + + private final ScaDependenciesDao scaDependenciesDao = db.getDbClient().scaDependenciesDao(); + + @Test + void insert_shouldPersistScaDependencies() { + var scaDependencyDto = insertScaDependency(); + + List<Map<String, Object>> select = db.select(db.getSession(), "select * from sca_dependencies"); + assertThat(select).hasSize(1); + Map<String, Object> stringObjectMap = select.get(0); + assertThat(stringObjectMap).containsExactlyInAnyOrderEntriesOf( + Map.ofEntries( + Map.entry("uuid", scaDependencyDto.uuid()), + Map.entry("component_uuid", scaDependencyDto.componentUuid()), + Map.entry("package_url", scaDependencyDto.packageUrl()), + Map.entry("package_manager", scaDependencyDto.packageManager().name()), + Map.entry("package_name", scaDependencyDto.packageName()), + Map.entry("version", scaDependencyDto.version()), + Map.entry("direct", scaDependencyDto.direct()), + Map.entry("scope", scaDependencyDto.scope()), + Map.entry("dependency_file_path", scaDependencyDto.dependencyFilePath()), + Map.entry("license_expression", scaDependencyDto.licenseExpression()), + Map.entry("known", scaDependencyDto.known()), + Map.entry("created_at", scaDependencyDto.createdAt()), + Map.entry("updated_at", scaDependencyDto.updatedAt()) + ) + ); + } + + @Test + void deleteByUuid_shouldDeleteScaDependencies() { + var scaDependencyDto = insertScaDependency(); + + List<Map<String, Object>> select = db.select(db.getSession(), "select * from sca_dependencies"); + assertThat(select).isNotEmpty(); + + scaDependenciesDao.deleteByUuid(db.getSession(), scaDependencyDto.uuid()); + + select = db.select(db.getSession(), "select * from sca_dependencies"); + assertThat(select).isEmpty(); + } + + @Test + void selectByQuery_shouldReturnScaDependencies_whenQueryByBranchUuid() { + ProjectData projectData = db.components().insertPublicProject(); + ScaDependencyDto scaDependencyDto = db.getScaDependenciesDbTester().insertScaDependency(projectData.mainBranchUuid()); + + ScaDependenciesQuery scaDependenciesQuery = new ScaDependenciesQuery(projectData.mainBranchUuid(), null); + List<ScaDependencyDto> results = scaDependenciesDao.selectByQuery(db.getSession(), scaDependenciesQuery, Pagination.all()); + + assertThat(results).hasSize(1); + assertThat(results.get(0)).usingRecursiveComparison().isEqualTo(scaDependencyDto); + } + + @Test + void selectByQuery_shouldReturnPaginatedScaDependencies() { + ScaDependencyDto scaDependencyDto1 = insertScaDependency("1"); + ScaDependencyDto scaDependencyDto2 = insertScaDependency("2"); + ScaDependencyDto scaDependencyDto3 = insertScaDependency("3"); + ScaDependencyDto scaDependencyDto4 = insertScaDependency("4"); + + ScaDependenciesQuery scaDependenciesQuery = new ScaDependenciesQuery(PROJECT_BRANCH_UUID, null); + List<ScaDependencyDto> page1Results = scaDependenciesDao.selectByQuery(db.getSession(), scaDependenciesQuery, Pagination.forPage(1).andSize(2)); + List<ScaDependencyDto> page2Results = scaDependenciesDao.selectByQuery(db.getSession(), scaDependenciesQuery, Pagination.forPage(2).andSize(2)); + + assertThat(page1Results).hasSize(2); + assertThat(page1Results.get(0)).usingRecursiveComparison().isEqualTo(scaDependencyDto1); + assertThat(page1Results.get(1)).usingRecursiveComparison().isEqualTo(scaDependencyDto2); + assertThat(page2Results).hasSize(2); + assertThat(page2Results.get(0)).usingRecursiveComparison().isEqualTo(scaDependencyDto3); + assertThat(page2Results.get(1)).usingRecursiveComparison().isEqualTo(scaDependencyDto4); + } + + @Test + void selectByQuery_shouldPartiallyMatchLongName_whenQueriedByText() { + ScaDependencyDto projectDepSearched = insertScaDependency("sEArched"); + insertScaDependency("notWanted"); + ScaDependencyDto projectDepSearchAsWell = insertScaDependency("sEArchedAsWell"); + insertScaDependency("notwantedeither"); + + ScaDependenciesQuery scaDependenciesQuery = new ScaDependenciesQuery(PROJECT_BRANCH_UUID, "long_nameSearCHed"); + List<ScaDependencyDto> results = scaDependenciesDao.selectByQuery(db.getSession(), scaDependenciesQuery, Pagination.all()); + + assertThat(results).hasSize(2); + assertThat(results.get(0)).usingRecursiveComparison().isEqualTo(projectDepSearched); + assertThat(results.get(1)).usingRecursiveComparison().isEqualTo(projectDepSearchAsWell); + } + + @Test + void selectByQuery_shouldExactlyMatchKee_whenQueriedByText() { + ScaDependencyDto projectDepSearched = insertScaDependency("1", dto -> dto.setKey("keySearched")); + insertScaDependency("2", dto -> dto.setKey("KEySearCHed")); + insertScaDependency("3", dto -> dto.setKey("some_keySearched")); + + ScaDependenciesQuery scaDependenciesQuery = new ScaDependenciesQuery(PROJECT_BRANCH_UUID, "keySearched"); + List<ScaDependencyDto> results = scaDependenciesDao.selectByQuery(db.getSession(), scaDependenciesQuery, Pagination.all()); + + assertThat(results).hasSize(1); + assertThat(results.get(0)).usingRecursiveComparison().isEqualTo(projectDepSearched); + } + + @Test + void update_shouldUpdateScaDependency() { + ScaDependencyDto scaDependencyDto = insertScaDependency(); + ScaDependencyDto updatedScaDependency = + scaDependencyDto.toBuilder().setUpdatedAt(scaDependencyDto.updatedAt() + 1).setVersion("newVersion").build(); + + scaDependenciesDao.update(db.getSession(), updatedScaDependency); + + List<Map<String, Object>> select = db.select(db.getSession(), "select * from sca_dependencies"); + assertThat(select).hasSize(1); + Map<String, Object> stringObjectMap = select.get(0); + assertThat(stringObjectMap).containsExactlyInAnyOrderEntriesOf( + Map.ofEntries( + Map.entry("uuid", updatedScaDependency.uuid()), + Map.entry("component_uuid", updatedScaDependency.componentUuid()), + Map.entry("package_url", updatedScaDependency.packageUrl()), + Map.entry("package_manager", updatedScaDependency.packageManager().name()), + Map.entry("package_name", updatedScaDependency.packageName()), + Map.entry("version", updatedScaDependency.version()), + Map.entry("direct", updatedScaDependency.direct()), + Map.entry("scope", updatedScaDependency.scope()), + Map.entry("dependency_file_path", updatedScaDependency.dependencyFilePath()), + Map.entry("license_expression", updatedScaDependency.licenseExpression()), + Map.entry("known", updatedScaDependency.known()), + Map.entry("created_at", updatedScaDependency.createdAt()), + Map.entry("updated_at", updatedScaDependency.updatedAt()) + ) + ); + } + + @Test + void countByQuery_shouldReturnTheTotalOfDependencies() { + insertScaDependency("sEArched"); + insertScaDependency("notWanted"); + insertScaDependency("sEArchedAsWell"); + db.getScaDependenciesDbTester().insertScaDependency("another_branch_uuid", "searched"); + + ScaDependenciesQuery scaDependenciesQuery = new ScaDependenciesQuery(PROJECT_BRANCH_UUID, "long_nameSearCHed"); + + assertThat(scaDependenciesDao.countByQuery(db.getSession(), scaDependenciesQuery)).isEqualTo(2); + assertThat(scaDependenciesDao.countByQuery(db.getSession(), new ScaDependenciesQuery(PROJECT_BRANCH_UUID, null))).isEqualTo(3); + assertThat(scaDependenciesDao.countByQuery(db.getSession(), new ScaDependenciesQuery("another_branch_uuid", null))).isEqualTo(1); + } + + private ScaDependencyDto insertScaDependency() { + return db.getScaDependenciesDbTester().insertScaDependency(PROJECT_BRANCH_UUID); + } + + private ScaDependencyDto insertScaDependency(String suffix) { + return insertScaDependency(suffix, null); + } + + private ScaDependencyDto insertScaDependency(String suffix, @Nullable Consumer<ComponentDto> dtoPopulator) { + return db.getScaDependenciesDbTester().insertScaDependency(PROJECT_BRANCH_UUID, suffix, dtoPopulator); + } +} diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/DaoModule.java b/server/sonar-db-dao/src/main/java/org/sonar/db/DaoModule.java index db5edec1bf1..dcff6ee1d0d 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/DaoModule.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/DaoModule.java @@ -89,6 +89,7 @@ import org.sonar.db.report.ReportSubscriptionDao; import org.sonar.db.rule.RuleChangeDao; import org.sonar.db.rule.RuleDao; import org.sonar.db.rule.RuleRepositoryDao; +import org.sonar.db.sca.ScaDependenciesDao; import org.sonar.db.scannercache.ScannerAnalysisCacheDao; import org.sonar.db.schemamigration.SchemaMigrationDao; import org.sonar.db.scim.ScimGroupDao; @@ -186,6 +187,7 @@ public class DaoModule extends Module { RuleChangeDao.class, RuleRepositoryDao.class, SamlMessageIdDao.class, + ScaDependenciesDao.class, ScannerAnalysisCacheDao.class, SchemaMigrationDao.class, ScimGroupDao.class, diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java b/server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java index 4ddbe135d14..279a9fdf83b 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java @@ -89,6 +89,7 @@ import org.sonar.db.report.ReportSubscriptionDao; import org.sonar.db.rule.RuleChangeDao; import org.sonar.db.rule.RuleDao; import org.sonar.db.rule.RuleRepositoryDao; +import org.sonar.db.sca.ScaDependenciesDao; import org.sonar.db.scannercache.ScannerAnalysisCacheDao; import org.sonar.db.schemamigration.SchemaMigrationDao; import org.sonar.db.scim.ScimGroupDao; @@ -202,6 +203,7 @@ public class DbClient { private final IssueFixedDao issueFixedDao; private final TelemetryMetricsSentDao telemetryMetricsSentDao; private final ProjectDependenciesDao projectDependenciesDao; + private final ScaDependenciesDao scaDependenciesDao; public DbClient(Database database, MyBatis myBatis, DBSessions dbSessions, Dao... daos) { this.database = database; @@ -299,6 +301,7 @@ public class DbClient { issueFixedDao = getDao(map, IssueFixedDao.class); telemetryMetricsSentDao = getDao(map, TelemetryMetricsSentDao.class); projectDependenciesDao = getDao(map, ProjectDependenciesDao.class); + scaDependenciesDao = getDao(map, ScaDependenciesDao.class); } public DbSession openSession(boolean batch) { @@ -666,4 +669,8 @@ public class DbClient { public ProjectDependenciesDao projectDependenciesDao() { return projectDependenciesDao; } + + public ScaDependenciesDao scaDependenciesDao() { + return scaDependenciesDao; + } } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java b/server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java index 250f51d9729..7f9fac3f962 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java @@ -153,6 +153,7 @@ import org.sonar.db.rule.RuleChangeMapper; import org.sonar.db.rule.RuleMapper; import org.sonar.db.rule.RuleParamDto; import org.sonar.db.rule.RuleRepositoryMapper; +import org.sonar.db.sca.ScaDependenciesMapper; import org.sonar.db.scannercache.ScannerAnalysisCacheMapper; import org.sonar.db.schemamigration.SchemaMigrationDto; import org.sonar.db.schemamigration.SchemaMigrationMapper; @@ -343,6 +344,7 @@ public class MyBatis { RuleChangeMapper.class, RuleRepositoryMapper.class, SamlMessageIdMapper.class, + ScaDependenciesMapper.class, ScannerAnalysisCacheMapper.class, SchemaMigrationMapper.class, ScimGroupMapper.class, diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/sca/PackageManager.java b/server/sonar-db-dao/src/main/java/org/sonar/db/sca/PackageManager.java new file mode 100644 index 00000000000..50b49bcd185 --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/sca/PackageManager.java @@ -0,0 +1,28 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db.sca; + +/** + * These values come from https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst and correspond + * to the package manager string used in PURLs. + */ +public enum PackageManager { + CARGO, COCOAPODS, COMPOSER, CONAN, CONDA, GEM, GOLANG, MAVEN, NPM, NUGET, PYPI +} diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/sca/ScaDependenciesDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/sca/ScaDependenciesDao.java new file mode 100644 index 00000000000..a2942a438d5 --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/sca/ScaDependenciesDao.java @@ -0,0 +1,64 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db.sca; + +import java.util.List; +import java.util.Optional; +import org.sonar.db.Dao; +import org.sonar.db.DbSession; +import org.sonar.db.Pagination; + +public class ScaDependenciesDao implements Dao { + + private static ScaDependenciesMapper mapper(DbSession session) { + return session.getMapper(ScaDependenciesMapper.class); + } + + public void insert(DbSession session, ScaDependencyDto scaDependencyDto) { + mapper(session).insert(scaDependencyDto); + } + + public void deleteByUuid(DbSession session, String uuid) { + mapper(session).deleteByUuid(uuid); + } + + public Optional<ScaDependencyDto> selectByUuid(DbSession dbSession, String uuid) { + return Optional.ofNullable(mapper(dbSession).selectByUuid(uuid)); + } + + /** + * Retrieves all dependencies with a specific branch UUID, no other filtering is done by this method. + */ + public List<ScaDependencyDto> selectByBranchUuid(DbSession dbSession, String branchUuid) { + return mapper(dbSession).selectByBranchUuid(branchUuid); + } + + public List<ScaDependencyDto> selectByQuery(DbSession session, ScaDependenciesQuery scaDependenciesQuery, Pagination pagination) { + return mapper(session).selectByQuery(scaDependenciesQuery, pagination); + } + + public int countByQuery(DbSession session, ScaDependenciesQuery scaDependenciesQuery) { + return mapper(session).countByQuery(scaDependenciesQuery); + } + + public void update(DbSession session, ScaDependencyDto scaDependencyDto) { + mapper(session).update(scaDependencyDto); + } +} diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/sca/ScaDependenciesMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/sca/ScaDependenciesMapper.java new file mode 100644 index 00000000000..16cdc145978 --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/sca/ScaDependenciesMapper.java @@ -0,0 +1,40 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db.sca; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import org.sonar.db.Pagination; + +public interface ScaDependenciesMapper { + void insert(ScaDependencyDto dto); + + void deleteByUuid(String uuid); + + ScaDependencyDto selectByUuid(String uuid); + + List<ScaDependencyDto> selectByBranchUuid(String branchUuid); + + List<ScaDependencyDto> selectByQuery(@Param("query") ScaDependenciesQuery query, @Param("pagination") Pagination pagination); + + void update(ScaDependencyDto dto); + + int countByQuery(@Param("query") ScaDependenciesQuery query); +} diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/sca/ScaDependenciesQuery.java b/server/sonar-db-dao/src/main/java/org/sonar/db/sca/ScaDependenciesQuery.java new file mode 100644 index 00000000000..7dde3be94b2 --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/sca/ScaDependenciesQuery.java @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db.sca; + +import java.util.Locale; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; + +import static org.sonar.db.DaoUtils.buildLikeValue; +import static org.sonar.db.WildcardPosition.BEFORE_AND_AFTER; + +public record ScaDependenciesQuery(String branchUuid, @Nullable String query) { + + /** + * Used by MyBatis mapper + */ + @CheckForNull + public String likeQuery() { + return query == null ? null : buildLikeValue(query, BEFORE_AND_AFTER).toLowerCase(Locale.ENGLISH); + } +} diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/sca/ScaDependencyDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/sca/ScaDependencyDto.java new file mode 100644 index 00000000000..6d1a87113f1 --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/sca/ScaDependencyDto.java @@ -0,0 +1,184 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db.sca; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Represents a Software Composition Analysis (SCA) dependency, associated with a component. + * The component will be a package component nested inside a project branch component. + * + * @param uuid primary key + * @param componentUuid the component the dependency is associated with + * @param packageUrl package URL following the PURL specification + * @param packageManager package manager e.g. PYPI + * @param packageName package name e.g. "urllib3" + * @param version package version e.g. "1.25.6" + * @param direct is this a direct dependency of the project + * @param scope the scope of the dependency e.g. "development" + * @param dependencyFilePath path to the file where the dependency was found, preferring the "manifest" to the "lockfile" + * @param licenseExpression an SPDX license expression (NOT a single license, can have parens/AND/OR) + * @param known is this package and version known to Sonar (if not it be internal, could be malicious, could be from a weird repo) + * @param createdAt timestamp of creation + * @param updatedAt timestamp of most recent update + */ +public record ScaDependencyDto( + String uuid, + String componentUuid, + String packageUrl, + PackageManager packageManager, + String packageName, + String version, + boolean direct, + String scope, + String dependencyFilePath, + String licenseExpression, + boolean known, + long createdAt, + long updatedAt) { + + // These need to be in sync with the database but because the db migration module and this module don't + // depend on each other, we can't make one just refer to the other. + public static final int PACKAGE_URL_MAX_LENGTH = 400; + public static final int PACKAGE_MANAGER_MAX_LENGTH = 20; + public static final int PACKAGE_NAME_MAX_LENGTH = 400; + public static final int VERSION_MAX_LENGTH = 400; + public static final int SCOPE_MAX_LENGTH = 100; + public static final int DEPENDENCY_FILE_PATH_MAX_LENGTH = 1000; + public static final int LICENSE_EXPRESSION_MAX_LENGTH = 400; + + public ScaDependencyDto { + // We want these to raise errors and not silently put junk values in the db + checkLength(packageUrl, PACKAGE_URL_MAX_LENGTH, "packageUrl"); + checkLength(packageName, PACKAGE_NAME_MAX_LENGTH, "packageName"); + checkLength(version, VERSION_MAX_LENGTH, "version"); + checkLength(scope, SCOPE_MAX_LENGTH, "scope"); + checkLength(dependencyFilePath, DEPENDENCY_FILE_PATH_MAX_LENGTH, "dependencyFilePath"); + checkLength(licenseExpression, LICENSE_EXPRESSION_MAX_LENGTH, "licenseExpression"); + } + + private static void checkLength(String value, int maxLength, String name) { + checkArgument(value.length() <= maxLength, "Maximum length of %s is %s: %s", name, maxLength, value); + } + + public static class Builder { + private String uuid; + private String componentUuid; + private String packageUrl; + private PackageManager packageManager; + private String packageName; + private String version; + private boolean direct; + private String scope; + private String dependencyFilePath; + private String licenseExpression; + private boolean known; + private long createdAt; + private long updatedAt; + + public Builder setUuid(String uuid) { + this.uuid = uuid; + return this; + } + + public Builder setComponentUuid(String componentUuid) { + this.componentUuid = componentUuid; + return this; + } + + public Builder setPackageUrl(String packageUrl) { + this.packageUrl = packageUrl; + return this; + } + + public Builder setPackageManager(PackageManager packageManager) { + this.packageManager = packageManager; + return this; + } + + public Builder setPackageName(String packageName) { + this.packageName = packageName; + return this; + } + + public Builder setVersion(String version) { + this.version = version; + return this; + } + + public Builder setDirect(boolean direct) { + this.direct = direct; + return this; + } + + public Builder setScope(String scope) { + this.scope = scope; + return this; + } + + public Builder setDependencyFilePath(String dependencyFilePath) { + this.dependencyFilePath = dependencyFilePath; + return this; + } + + public Builder setLicenseExpression(String licenseExpression) { + this.licenseExpression = licenseExpression; + return this; + } + + public Builder setKnown(boolean known) { + this.known = known; + return this; + } + + public Builder setCreatedAt(long createdAt) { + this.createdAt = createdAt; + return this; + } + + public Builder setUpdatedAt(long updatedAt) { + this.updatedAt = updatedAt; + return this; + } + + public ScaDependencyDto build() { + return new ScaDependencyDto( + uuid, componentUuid, packageUrl, packageManager, packageName, version, direct, scope, dependencyFilePath, licenseExpression, known, createdAt, updatedAt + ); + } + } + + public Builder toBuilder() { + return new Builder() + .setUuid(this.uuid) + .setComponentUuid(this.componentUuid) + .setPackageUrl(this.packageUrl) + .setPackageManager(this.packageManager) + .setPackageName(this.packageName) + .setVersion(this.version) + .setDirect(this.direct) + .setScope(this.scope) + .setDependencyFilePath(this.dependencyFilePath) + .setLicenseExpression(this.licenseExpression) + .setKnown(this.known) + .setCreatedAt(this.createdAt) + .setUpdatedAt(this.updatedAt); + } +} diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/sca/package-info.java b/server/sonar-db-dao/src/main/java/org/sonar/db/sca/package-info.java new file mode 100644 index 00000000000..91273c3d426 --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/sca/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.db.sca; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/sca/ScaDependenciesMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/sca/ScaDependenciesMapper.xml new file mode 100644 index 00000000000..6ec1e6d3948 --- /dev/null +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/sca/ScaDependenciesMapper.xml @@ -0,0 +1,113 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "mybatis-3-mapper.dtd"> +<mapper namespace="org.sonar.db.sca.ScaDependenciesMapper"> + <sql id="scaDependenciesColumns"> + sd.uuid as uuid, + sd.component_uuid as componentUuid, + sd.package_url as packageUrl, + sd.package_manager as packageManager, + sd.package_name as packageName, + sd.version as version, + sd.direct as direct, + sd.scope as scope, + sd.dependency_file_path as dependencyFilePath, + sd.license_expression as licenseExpression, + sd.known as known, + sd.created_at as createdAt, + sd.updated_at as updatedAt + </sql> + + <insert id="insert" parameterType="org.sonar.db.sca.ScaDependencyDto" useGeneratedKeys="false"> + insert into sca_dependencies ( + uuid, + component_uuid, + package_url, + package_manager, + package_name, + version, + direct, + scope, + dependency_file_path, + license_expression, + known, + created_at, + updated_at + ) values ( + #{uuid,jdbcType=VARCHAR}, + #{componentUuid,jdbcType=VARCHAR}, + #{packageUrl,jdbcType=VARCHAR}, + #{packageManager,jdbcType=VARCHAR}, + #{packageName,jdbcType=VARCHAR}, + #{version,jdbcType=VARCHAR}, + #{direct,jdbcType=BOOLEAN}, + #{scope,jdbcType=VARCHAR}, + #{dependencyFilePath,jdbcType=VARCHAR}, + #{licenseExpression,jdbcType=VARCHAR}, + #{known,jdbcType=BOOLEAN}, + #{createdAt,jdbcType=BIGINT}, + #{updatedAt,jdbcType=BIGINT} + ) + </insert> + + <delete id="deleteByUuid" parameterType="string"> + delete from sca_dependencies + where uuid = #{uuid,jdbcType=VARCHAR} + </delete> + + <select id="selectByUuid" parameterType="string" resultType="org.sonar.db.sca.ScaDependencyDto"> + select <include refid="scaDependenciesColumns"/> + from sca_dependencies pd + where sd.uuid = #{uuid,jdbcType=VARCHAR} + </select> + + <select id="selectByBranchUuid" parameterType="string" resultType="org.sonar.db.sca.ScaDependencyDto"> + select <include refid="scaDependenciesColumns"/> + from sca_dependencies sd + inner join components c on sd.component_uuid = c.uuid + where c.branch_uuid = #{branchUuid,jdbcType=VARCHAR} + </select> + + <select id="selectByQuery" parameterType="map" resultType="org.sonar.db.sca.ScaDependencyDto"> + select <include refid="scaDependenciesColumns"/> + <include refid="sqlSelectByQuery" /> + ORDER BY c.kee ASC + <include refid="org.sonar.db.common.Common.pagination"/> + </select> + + <select id="countByQuery" resultType="int"> + select count(sd.uuid) + <include refid="sqlSelectByQuery" /> + </select> + + <sql id="sqlSelectByQuery"> + from sca_dependencies sd + inner join components c on sd.component_uuid = c.uuid + where c.branch_uuid = #{query.branchUuid,jdbcType=VARCHAR} + <if test="query.query() != null"> + AND ( + c.kee = #{query.query,jdbcType=VARCHAR} + OR lower(c.long_name) LIKE #{query.likeQuery} ESCAPE '/' + ) + </if> + </sql> + + <update id="update" parameterType="org.sonar.db.sca.ScaDependencyDto" useGeneratedKeys="false"> + update sca_dependencies + set + uuid = #{uuid, jdbcType=VARCHAR}, + component_uuid = #{componentUuid, jdbcType=VARCHAR}, + package_url = #{packageUrl, jdbcType=VARCHAR}, + package_manager = #{packageManager, jdbcType=VARCHAR}, + package_name = #{packageName, jdbcType=VARCHAR}, + version = #{version, jdbcType=VARCHAR}, + direct = #{direct, jdbcType=BOOLEAN}, + scope = #{scope, jdbcType=VARCHAR}, + dependency_file_path = #{dependencyFilePath, jdbcType=VARCHAR}, + license_expression = #{licenseExpression, jdbcType=VARCHAR}, + known = #{known, jdbcType=BOOLEAN}, + updated_at = #{updatedAt, jdbcType=BIGINT} + where + uuid = #{uuid, jdbcType=VARCHAR} + </update> + +</mapper> diff --git a/server/sonar-db-dao/src/schema/schema-sq.ddl b/server/sonar-db-dao/src/schema/schema-sq.ddl index 5e13a691d7a..07b6b866060 100644 --- a/server/sonar-db-dao/src/schema/schema-sq.ddl +++ b/server/sonar-db-dao/src/schema/schema-sq.ddl @@ -1032,6 +1032,24 @@ CREATE TABLE "SAML_MESSAGE_IDS"( ALTER TABLE "SAML_MESSAGE_IDS" ADD CONSTRAINT "PK_SAML_MESSAGE_IDS" PRIMARY KEY("UUID"); CREATE UNIQUE NULLS NOT DISTINCT INDEX "SAML_MESSAGE_IDS_UNIQUE" ON "SAML_MESSAGE_IDS"("MESSAGE_ID" NULLS FIRST); +CREATE TABLE "SCA_DEPENDENCIES"( + "UUID" CHARACTER VARYING(40) NOT NULL, + "COMPONENT_UUID" CHARACTER VARYING(40) NOT NULL, + "PACKAGE_URL" CHARACTER VARYING(400) NOT NULL, + "PACKAGE_MANAGER" CHARACTER VARYING(20) NOT NULL, + "PACKAGE_NAME" CHARACTER VARYING(400) NOT NULL, + "VERSION" CHARACTER VARYING(400) NOT NULL, + "DIRECT" BOOLEAN NOT NULL, + "SCOPE" CHARACTER VARYING(100) NOT NULL, + "DEPENDENCY_FILE_PATH" CHARACTER VARYING(1000) NOT NULL, + "LICENSE_EXPRESSION" CHARACTER VARYING(400) NOT NULL, + "KNOWN" BOOLEAN NOT NULL, + "CREATED_AT" BIGINT NOT NULL, + "UPDATED_AT" BIGINT NOT NULL +); +ALTER TABLE "SCA_DEPENDENCIES" ADD CONSTRAINT "PK_SCA_DEPENDENCIES" PRIMARY KEY("UUID"); +CREATE INDEX "SCA_DEPS_COMPONENT_UUID" ON "SCA_DEPENDENCIES"("COMPONENT_UUID" NULLS FIRST); + CREATE TABLE "SCANNER_ANALYSIS_CACHE"( "BRANCH_UUID" CHARACTER VARYING(40) NOT NULL, "DATA" BINARY LARGE OBJECT NOT NULL diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/sca/PackageManagerTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/sca/PackageManagerTest.java new file mode 100644 index 00000000000..2ae5705624a --- /dev/null +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/sca/PackageManagerTest.java @@ -0,0 +1,34 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db.sca; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class PackageManagerTest { + + @Test + void test_namesAreShortEnough() { + for (PackageManager packageManager : PackageManager.values()) { + assertThat(packageManager.name().length()).isLessThanOrEqualTo(ScaDependencyDto.PACKAGE_MANAGER_MAX_LENGTH); + } + } +} diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/sca/ScaDependencyDtoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/sca/ScaDependencyDtoTest.java new file mode 100644 index 00000000000..96b3df92fe8 --- /dev/null +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/sca/ScaDependencyDtoTest.java @@ -0,0 +1,45 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db.sca; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class ScaDependencyDtoTest { + + @Test + void toBuilder_build_shouldRoundTrip() { + var scaDependencyDto = new ScaDependencyDto("scaDependencyUuid", + "componentUuid", + "packageUrl", + PackageManager.MAVEN, + "foo:bar", + "1.0.0", + true, + "compile", + "pom.xml", + "BSD-3-Clause", + true, + 1L, + 2L); + assertThat(scaDependencyDto).isEqualTo(scaDependencyDto.toBuilder().build()); + } +} diff --git a/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/DbTester.java b/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/DbTester.java index e8915f097dd..7b0cae590de 100644 --- a/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/DbTester.java +++ b/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/DbTester.java @@ -57,6 +57,7 @@ import org.sonar.db.property.PropertyDbTester; import org.sonar.db.qualitygate.QualityGateDbTester; import org.sonar.db.qualityprofile.QualityProfileDbTester; import org.sonar.db.rule.RuleDbTester; +import org.sonar.db.sca.ScaDependenciesDbTester; import org.sonar.db.source.FileSourceTester; import org.sonar.db.user.UserDbTester; import org.sonar.db.webhook.WebhookDbTester; @@ -98,6 +99,7 @@ public class DbTester extends AbstractDbTester<TestDbImpl> implements BeforeEach private final AuditDbTester auditDbTester; private final AnticipatedTransitionDbTester anticipatedTransitionDbTester; private final ProjectDependenciesDbTester projectDependenciesDbTester; + private final ScaDependenciesDbTester scaDependenciesDbTester; private DbTester(UuidFactory uuidFactory, System2 system2, @Nullable String schemaPath, AuditPersister auditPersister, MyBatisConfExtension... confExtensions) { super(TestDbImpl.create(schemaPath, confExtensions)); @@ -131,6 +133,7 @@ public class DbTester extends AbstractDbTester<TestDbImpl> implements BeforeEach this.auditDbTester = new AuditDbTester(this); this.anticipatedTransitionDbTester = new AnticipatedTransitionDbTester(this); this.projectDependenciesDbTester = new ProjectDependenciesDbTester(this); + this.scaDependenciesDbTester = new ScaDependenciesDbTester(this); } public static DbTester create() { @@ -282,6 +285,8 @@ public class DbTester extends AbstractDbTester<TestDbImpl> implements BeforeEach return projectDependenciesDbTester; } + public ScaDependenciesDbTester getScaDependenciesDbTester() { return scaDependenciesDbTester; } + @Override public void afterEach(ExtensionContext context) throws Exception { after(); diff --git a/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/sca/ScaDependenciesDbTester.java b/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/sca/ScaDependenciesDbTester.java new file mode 100644 index 00000000000..fec30bea3fa --- /dev/null +++ b/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/sca/ScaDependenciesDbTester.java @@ -0,0 +1,87 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db.sca; + +import java.util.function.Consumer; +import javax.annotation.Nullable; +import org.sonar.db.DbClient; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; + +import static org.apache.commons.lang3.StringUtils.EMPTY; + +public class ScaDependenciesDbTester { + private final DbTester db; + private final DbClient dbClient; + + public ScaDependenciesDbTester(DbTester db) { + this.db = db; + this.dbClient = db.getDbClient(); + } + + public ComponentDto newPackageComponentDto(String branchUuid, String suffix, @Nullable Consumer<ComponentDto> dtoPopulator) { + var name = "foo:bar"; + ComponentDto componentDto = new ComponentDto().setUuid("uuid" + suffix) + .setKey("key" + suffix) + .setUuidPath("uuidPath" + suffix) + .setName(name + suffix) + .setLongName("long_name" + suffix) + .setBranchUuid(branchUuid); + + if (dtoPopulator != null) { + dtoPopulator.accept(componentDto); + } + + return componentDto; + } + + public ScaDependencyDto newScaDependencyDto(ComponentDto componentDto, String suffix) { + return new ScaDependencyDto("scaDependencyUuid" + suffix, + componentDto.uuid(), + "packageUrl" + suffix, + PackageManager.MAVEN, + componentDto.name(), + "1.0.0", + true, + "compile", + "pom.xml", + "BSD-3-Clause", + true, + 1L, + 2L); + } + + public ScaDependencyDto insertScaDependency(String branchUuid) { + return insertScaDependency(branchUuid, EMPTY, null); + } + + public ScaDependencyDto insertScaDependency(String branchUuid, String suffix) { + return insertScaDependency(branchUuid, suffix, null); + } + + public ScaDependencyDto insertScaDependency(String branchUuid, String suffix, @Nullable Consumer<ComponentDto> dtoPopulator) { + var componentDto = newPackageComponentDto(branchUuid, suffix, dtoPopulator); + + db.components().insertComponent(componentDto); + var scaDependencyDto = newScaDependencyDto(componentDto, suffix); + dbClient.scaDependenciesDao().insert(db.getSession(), scaDependencyDto); + return scaDependencyDto; + } +} |