aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/CODEOWNERS6
-rw-r--r--.vscode/settings.json3
-rw-r--r--README.md2
-rw-r--r--build.gradle193
-rw-r--r--gradle.properties11
-rw-r--r--plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java2
-rw-r--r--plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/architecture/ArchitectureSensor.java17
-rw-r--r--plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/global/ErrorThrowingSensor.java71
-rw-r--r--plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneIssuePerFileSensor.java13
-rw-r--r--plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/XooRulesDefinition.java3
-rw-r--r--plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/architecture/ArchitectureSensorTest.java2
-rw-r--r--plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/rule/XooRulesDefinitionTest.java3
-rw-r--r--server/sonar-alm-client/src/test/java/org/sonar/alm/client/gitlab/GitlabApplicationClientTest.java18
-rw-r--r--server/sonar-ce-common/src/main/java/org/sonar/ce/common/scanner/ScannerReportReader.java2
-rw-r--r--server/sonar-ce-common/src/testFixtures/java/org/sonar/ce/common/scanner/ScannerReportReaderRule.java12
-rw-r--r--server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryImplIT.java39
-rw-r--r--server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepIT.java5
-rw-r--r--server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepIT.java112
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/component/TreeRootHolder.java7
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/formula/AverageFormula.java161
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/formula/DistributionFormula.java89
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java3
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationCheck.java2
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationChecksStep.java5
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/metric/MetricRepository.java12
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryImpl.java10
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolder.java6
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderImpl.java10
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodOrigin.java25
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/scanner/ScannerReportReaderImpl.java8
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java16
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStep.java113
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java10
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/AverageFormulaExecutionTest.java128
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/AverageFormulaTest.java219
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/DistributionFormulaExecutionTest.java122
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/DistributionFormulaTest.java106
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationChecksStepTest.java15
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderRule.java10
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/scanner/ScannerReportReaderImplTest.java8
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepTest.java194
-rw-r--r--server/sonar-ce-task-projectanalysis/src/testFixtures/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryRule.java5
-rw-r--r--server/sonar-ce-task/src/test/java/org/sonar/ce/task/step/ComputationStepExecutorTest.java1
-rw-r--r--server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java3
-rw-r--r--server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java7
-rw-r--r--server/sonar-db-dao/src/it/java/org/sonar/db/alm/setting/ProjectAlmSettingDaoIT.java158
-rw-r--r--server/sonar-db-dao/src/it/java/org/sonar/db/component/BranchDaoIT.java35
-rw-r--r--server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java74
-rw-r--r--server/sonar-db-dao/src/it/java/org/sonar/db/report/RegulatoryReportDaoIT.java2
-rw-r--r--server/sonar-db-dao/src/it/java/org/sonar/db/user/GroupDaoIT.java41
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingDto.java17
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingQuery.java71
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java4
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java2
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeCommands.java26
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeDao.java2
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeMapper.java8
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/qualitygate/QualityGateFindingDto.java5
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupQuery.java31
-rw-r--r--server/sonar-db-dao/src/main/resources/org/sonar/db/alm/setting/ProjectAlmSettingMapper.xml6
-rw-r--r--server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml9
-rw-r--r--server/sonar-db-dao/src/main/resources/org/sonar/db/purge/PurgeMapper.xml14
-rw-r--r--server/sonar-db-dao/src/main/resources/org/sonar/db/qualitygate/QualityGateMapper.xml6
-rw-r--r--server/sonar-db-dao/src/main/resources/org/sonar/db/report/RegulatoryReportMapper.xml4
-rw-r--r--server/sonar-db-dao/src/main/resources/org/sonar/db/user/GroupMapper.xml6
-rw-r--r--server/sonar-db-dao/src/schema/schema-sq.ddl110
-rw-r--r--server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java2
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddAnalysisUuidOnArchitectureGraphsIT.java55
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddAssigneeNameToScaIssuesReleasesIT.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddAssigneeToScaIssuesReleasesIT.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddCommentToScaIssuesReleasesChangesTableIT.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddGraphVersionOnArchitectureGraphsIT.java54
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddIsNewToScaDependenciesTableIT.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddIsNewToScaReleasesTableIT.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddPerspectiveKeyOnArchitectureGraphsIT.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddPolicyUpdatedAtToScaLicenseProfilesTableIT.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddPreviousManualStatusToScaIssuesReleasesIT.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddStatusToScaIssuesReleasesTableTestIT.java57
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/BackfillRemoveAssigneeNameFromIssueReleaseChangesIT.java89
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateIndexOnScaIssuesReleaseChangesReleaseIdIT.java52
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaAnalysesTableIT.java64
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaEncounteredLicensesTableIT.java59
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaIssuesReleasesChangesTableIT.java63
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfileCategoriesTableIT.java60
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfileCustomizationsTableIT.java61
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfileProjectsTableIT.java60
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfilesTableIT.java61
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnArchitectureGraphsIT.java56
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaAnalysesIT.java52
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaEncounteredLicensesIT.java52
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfileCategoriesIT.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfileCustomizationsIT.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfileProjectsIT.java52
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfilesIT.java52
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaReleasesIT.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropAssigneeNameFromScaIssuesReleasesIT.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropChangeTypeFromScaIssuesReleasesChangesTableIT.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropIndexOnArchitectureGraphsIT.java59
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaDependenciesTableIT.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaReleasesTableIT.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/MigrateRemoveDuplicateScaReleasesIT.java138
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/MigrateRemoveNonCanonicalScaEncounteredLicensesIT.java69
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/MigrateToIsNewOnScaDependenciesIT.java82
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/MigrateToIsNewOnScaReleasesIT.java84
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTableIT.java72
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/PopulateStatusColumnForScaIssuesReleasesTableTestIT.java75
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/UpdateArchitectureGraphsSourceColumnRenameIT.java63
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaIssuesReleasesOpenStatusIT.java81
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaIssuesReleasesStatusColumnNotNullableTestIT.java68
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullableIT.java66
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddAnalysisParametersToScaAnalysesIT.java54
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddIndexOnAlmRepoInProjectAlmSettingsIT.java56
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddManualSeverityWarningToScaIssuesTest.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddOrganizationUuidToScaLicenseProfilesIT.java54
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssuesTest.java63
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddWithdrawnToScaVulnerabilityIssuesTest.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/CreateUniqueIndexOnScaLicenseProfilesIT.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/DropUniqueIndexOnScaLicenseProfilesTest.java57
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTableIT.java80
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/UpdateRulesNameColumnSizeTest.java55
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullableTestIT.java68
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MigrationConfigurationModule.java2
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/def/BooleanColumnDef.java2
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/sql/DropColumnsBuilder.java6
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/DropColumnChange.java12
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/CreateUniqueIndexOnArchitectureGraphs.java29
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddAnalysisUuidOnArchitectureGraphs.java54
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddAssigneeNameToScaIssuesReleases.java54
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddAssigneeToScaIssuesReleases.java54
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddCommentToScaIssuesReleasesChangesTable.java55
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddGraphVersionOnArchitectureGraphsTable.java55
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddIsNewToScaDependenciesTable.java53
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddIsNewToScaReleasesTable.java53
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddPerspectiveKeyOnArchitectureGraphs.java54
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddPolicyUpdatedAtToScaLicenseProfilesTable.java52
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddPreviousManualStatusToScaIssuesReleases.java54
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddStatusToScaIssuesReleasesTable.java53
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/BackfillRemoveAssigneeNameFromIssueReleaseChanges.java69
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateIndexOnScaIssuesReleaseChangesReleaseId.java54
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaAnalysesTable.java57
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaEncounteredLicensesTable.java55
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaIssuesReleasesChangesTable.java55
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfileCategoriesTable.java59
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfileCustomizationsTable.java63
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfileProjectsTable.java54
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfilesTable.java56
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnArchitectureGraphs.java61
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaAnalyses.java55
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaEncounteredLicenses.java56
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfileCategories.java58
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfileCustomizations.java58
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfileProjects.java56
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfiles.java56
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaReleases.java57
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DbVersion202503.java47
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropAssigneeNameFromScaIssuesReleases.java (renamed from server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/NetworkInterfaceProvider.java)20
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropChangeTypeFromScaIssuesReleasesChangesTable.java32
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropIndexOnArchitectureGraphs.java33
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaDependenciesTable.java32
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaReleasesTable.java32
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/MigrateRemoveDuplicateScaReleases.java112
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/MigrateRemoveNonCanonicalScaEncounteredLicenses.java90
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/MigrateToIsNewOnScaDependencies.java31
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/MigrateToIsNewOnScaReleases.java31
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/MigrateToIsNewOnScaTable.java53
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTable.java50
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/PopulateStatusColumnForScaIssuesReleasesTable.java51
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/UpdateArchitectureGraphsSourceColumnRename.java33
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaIssuesReleasesOpenStatus.java47
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaIssuesReleasesStatusColumnNotNullable.java54
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullable.java53
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddAnalysisParametersToScaAnalyses.java54
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddIndexOnAlmRepoInProjectAlmSettings.java35
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddManualSeverityWarningToScaIssues.java54
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddOrganizationUuidToScaLicenseProfiles.java57
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssues.java67
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddWithdrawnToScaVulnerabilityIssues.java54
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/CreateUniqueIndexOnScaLicenseProfiles.java58
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DbVersion202504.java44
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DropUniqueIndexOnScaLicenseProfiles.java32
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTable.java50
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/UpdateRulesNameColumnSize.java53
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable.java54
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/package-info.java (renamed from sonar-ws/src/main/java/org/sonarqube/ws/client/roots/package-info.java)5
-rw-r--r--server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v202504/DbVersion202504Test.java (renamed from sonar-ws/src/main/java/org/sonarqube/ws/client/roots/SetRootRequest.java)33
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/cluster/ClusterAppStateImpl.java23
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/es/EsSettings.java4
-rw-r--r--server/sonar-main/src/test/java/org/sonar/application/cluster/AppNodesClusterHostsConsistencyTest.java4
-rw-r--r--server/sonar-main/src/test/java/org/sonar/application/cluster/ClusterAppStateImplTest.java18
-rw-r--r--server/sonar-main/src/test/java/org/sonar/application/es/EsSettingsTest.java19
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/ProcessProperties.java5
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/cluster/hz/DistributedLock.java85
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/cluster/hz/DistributedReference.java69
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMember.java3
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberImpl.java9
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/cluster/hz/DistributedLockTest.java176
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/cluster/hz/DistributedReferenceTest.java112
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/cluster/hz/MockIMap.java577
-rw-r--r--server/sonar-server-common/build.gradle3
-rw-r--r--server/sonar-server-common/src/it/java/org/sonar/server/component/index/EntityDefinitionIndexerIT.java73
-rw-r--r--server/sonar-server-common/src/it/java/org/sonar/server/rule/index/RuleIndexIT.java16
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/component/index/EntityDefinitionIndexer.java33
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/feature/SonarQubeFeature.java8
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/SearchRequest.java11
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/TaintChecker.java4
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java10
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java2
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java3
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/index/SecurityStandardCategoryStatistics.java33
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationHandler.java2
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/IssueWorkflow.java7
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/codequalityissue/CodeQualityIssueWorkflow.java12
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/codequalityissue/CodeQualityIssueWorkflowDefinition.java27
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/securityhotspot/SecurityHotspotWorkflow.java11
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/securityhotspot/SecurityHotspotWorkflowDefinition.java15
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/securityhotspot/SecurityHotspotWorkflowTransition.java11
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/metric/StandardToMQRMetrics.java9
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/network/NetworkInterfaceProvider.java (renamed from server/sonar-server-common/src/main/java/org/sonar/server/webhook/NetworkInterfaceProvider.java)2
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/network/package-info.java (renamed from server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/statemachine/package-info.java)2
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleDoc.java11
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleIndex.java35
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleIndexDefinition.java2
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleQuery.java73
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/security/SecurityStandards.java13
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/webhook/WebhookCustomDns.java1
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/webhook/WebhookModule.java1
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/TaintCheckerTest.java24
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/index/SecurityStandardCategoryStatisticsTest.java50
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowForCodeQualityIssuesTest.java48
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowForSecurityHotspotsTest.java19
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/log/DistributedServerLoggingTest.java6
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/network/NetworkInterfaceProviderTest.java (renamed from server/sonar-server-common/src/test/java/org/sonar/server/webhook/NetworkInterfaceProviderTest.java)2
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/rule/index/RuleDocTest.java1
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/security/SecurityStandardsTest.java8
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/webhook/WebhookCallerImplTest.java4
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/webhook/WebhookCustomDnsTest.java7
-rw-r--r--server/sonar-statemachine/build.gradle29
-rw-r--r--server/sonar-statemachine/src/main/java/org/sonar/issue/workflow/statemachine/State.java (renamed from server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/statemachine/State.java)2
-rw-r--r--server/sonar-statemachine/src/main/java/org/sonar/issue/workflow/statemachine/StateMachine.java (renamed from server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/statemachine/StateMachine.java)2
-rw-r--r--server/sonar-statemachine/src/main/java/org/sonar/issue/workflow/statemachine/Transition.java (renamed from server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/statemachine/Transition.java)17
-rw-r--r--server/sonar-statemachine/src/main/java/org/sonar/issue/workflow/statemachine/package-info.java23
-rw-r--r--server/sonar-statemachine/src/test/java/org/sonar/issue/workflow/statemachine/StateMachineTest.java (renamed from server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/statemachine/StateMachineTest.java)2
-rw-r--r--server/sonar-statemachine/src/test/java/org/sonar/issue/workflow/statemachine/StateTest.java (renamed from server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/statemachine/StateTest.java)2
-rw-r--r--server/sonar-statemachine/src/test/java/org/sonar/issue/workflow/statemachine/TransitionTest.java (renamed from server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/statemachine/TransitionTest.java)6
-rw-r--r--server/sonar-telemetry/src/main/java/org/sonar/telemetry/metrics/TelemetryMetricsMapper.java70
-rw-r--r--server/sonar-telemetry/src/test/java/org/sonar/telemetry/metrics/TelemetryMetricsMapperTest.java96
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/TooManyRequestsException.java27
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java7
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java15
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java28
-rw-r--r--server/sonar-webserver-auth/src/it/java/org/sonar/server/authentication/JwtHttpHandlerIT.java110
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/ActiveTimeoutProvider.java26
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/HardcodedActiveTimeoutProvider.java34
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java54
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposer.java2
-rw-r--r--server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/HardcodedActiveTimeoutProviderTest.java (renamed from server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/NetworkInterfaceProviderTest.java)23
-rw-r--r--server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/OAuth2AuthenticationParametersImplTest.java37
-rw-r--r--server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposerTest.java4
-rw-r--r--server/sonar-webserver-common/src/it/java/org/sonar/server/common/component/ComponentUpdaterIT.java31
-rw-r--r--server/sonar-webserver-common/src/it/java/org/sonar/server/common/user/service/UserServiceIT.java46
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/component/ComponentUpdater.java5
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupSearchRequest.java2
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupService.java2
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/GitUrlParser.java222
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingSearchStrategy.java72
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsSearchRequest.java5
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsService.java81
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/service/UserService.java12
-rw-r--r--server/sonar-webserver-common/src/test/java/org/sonar/server/common/group/service/GroupServiceTest.java33
-rw-r--r--server/sonar-webserver-common/src/test/java/org/sonar/server/common/projectbindings/service/GitUrlParserTest.java280
-rw-r--r--server/sonar-webserver-common/src/test/java/org/sonar/server/common/projectbindings/service/ProjectBindingsServiceTest.java112
-rw-r--r--server/sonar-webserver-core/build.gradle2
-rw-r--r--server/sonar-webserver-core/src/it/java/org/sonar/server/startup/RegisterMetricsIT.java5
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/issue/index/AsyncIssueIndexCreationTelemetry.java28
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/RulesRegistrationContext.java8
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterMetrics.java30
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/issue/index/AsyncIssueIndexCreationTelemetryTest.java25
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/rule/registration/RulesRegistrationContextTest.java69
-rw-r--r--server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java110
-rw-r--r--server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java12
-rw-r--r--server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java35
-rw-r--r--server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java14
-rw-r--r--server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexSecurityReportsTest.java63
-rw-r--r--server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java81
-rw-r--r--server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java9
-rw-r--r--server/sonar-webserver-monitoring/build.gradle11
-rw-r--r--server/sonar-webserver-monitoring/src/main/java/org/sonar/server/monitoring/MainCollector.java16
-rw-r--r--server/sonar-webserver-monitoring/src/main/java/org/sonar/server/monitoring/ServerMonitoringMetrics.java15
-rw-r--r--server/sonar-webserver-monitoring/src/main/java/org/sonar/server/monitoring/ce/RecentTasksDurationTask.java24
-rw-r--r--server/sonar-webserver-monitoring/src/test/java/org/sonar/server/monitoring/MainCollectorTest.java45
-rw-r--r--server/sonar-webserver-monitoring/src/test/java/org/sonar/server/monitoring/ServerMonitoringMetricsTest.java12
-rw-r--r--server/sonar-webserver-monitoring/src/test/java/org/sonar/server/monitoring/ce/RecentTasksDurationTaskTest.java16
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/controller/EmailConfigurationController.java6
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/controller/GithubConfigurationController.java6
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/controller/GitlabConfigurationController.java6
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/DefaultGroupController.java6
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/GroupController.java17
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/request/GroupsSearchRestRequest.java9
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/mode/controller/ModeController.java3
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsController.java38
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/ProjectBindingsController.java3
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/request/ProjectBindingsSearchRestRequest.java18
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/UserController.java10
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGenerator.java4
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/UserRestResponseForAnonymousUsers.java1
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/UserRestResponseForLoggedInUsers.java7
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/ErrorMessages.java1
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java9
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/CommonWebConfig.java47
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/group/controller/DefaultGroupControllerTest.java6
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsControllerTest.java81
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGeneratorTest.java5
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandlerTest.java11
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/config/CommonWebConfigTest.java32
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/resources/http/http-client.env.json45
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/resources/http/project-bindings.http89
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/TransitionServiceIT.java2
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/SearchActionIT.java37
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionsActionIT.java13
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/live/LiveMeasureUpdaterWorkflowTest.java169
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualitygate/QualityGateCaycCheckerIT.java3
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualitygate/QualityGateConditionsUpdaterIT.java10
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/ws/ActivateRulesActionIT.java1
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/ws/DeactivateRulesActionIT.java1
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/rule/ws/SearchActionIT.java2
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/setting/ws/SetActionIT.java392
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/webhook/ws/CreateActionIT.java5
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/webhook/ws/DeleteActionIT.java3
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/webhook/ws/ListActionIT.java5
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/webhook/ws/UpdateActionIT.java5
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/feature/ws/ListAction.java2
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/TransitionService.java40
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java5
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java6
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java16
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java120
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureUpdaterWorkflow.java204
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java18
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/ActiveVersionEvaluator.java28
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateConditionsUpdater.java3
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ValidRatingMetrics.java14
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImpl.java2
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/ActivateRulesAction.java3
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RuleQueryFactory.java2
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RuleWsSupport.java6
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RulesWsParameters.java9
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/SearchAction.java381
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/ShowAction.java2
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DismissNoticeAction.java6
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/SearchAction.java7
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/WebhookSupport.java3
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/WebhooksWsModule.java3
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/batch/BatchIndexTest.java72
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionParserTest.java20
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/ActiveVersionEvaluatorTest.java76
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImplTest.java49
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/WebhookSupportTest.java1
-rw-r--r--server/sonar-webserver/build.gradle3
-rw-r--r--server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java12
-rw-r--r--server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java2
-rw-r--r--server/sonar-webserver/src/main/java/org/sonar/server/platform/web/CspFilter.java4
-rw-r--r--server/sonar-webserver/src/main/java/org/sonar/server/platform/web/NoCacheFilter.java (renamed from sonar-ws/src/main/java/org/sonarqube/ws/client/roots/UnsetRootRequest.java)39
-rw-r--r--server/sonar-webserver/src/test/java/org/sonar/server/app/EmbeddedTomcatTest.java39
-rw-r--r--server/sonar-webserver/src/test/java/org/sonar/server/platform/web/CspFilterTest.java3
-rw-r--r--server/sonar-webserver/src/test/java/org/sonar/server/platform/web/NoCacheFilterTest.java59
-rw-r--r--server/sonar-webserver/src/test/java/org/sonar/server/platform/web/StaticResourcesServletTest.java43
-rw-r--r--settings.gradle10
-rw-r--r--sonar-application/build.gradle12
-rw-r--r--sonar-application/bundled_plugins.gradle1
-rw-r--r--sonar-application/src/main/assembly/conf/sonar.properties18
-rw-r--r--sonar-application/src/main/assembly/security/CVE-review-and-treatment-status-sqcb.csv61
-rw-r--r--sonar-core/src/main/java/org/sonar/core/config/AiCodefixPropertyDefinitions.java47
-rw-r--r--sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java1
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java20
-rw-r--r--sonar-core/src/main/java/org/sonar/core/scadata/DefaultScaDataSourceImpl.java41
-rw-r--r--sonar-core/src/main/java/org/sonar/core/scadata/ScaDataSource.java33
-rw-r--r--sonar-core/src/main/java/org/sonar/core/scadata/package-info.java20
-rw-r--r--sonar-core/src/main/java/org/sonar/core/util/ProcessWrapperFactory.java46
-rw-r--r--sonar-core/src/test/java/org/sonar/core/scadata/DefaultScaDataSourceImplTest.java48
-rw-r--r--sonar-core/src/test/java/org/sonar/core/util/ProcessWrapperFactoryTest.java49
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/DefaultIndexedFile.java21
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/DefaultInputFile.java12
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/TestInputFileBuilder.java9
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/AbsolutePathPredicate.java21
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/ChangedFilePredicate.java8
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/DefaultFilePredicates.java4
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/FileExtensionPredicate.java5
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/FilenamePredicate.java4
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/NonHiddenFilesPredicate.java (renamed from sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/StatusPredicate.java)18
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/DefaultSensorDescriptor.java21
-rw-r--r--sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/DefaultIndexedFileTest.java10
-rw-r--r--sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/fs/DefaultInputFileTest.java17
-rw-r--r--sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/fs/TestInputFileBuilderTest.java2
-rw-r--r--sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/predicates/AbsolutePathPredicateTest.java51
-rw-r--r--sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/predicates/ChangedFilePredicateTest.java46
-rw-r--r--sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/predicates/FileExtensionPredicateTest.java14
-rw-r--r--sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/predicates/FilenamePredicateTest.java8
-rw-r--r--sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/predicates/NonHiddenFilesPredicateTest.java53
-rw-r--r--sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/sensor/internal/DefaultSensorDescriptorTest.java7
-rw-r--r--sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/analysisdata/AnalysisDataIT.java2
-rw-r--r--sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/bootstrap/BootstrapMediumIT.java16
-rw-r--r--sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/fs/FileSystemMediumIT.java416
-rw-r--r--sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/scm/ScmMediumIT.java2
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/JGitCleanupService.java47
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerMain.java66
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SpringScannerContainer.java4
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/RulesSeverityDetector.java7
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/RunMapper.java2
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ScannerWsClientProvider.java2
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliCacheService.java7
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliService.java61
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/ScaExecutor.java11
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/ScaProperties.java17
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/MutableModuleSettings.java65
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/MutableProjectSettings.java71
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectConfigurationProvider.java8
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/SpringModuleScanContainer.java4
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/DirectoryFileVisitor.java33
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/FileIndexer.java15
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/FilePreprocessor.java21
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/HiddenFilesProjectData.java77
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/HiddenFilesVisitorHelper.java112
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/ModuleInputComponentStore.java27
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/MutableFileSystem.java35
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/ProjectFileIndexer.java25
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/ProjectFilePreprocessor.java31
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/AbstractSensorWrapper.java9
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ModuleSensorContext.java5
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ProjectSensorContext.java16
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scm/git/CompositeBlameCommand.java9
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scm/git/NativeGitBlameCommand.java7
-rw-r--r--sonar-scanner-engine/src/main/resources/logback.xml13
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/RulesSeverityDetectorTest.java59
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/RunMapperTest.java70
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/CliCacheServiceTest.java11
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/CliServiceTest.java173
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/ScaExecutorTest.java34
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/ScaPropertiesTest.java45
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/ProjectConfigurationProviderTest.java14
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/DirectoryFileVisitorTest.java53
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/HiddenFilesProjectDataTest.java160
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/HiddenFilesVisitorHelperTest.java315
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/ModuleInputComponentStoreTest.java157
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/MutableFileSystemTest.java99
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java21
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/ModuleSensorContextTest.java4
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/ProjectSensorContextTest.java14
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scm/git/ChangedFileTest.java2
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scm/git/JGitUtilsTest.java7
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scm/git/NativeGitBlameCommandTest.java14
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/scanner/sca/echo_args.bat2
-rwxr-xr-xsonar-scanner-engine/src/test/resources/org/sonar/scanner/sca/echo_args.sh2
-rw-r--r--sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/sonar-project.properties4
-rw-r--r--sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/.hidden/.nestedHidden/.xoo1
-rw-r--r--sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/.hidden/.nestedHidden/Class.xoo8
-rw-r--r--sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/.hidden/.nestedHidden/visibleInHiddenFolder/.xoo1
-rw-r--r--sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/.hidden/.nestedHidden/visibleInHiddenFolder/Class.xoo8
-rw-r--r--sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/.hidden/.xoo1
-rw-r--r--sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/.hidden/Class.xoo8
-rw-r--r--sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/nonHidden/.hiddenInVisibleFolder/.xoo1
-rw-r--r--sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/nonHidden/.hiddenInVisibleFolder/.xoo.ignore0
-rw-r--r--sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/nonHidden/.hiddenInVisibleFolder/Class.xoo8
-rw-r--r--sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/nonHidden/.xoo1
-rw-r--r--sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/nonHidden/Class.xoo8
-rw-r--r--sonar-scanner-protocol/src/it/java/org/sonar/scanner/protocol/output/ScannerReportReaderIT.java12
-rw-r--r--sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java4
-rw-r--r--sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportReader.java8
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/client/BaseRequest.java16
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/client/DefaultWsClient.java8
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/client/WsClient.java3
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/client/WsRequest.java10
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java2
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/client/roots/RootsService.java82
-rw-r--r--sonar-ws/src/test/java/org/sonarqube/ws/client/BaseRequestTest.java9
-rw-r--r--sonar-ws/src/test/java/org/sonarqube/ws/client/PostRequestTest.java2
475 files changed, 15067 insertions, 3448 deletions
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index b16a72fc95d..e325338ffb1 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,5 +1,5 @@
gradle.properties @SonarSource/platform-onprem-squad
.github/CODEOWNERS @SonarSource/platform-onprem-squad
-/private/core-extension-securityreport/ @sonarsource/corpex-softwarequalities-squad
-/private/core-extension-enterprise-server/ @sonarsource/corpex-team
-/private/it-governance/ @sonarsource/corpex-team
+/private/core-extension-securityreport/ @sonarsource/orchestration-reporting-squad
+/private/core-extension-enterprise-server/ @sonarsource/orchestration-reporting-squad
+/private/it-governance/ @sonarsource/orchestration-reporting-squad
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 81b53b5bfb6..6b80b53c702 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -31,7 +31,8 @@
"**/node_modules": true,
"**/out": true,
"**/coverage": true,
- "**/target": true
+ "**/target": true,
+ "**/*.java": false
},
"javascript.preferences.importModuleSpecifier": "relative",
"typescript.preferences.importModuleSpecifier": "project-relative",
diff --git a/README.md b/README.md
index 423f48ab73e..4a645c8a8fa 100644
--- a/README.md
+++ b/README.md
@@ -102,7 +102,7 @@ You can also target a specific version of the webapp by updating the `webappVers
Historically our translations were stored in `sonar-core/src/main/resources/org/sonar/l10n/core.properties`, but this file is now deprecated and not updated anymore.
Default translations (in English) are now defined in the webapp repository, here:
-https://github.com/SonarSource/sonarqube-webapp/blob/master/server/sonar-web/src/main/js/l10n/default.ts
+https://github.com/SonarSource/sonarqube-webapp/blob/master/libs/sq-server-shared/src/l10n/default.ts
The format has changed but you can still have it as a `.properties` file format by running the following command:
diff --git a/build.gradle b/build.gradle
index cf1fe8f389c..8b33f890c46 100644
--- a/build.gradle
+++ b/build.gradle
@@ -13,8 +13,8 @@ plugins {
id 'com.jfrog.artifactory' version '5.2.5'
id "de.undercouch.download" version "5.6.0" apply false
id 'io.spring.dependency-management' version '1.1.7'
- id "org.cyclonedx.bom" version "2.2.0" apply false
- id 'org.sonarqube' version '6.0.1.5171'
+ id "org.cyclonedx.bom" version "2.3.1" apply false
+ id 'org.sonarqube' version '6.2.0.5505'
}
if (!JavaVersion.current().isCompatibleWith(VERSION_17)) {
@@ -39,6 +39,11 @@ allprojects {
apply plugin: 'com.jfrog.artifactory'
apply plugin: 'maven-publish'
+ // Global exclusion of eddsa dependency
+ configurations.all {
+ exclude group: 'net.i2p.crypto', module: 'eddsa'
+ }
+
ext.versionInSources = version
ext.buildNumber = System.getProperty("buildNumber")
// when no buildNumber is provided, then project version must end with '-SNAPSHOT'
@@ -229,8 +234,8 @@ subprojects {
}
ext {
- protobufVersion = '4.30.1'
- springSecurityVersion = '6.4.4'
+ protobufVersion = '4.31.1'
+ springSecurityVersion = '6.5.1'
elasticSearchClientVersion = '7.17.28'
}
@@ -269,10 +274,13 @@ subprojects {
transitive = false
}
- // global exclusions
+ // global exclusions and resolution strategies
all {
// do not conflict with com.sun.mail:javax.mail
exclude group: 'javax.mail', module: 'mail'
+ resolutionStrategy {
+ force 'net.java.dev.jna:jna:5.14.0'
+ }
}
}
@@ -281,58 +289,63 @@ subprojects {
dependencies {
// bundled plugin list -- keep it alphabetically ordered
dependency 'com.sonarsource.abap:sonar-abap-plugin:3.15.1.6010'
+ dependency 'com.sonarsource.armor:sonar-jasmin-plugin:1.3.0.6541'
dependency 'com.sonarsource.cobol:sonar-cobol-plugin:5.8.1.8428'
- dependency 'com.sonarsource.cpp:sonar-cfamily-dependencies-plugin:6.65.0.81949'
- dependency 'com.sonarsource.cpp:sonar-cfamily-plugin:6.65.0.81949'
- dependency 'com.sonarsource.dart:sonar-dart-plugin:1.1.0.2133'
- dependency 'com.sonarsource.dbd:sonar-dbd-plugin:1.36.1.13250'
- dependency 'com.sonarsource.dbd:sonar-dbd-java-frontend-plugin:1.36.1.13250'
- dependency 'com.sonarsource.dbd:sonar-dbd-python-frontend-plugin:1.36.1.13250'
- dependency 'com.sonarsource.dotnet:sonar-csharp-enterprise-plugin:10.7.0.110445'
- dependency 'com.sonarsource.dotnet:sonar-vbnet-enterprise-plugin:10.7.0.110445'
- dependency 'com.sonarsource.go:sonar-go-enterprise-plugin:1.21.1.1670'
+ dependency 'com.sonarsource.cpp:sonar-cfamily-dependencies-plugin:6.70.0.87073'
+ dependency 'com.sonarsource.cpp:sonar-cfamily-plugin:6.70.0.87073'
+ dependency 'com.sonarsource.dart:sonar-dart-plugin:1.3.0.2614'
+ dependency 'com.sonarsource.dbd:sonar-dbd-plugin:2.2.0.16530'
+ dependency 'com.sonarsource.dbd:sonar-dbd-java-frontend-plugin:2.2.0.16530'
+ dependency 'com.sonarsource.dbd:sonar-dbd-python-frontend-plugin:2.2.0.16530'
+ dependency 'com.sonarsource.dotnet:sonar-csharp-enterprise-plugin:10.15.0.120848'
+ dependency 'com.sonarsource.dotnet:sonar-vbnet-enterprise-plugin:10.15.0.120848'
+ dependency 'com.sonarsource.go:sonar-go-enterprise-plugin:1.26.0.3421'
dependency 'com.sonarsource.pli:sonar-pli-plugin:1.16.0.5325'
- dependency 'com.sonarsource.plsql:sonar-plsql-plugin:3.15.0.7123'
- dependency 'com.sonarsource.plugins.vb:sonar-vb-plugin:2.14.0.5475'
+ dependency 'com.sonarsource.plsql:sonar-plsql-plugin:3.17.0.7448'
+ dependency 'com.sonarsource.plugins.vb:sonar-vb-plugin:2.14.1.5552'
dependency 'com.sonarsource.rpg:sonar-rpg-plugin:3.10.0.5337'
- dependency 'com.sonarsource.security:sonar-security-csharp-frontend-plugin:11.1.0.35630'
- dependency 'com.sonarsource.security:sonar-security-java-frontend-plugin:11.1.0.35630'
- dependency 'com.sonarsource.security:sonar-security-php-frontend-plugin:11.1.0.35630'
- dependency 'com.sonarsource.security:sonar-security-plugin:11.1.0.35630'
- dependency 'com.sonarsource.security:sonar-security-python-frontend-plugin:11.1.0.35630'
- dependency 'com.sonarsource.security:sonar-security-js-frontend-plugin:11.1.0.35630'
- dependency 'com.sonarsource.slang:sonar-apex-plugin:1.18.0.198'
- dependency 'org.sonarsource.slang:sonar-ruby-plugin:1.18.0.234'
- dependency 'org.sonarsource.slang:sonar-scala-plugin:1.18.0.266'
- dependency 'com.sonarsource.swift:sonar-swift-plugin:4.13.1.8101'
- dependency 'com.sonarsource.tsql:sonar-tsql-plugin:1.14.0.7614'
- dependency 'org.sonarsource.dotnet:sonar-csharp-plugin:10.7.0.110445'
- dependency 'org.sonarsource.dotnet:sonar-vbnet-plugin:10.7.0.110445'
+ dependency 'com.sonarsource.security:sonar-security-csharp-frontend-plugin:11.6.0.39346'
+ dependency 'com.sonarsource.security:sonar-security-go-frontend-plugin:11.6.0.39346'
+ dependency 'com.sonarsource.security:sonar-security-java-frontend-plugin:11.6.0.39346'
+ dependency 'com.sonarsource.security:sonar-security-js-frontend-plugin:11.6.0.39346'
+ dependency 'com.sonarsource.security:sonar-security-kotlin-frontend-plugin:11.6.0.39346'
+ dependency 'com.sonarsource.security:sonar-security-php-frontend-plugin:11.6.0.39346'
+ dependency 'com.sonarsource.security:sonar-security-plugin:11.6.0.39346'
+ dependency 'com.sonarsource.security:sonar-security-python-frontend-plugin:11.6.0.39346'
+ dependency 'com.sonarsource.security:sonar-security-vbnet-frontend-plugin:11.6.0.39346'
+ dependency 'com.sonarsource.slang:sonar-apex-plugin:1.20.0.552'
+ dependency 'org.sonarsource.slang:sonar-ruby-plugin:1.19.0.471'
+ dependency 'org.sonarsource.slang:sonar-scala-plugin:1.19.0.484'
+ dependency 'com.sonarsource.swift:sonar-swift-plugin:4.14.0.8764'
+ dependency 'com.sonarsource.tsql:sonar-tsql-plugin:1.15.0.7898'
+ dependency 'org.sonarsource.dotnet:sonar-csharp-plugin:10.15.0.120848'
+ dependency 'org.sonarsource.dotnet:sonar-vbnet-plugin:10.15.0.120848'
dependency 'org.sonarsource.flex:sonar-flex-plugin:2.14.0.5032'
- dependency 'org.sonarsource.go:sonar-go-plugin:1.21.1.1670'
+ dependency 'org.sonarsource.go:sonar-go-plugin:1.26.0.3421'
dependency 'org.sonarsource.html:sonar-html-plugin:3.19.0.5695'
dependency 'org.sonarsource.jacoco:sonar-jacoco-plugin:1.3.0.1538'
- dependency 'org.sonarsource.java:sonar-java-plugin:8.11.0.38440'
- dependency 'org.sonarsource.java:sonar-java-symbolic-execution-plugin:8.11.0.38440'
- dependency 'org.sonarsource.javascript:sonar-javascript-plugin:10.21.1.30825'
- dependency 'org.sonarsource.php:sonar-php-plugin:3.45.0.12991'
+ dependency 'org.sonarsource.java:sonar-java-plugin:8.18.0.40025'
+ dependency 'org.sonarsource.java:sonar-java-symbolic-execution-plugin:8.16.0.131'
+ dependency 'org.sonarsource.javascript:sonar-javascript-plugin:10.25.0.33900'
+ dependency 'org.sonarsource.php:sonar-php-plugin:3.46.0.13151'
dependency 'org.sonarsource.plugins.cayc:sonar-cayc-plugin:2.4.0.2018'
- dependency 'org.sonarsource.python:sonar-python-plugin:5.2.0.20808'
- dependency 'com.sonarsource.python:sonar-python-enterprise-plugin:5.2.0.20808'
- dependency 'org.sonarsource.kotlin:sonar-kotlin-plugin:3.0.1.6889'
+ dependency 'org.sonarsource.python:sonar-python-plugin:5.7.0.24163'
+ dependency 'org.sonarsource.rust:sonar-rust-plugin:1.0.3.786'
+ dependency 'com.sonarsource.python:sonar-python-enterprise-plugin:5.7.0.24163'
+ dependency 'org.sonarsource.kotlin:sonar-kotlin-plugin:3.3.0.7402'
dependency "org.sonarsource.api.plugin:sonar-plugin-api:$pluginApiVersion"
dependency "org.sonarsource.api.plugin:sonar-plugin-api-test-fixtures:$pluginApiVersion"
- dependency 'org.sonarsource.xml:sonar-xml-plugin:2.12.0.5749'
- dependency 'org.sonarsource.iac:sonar-iac-plugin:1.44.0.14670'
- dependency 'com.sonarsource.iac:sonar-iac-enterprise-plugin:1.44.0.14670'
- dependency 'org.sonarsource.text:sonar-text-plugin:2.21.1.5779'
- dependency 'com.sonarsource.text:sonar-text-developer-plugin:2.21.1.5779'
- dependency 'com.sonarsource.text:sonar-text-enterprise-plugin:2.21.1.5779'
+ dependency 'org.sonarsource.xml:sonar-xml-plugin:2.13.0.5938'
+ dependency 'org.sonarsource.iac:sonar-iac-plugin:1.48.0.15768'
+ dependency 'com.sonarsource.iac:sonar-iac-enterprise-plugin:1.48.0.15768'
+ dependency 'org.sonarsource.text:sonar-text-plugin:2.26.0.7517'
+ dependency 'com.sonarsource.text:sonar-text-developer-plugin:2.26.0.7517'
+ dependency 'com.sonarsource.text:sonar-text-enterprise-plugin:2.26.0.7517'
dependency 'com.sonarsource.jcl:sonar-jcl-plugin:1.4.1.1493'
- dependency 'com.sonarsource.architecture:sonar-architecture-plugin:1.9.0.4841'
- dependency 'com.sonarsource.architecture:sonar-architecture-java-frontend-plugin:1.9.0.4841'
- dependency 'com.sonarsource.architecture:sonar-architecture-javascript-frontend-plugin:1.9.0.4841'
-
+ dependency 'com.sonarsource.architecture:sonar-architecture-plugin:2.0.0.6303'
+ dependency 'com.sonarsource.architecture:sonar-architecture-java-frontend-plugin:2.0.0.6303'
+ dependency 'com.sonarsource.architecture:sonar-architecture-javascript-frontend-plugin:2.0.0.6303'
+
// Webapp
dependency "org.sonarsource.sonarqube:webapp-assets:$webappVersion"
@@ -345,19 +358,16 @@ subprojects {
// https://mvnrepository.com/artifact/ch.qos.logback.access/common
dependency('ch.qos.logback.access:common:2.0.3')
dependency('ch.qos.logback.access:logback-access-tomcat:2.0.6')
- dependency('commons-beanutils:commons-beanutils:1.10.1') {
- exclude 'commons-logging:commons-logging'
- }
dependency 'commons-codec:commons-codec:1.18.0'
dependency 'commons-dbutils:commons-dbutils:1.8.1'
- dependency 'commons-io:commons-io:2.18.0'
- imports { mavenBom 'com.fasterxml.jackson:jackson-bom:2.18.3' }
+ dependency 'commons-io:commons-io:2.19.0'
+ imports { mavenBom 'com.fasterxml.jackson:jackson-bom:2.19.0' }
dependency 'com.eclipsesource.minimal-json:minimal-json:0.9.5'
dependencySet(group: 'com.github.scribejava', version: '8.3.3') {
entry 'scribejava-apis'
entry 'scribejava-core'
}
- dependency('com.github.erosb:json-sKema:0.21.0') {
+ dependency('com.github.erosb:json-sKema:0.23.0') {
// this version of json-sKema does not make use of commons-collections, so we can exclude it safely
exclude 'commons-collections:commons-collections'
}
@@ -373,8 +383,8 @@ subprojects {
dependency 'io.prometheus:simpleclient_common:0.16.0'
dependency 'io.prometheus:simpleclient_servlet:0.16.0'
dependency 'com.github.spotbugs:spotbugs-annotations:4.9.3'
- dependency 'com.google.code.gson:gson:2.12.1'
- dependency('com.google.guava:guava:33.4.5-jre') {
+ dependency 'com.google.code.gson:gson:2.13.1'
+ dependency('com.google.guava:guava:33.4.8-jre') {
exclude 'com.google.errorprone:error_prone_annotations'
exclude 'com.google.guava:listenablefuture'
exclude 'com.google.j2objc:j2objc-annotations'
@@ -383,17 +393,17 @@ subprojects {
}
dependency "com.google.protobuf:protobuf-java:${protobufVersion}"
dependency 'com.h2database:h2:2.3.232'
- dependencySet(group: 'com.hazelcast', version: '5.4.0') {
+ dependencySet(group: 'com.hazelcast', version: '5.5.0') {
entry 'hazelcast'
}
// Documentation must be updated if mssql-jdbc is updated: https://github.com/SonarSource/sonarqube/commit/03e4773ebf6cba854cdcf57a600095f65f4f53e7
dependency('com.microsoft.sqlserver:mssql-jdbc:12.10.0.jre11') {
exclude 'com.fasterxml.jackson.core:jackson-databind'
}
- dependency 'com.microsoft.azure:msal4j:1.19.1'
- dependency 'com.oracle.database.jdbc:ojdbc11:23.7.0.25.01'
- dependency 'com.datadoghq:dd-java-agent:1.30.1'
- dependency 'org.aspectj:aspectjtools:1.9.23'
+ dependency 'com.microsoft.azure:msal4j:1.22.0'
+ dependency 'com.oracle.database.jdbc:ojdbc11:23.8.0.25.04'
+ dependency 'com.datadoghq:dd-java-agent:1.49.0'
+ dependency 'org.aspectj:aspectjtools:1.9.24'
dependencySet(group: 'com.squareup.okhttp3', version: '4.12.0') {
entry 'okhttp'
entry 'mockwebserver'
@@ -401,7 +411,7 @@ subprojects {
entry 'logging-interceptor'
}
dependency 'commons-logging:commons-logging:1.3.5'
- dependency 'org.json:json:20250107'
+ dependency 'org.json:json:20250517'
// To be removed after migration to JUnit5 is finished
dependency 'com.tngtech.java:junit-dataprovider:1.13.1'
dependency 'com.tngtech.junit.dataprovider:junit-jupiter-params-dataprovider:2.10'
@@ -411,28 +421,30 @@ subprojects {
entry 'jjwt-jackson'
}
dependency 'com.auth0:java-jwt:4.5.0'
- dependency 'io.netty:netty-all:4.1.119.Final'
+ dependency 'io.netty:netty-all:4.2.1.Final'
dependency 'jakarta.mail:jakarta.mail-api:2.1.3'
dependency 'jakarta.annotation:jakarta.annotation-api:3.0.0'
dependency 'jakarta.inject:jakarta.inject-api:2.0.1'
dependency 'jakarta.servlet:jakarta.servlet-api:6.1.0'
dependency 'junit:junit:4.13.2'
- dependency 'org.xmlunit:xmlunit-core:2.10.0'
- dependency 'org.xmlunit:xmlunit-matchers:2.10.0'
+ dependencySet(group: 'org.xmlunit', version: '2.10.2') {
+ entry 'xmlunit-core'
+ entry 'xmlunit-matchers'
+ }
dependency 'org.lz4:lz4-java:1.8.0'
dependency 'org.littleshoot:littleproxy:1.1.2'
dependency 'net.sf.trove4j:core:3.1.0'
dependency 'org.awaitility:awaitility:4.3.0'
- dependency 'org.apache.commons:commons-collections4:4.4'
+ dependency 'org.apache.commons:commons-collections4:4.5.0'
dependency 'org.apache.commons:commons-csv:1.14.0'
dependency 'org.apache.commons:commons-lang3:3.17.0'
dependency 'org.apache.commons:commons-email2-jakarta:2.0.0-M1'
- dependency 'org.apache.commons:commons-exec:1.4.0'
- dependency 'org.apache.commons:commons-text:1.13.0'
+ dependency 'org.apache.commons:commons-exec:1.5.0'
+ dependency 'org.apache.commons:commons-text:1.13.1'
dependency 'org.apache.mina:mina-core:2.2.4'
dependency 'org.apache.kerby:kerb-simplekdc:2.1.0'
dependency 'org.apache.kerby:ldap-backend:2.1.0'
- dependency 'com.zaxxer:HikariCP:6.2.1'
+ dependency 'com.zaxxer:HikariCP:6.3.0'
dependency('org.apache.directory.server:apacheds-server-integ:2.0.0.AM27') {
exclude 'log4j:log4j'
}
@@ -442,7 +454,7 @@ subprojects {
entry 'log4j-api'
entry 'log4j-to-slf4j'
}
- dependencySet(group: 'org.apache.tomcat.embed', version: '10.1.39') {
+ dependencySet(group: 'org.apache.tomcat.embed', version: '11.0.8') {
entry 'tomcat-embed-core'
entry('tomcat-embed-jasper') {
exclude 'org.eclipse.jdt.core.compiler:ecj'
@@ -463,38 +475,29 @@ subprojects {
}
dependency 'org.cyclonedx:cyclonedx-core-java:10.2.1'
dependency 'com.fasterxml.staxmate:staxmate:2.4.1'
- dependencySet(group: 'org.eclipse.jetty', version: '11.0.25') {
- entry 'jetty-proxy'
- entry 'jetty-server'
- entry 'jetty-servlet'
- }
dependency("org.elasticsearch.client:elasticsearch-rest-high-level-client:${elasticSearchClientVersion}") {
exclude 'org.apache.logging.log4j:log4j-core'
}
dependency "org.elasticsearch.plugin:transport-netty4-client:${elasticSearchClientVersion}"
dependency 'org.elasticsearch:mocksocket:1.2'
- dependency 'org.eclipse.jgit:org.eclipse.jgit:7.2.0.202503040940-r'
+ dependency 'org.eclipse.jgit:org.eclipse.jgit:7.3.0.202506031305-r'
dependency "org.codelibs.elasticsearch.module:analysis-common:7.17.22"
dependency "org.codelibs.elasticsearch.module:reindex:7.17.22"
- dependency('org.tmatesoft.svnkit:svnkit:1.10.11') {
- exclude 'net.i2p.crypto:eddsa'
- }
+ dependency('org.tmatesoft.svnkit:svnkit:1.10.11')
dependency 'org.hamcrest:hamcrest:3.0'
- dependency 'org.jsoup:jsoup:1.19.1'
+ dependency 'org.jsoup:jsoup:1.20.1'
// JUnit 5
imports { mavenBom 'org.junit:junit-bom:5.11.4' }
dependency 'org.junit-pioneer:junit-pioneer:2.3.0'
- dependencySet(group: 'org.mockito', version: '5.16.1') {
+ dependencySet(group: 'org.mockito', version: '5.18.0') {
entry 'mockito-core'
entry('mockito-junit-jupiter') {
exclude 'org.junit.jupiter:junit-jupiter-api'
}
}
- dependencySet(group: 'org.spdx', version: '2.0.0-RC2') {
- entry 'java-spdx-library'
- entry 'spdx-jackson-store'
- }
- dependencySet(group: 'org.springframework', version: '6.2.5') {
+ dependency 'org.spdx:java-spdx-library:2.0.0'
+ dependency 'org.spdx:spdx-jackson-store:2.0.2'
+ dependencySet(group: 'org.springframework', version: '6.2.8') {
entry 'spring-test'
entry('spring-context') {
exclude 'commons-logging:commons-logging'
@@ -511,17 +514,21 @@ subprojects {
entry 'log4j-over-slf4j'
entry 'slf4j-api'
}
- dependency 'org.postgresql:postgresql:42.7.5'
+ dependency 'org.postgresql:postgresql:42.7.7'
dependency 'org.reflections:reflections:0.10.2'
dependency 'org.simpleframework:simple:5.1.6'
- dependency 'org.sonarsource.git.blame:git-files-blame:1.1.0.1835'
- dependency('org.sonarsource.orchestrator:sonar-orchestrator-junit4:5.4.0.2489') {
+ dependency 'org.sonarsource.git.blame:git-files-blame:2.0.0.2053'
+ dependency('org.sonarsource.orchestrator:sonar-orchestrator-junit4:5.6.2.2625') {
exclude 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml'
}
- dependency('org.sonarsource.orchestrator:sonar-orchestrator-junit5:5.4.0.2489') {
+ dependency('org.sonarsource.orchestrator:sonar-orchestrator-junit5:5.6.2.2625') {
exclude 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml'
}
- dependency 'com.sonarsource.pdfreport:security-report-pdf-generation:2.0.0.184'
+ dependency "com.sonarsource.pdfreport:portfolio-report-pdf-generation:${pdfreportVersion}"
+ dependency "com.sonarsource.pdfreport:regulatory-report-pdf-generation:${pdfreportVersion}"
+ dependency "com.sonarsource.pdfreport:security-report-pdf-generation:${pdfreportVersion}"
+ dependency "com.sonarsource.pdfreport:pdf-generator-utils:${pdfreportVersion}"
+ dependency 'com.sonarsource.fixsuggestions:ai-suggestions-shared:1.0.0.1312'
dependency 'org.sonarsource.update-center:sonar-update-center-common:1.35.0.2835'
dependency 'org.sonarsource.classloader:sonar-classloader:1.1.0.1059'
dependency 'org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.6'
@@ -531,9 +538,9 @@ subprojects {
entry 'greenmail-junit5'
}
dependency 'org.yaml:snakeyaml:2.4'
- dependency 'org.hibernate.validator:hibernate-validator:8.0.2.Final'
+ dependency 'org.hibernate.validator:hibernate-validator:9.0.0.Final'
dependency 'org.kohsuke:github-api:1.327'
- dependency 'org.wiremock:wiremock-standalone:3.12.1'
+ dependency 'org.wiremock:wiremock-standalone:3.13.0'
dependency 'org.skyscreamer:jsonassert:1.5.3'
// to be used only in sonar-ws-generator
dependency("org.apache.velocity:velocity:1.7") {
@@ -555,7 +562,7 @@ subprojects {
develocity.testRetry {
maxRetries = 3
maxFailures = 30
- failOnPassedAfterRetry = true
+ failOnPassedAfterRetry = System.getenv("CIRRUS_PR") != null
}
}
}
diff --git a/gradle.properties b/gradle.properties
index 850e30b21a7..25600d23909 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,11 +1,12 @@
group=org.sonarsource.sonarqube
-version=25.4
+version=25.9
# End Of Life date for the version. MMF-3763. format is yyyy-MM-dd
# 6 months from the release date for non LTA versions
-# 30 months from the release date for LTA versions
+# 18 months from the release date for LTA versions
# No change required for patch versions
-versionEOL=2025-07-01
-pluginApiVersion=11.3.0.2824
+versionEOL=2026-04-01
+pdfreportVersion=2.0.0.316
+pluginApiVersion=13.0.0.3026
description=Open source platform for continuous inspection of code quality
projectTitle=SonarQube
org.gradle.jvmargs=-Xmx2048m
@@ -15,4 +16,4 @@ elasticSearchServerVersion=8.16.3
projectType=application
artifactoryUrl=https://repox.jfrog.io/repox
jre_release_name=jdk-17.0.13+11
-webappVersion=2025.3.0.14738
+webappVersion=2025.5.0.23948
diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java
index b4aad85bdb0..652d17d78bb 100644
--- a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java
+++ b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java
@@ -33,6 +33,7 @@ import org.sonar.xoo.extensions.XooIssueFilter;
import org.sonar.xoo.extensions.XooPostJob;
import org.sonar.xoo.extensions.XooProjectBuilder;
import org.sonar.xoo.global.DeprecatedGlobalSensor;
+import org.sonar.xoo.global.ErrorThrowingSensor;
import org.sonar.xoo.global.GlobalProjectSensor;
import org.sonar.xoo.lang.CpdTokenizerSensor;
import org.sonar.xoo.lang.LineMeasureSensor;
@@ -185,6 +186,7 @@ public class XooPlugin implements Plugin {
SensorMetrics.class,
DeprecatedGlobalSensor.class,
GlobalProjectSensor.class,
+ ErrorThrowingSensor.class,
HotspotWithoutContextSensor.class,
HotspotWithContextsSensor.class,
diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/architecture/ArchitectureSensor.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/architecture/ArchitectureSensor.java
index e3e75a9932f..237f8fa71ba 100644
--- a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/architecture/ArchitectureSensor.java
+++ b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/architecture/ArchitectureSensor.java
@@ -36,7 +36,7 @@ public class ArchitectureSensor implements ProjectSensor {
@Override
public void execute(SensorContext context) {
- final String mimeType = "application/file_graph+json;version=1.0";
+ final String mimeType = "application/graph+json;version=2.0.0";
long count = StreamSupport.stream(
context.fileSystem().inputFiles(
@@ -44,16 +44,23 @@ public class ArchitectureSensor implements ProjectSensor {
.count();
context.addAnalysisData(
- "architecture.file_graph.java",
+ "architecture.graph." + Xoo.KEY + ".file_graph",
mimeType,
- new ByteArrayInputStream(("{graph:\"data\", \"classCount\":" + count + "}")
+ new ByteArrayInputStream(("{\"graph\":\"files\", \"fileCount\":" + count + "}")
.getBytes(StandardCharsets.UTF_8))
);
context.addAnalysisData(
- "architecture.file_graph." + Xoo.KEY,
+ "architecture.graph." + Xoo.KEY + ".file_graph.module_persp",
mimeType,
- new ByteArrayInputStream(("{graph:\"data\", \"fileCount\":" + count + "}")
+ new ByteArrayInputStream(("{\"graph\":\"modules\", \"fileCount\":" + count + "}")
+ .getBytes(StandardCharsets.UTF_8))
+ );
+
+ context.addAnalysisData(
+ "architecture.graph." + Xoo.KEY + ".namespace",
+ mimeType,
+ new ByteArrayInputStream(("{\"graph\":\"namespace\", \"fileCount\":" + count + "}")
.getBytes(StandardCharsets.UTF_8))
);
}
diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/global/ErrorThrowingSensor.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/global/ErrorThrowingSensor.java
new file mode 100644
index 00000000000..dde8dd69453
--- /dev/null
+++ b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/global/ErrorThrowingSensor.java
@@ -0,0 +1,71 @@
+/*
+ * 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.xoo.global;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.sensor.Sensor;
+import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.batch.sensor.SensorDescriptor;
+
+/**
+ * Sensor that throws a {@link java.lang.Error} during execution.
+ */
+public class ErrorThrowingSensor implements Sensor {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ErrorThrowingSensor.class);
+
+ public static final String ENABLE_PROP = "sonar.scanner.errorSensor";
+
+ @Override
+ public void describe(SensorDescriptor descriptor) {
+ descriptor
+ .name("Error Throwing Sensor")
+ .onlyWhenConfiguration(c -> c.hasKey(ENABLE_PROP));
+ }
+
+ @Override
+ public void execute(SensorContext context) {
+ LOG.info("Running Error Throwing sensor");
+ runNonDaemonThread();
+ throw new XooError("This is thrown by the ErrorThrowing Sensor, it's its job to throw it!");
+ }
+
+ private static void runNonDaemonThread() {
+ Thread nonDaemonThread = new Thread(() -> {
+ while (true) {
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+ });
+ LOG.info("Starting non-daemon Thread");
+ nonDaemonThread.start();
+ }
+
+ static class XooError extends Error {
+ public XooError(String message) {
+ super(message);
+ }
+ }
+}
diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneIssuePerFileSensor.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneIssuePerFileSensor.java
index 135a1a0a4ab..ef1955d37d9 100644
--- a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneIssuePerFileSensor.java
+++ b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneIssuePerFileSensor.java
@@ -23,6 +23,7 @@ import org.sonar.api.batch.fs.FileSystem;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.rule.ActiveRules;
import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.batch.sensor.SensorDescriptor;
import org.sonar.api.batch.sensor.issue.NewIssue;
import org.sonar.api.config.Configuration;
import org.sonar.api.rule.RuleKey;
@@ -32,6 +33,7 @@ public class OneIssuePerFileSensor extends AbstractXooRuleSensor {
public static final String RULE_KEY = "OneIssuePerFile";
private static final String EFFORT_TO_FIX_PROPERTY = "sonar.oneIssuePerFile.effortToFix";
+ private static final String ENABLE_HIDDEN_FILE_PROCESSING = "sonar.oneIssuePerFile.enableHiddenFileProcessing";
private final Configuration settings;
@@ -41,11 +43,20 @@ public class OneIssuePerFileSensor extends AbstractXooRuleSensor {
}
@Override
+ public void describe(SensorDescriptor descriptor) {
+ super.describe(descriptor);
+ if (settings.getBoolean(ENABLE_HIDDEN_FILE_PROCESSING).orElse(false)) {
+ descriptor.processesHiddenFiles();
+ }
+ }
+
+ @Override
protected String getRuleKey() {
return RULE_KEY;
}
- @Override protected void processFile(InputFile inputFile, SensorContext context, RuleKey ruleKey, String languageKey) {
+ @Override
+ protected void processFile(InputFile inputFile, SensorContext context, RuleKey ruleKey, String languageKey) {
NewIssue newIssue = context.newIssue()
.forRule(ruleKey)
.gap(settings.getDouble(EFFORT_TO_FIX_PROPERTY).orElse(0.0));
diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/XooRulesDefinition.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/XooRulesDefinition.java
index db8d42d0d28..6a414972d5b 100644
--- a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/XooRulesDefinition.java
+++ b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/XooRulesDefinition.java
@@ -48,6 +48,7 @@ import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSe
import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.INTRODUCTION_SECTION_KEY;
import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.RESOURCES_SECTION_KEY;
import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ROOT_CAUSE_SECTION_KEY;
+import static org.sonar.api.server.rule.RulesDefinition.OwaspMobileTop10Version.Y2024;
import static org.sonar.api.server.rule.RulesDefinition.OwaspTop10Version.Y2017;
import static org.sonar.api.server.rule.RulesDefinition.OwaspTop10Version.Y2021;
@@ -293,11 +294,13 @@ public class XooRulesDefinition implements RulesDefinition {
hotspot
.addOwaspTop10(OwaspTop10.A1, OwaspTop10.A3)
.addOwaspTop10(Y2021, OwaspTop10.A3, OwaspTop10.A2)
+ .addOwaspMobileTop10(Y2024, OwaspMobileTop10.M4, OwaspMobileTop10.M8)
.addCwe(1, 89, 123, 863);
oneVulnerabilityIssuePerProject
.addOwaspTop10(Y2017, OwaspTop10.A9, OwaspTop10.A10)
.addOwaspTop10(Y2021, OwaspTop10.A6, OwaspTop10.A9)
+ .addOwaspMobileTop10(Y2024, OwaspMobileTop10.M3, OwaspMobileTop10.M5)
.addCwe(89, 250, 311, 546, 564, 943);
}
diff --git a/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/architecture/ArchitectureSensorTest.java b/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/architecture/ArchitectureSensorTest.java
index d1bac6b14b6..d43ac0ff165 100644
--- a/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/architecture/ArchitectureSensorTest.java
+++ b/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/architecture/ArchitectureSensorTest.java
@@ -76,7 +76,7 @@ public class ArchitectureSensorTest {
// then
ArgumentCaptor<InputStream> inputStreamCaptor = ArgumentCaptor.forClass(InputStream.class);
- verify(context).addAnalysisData(eq("architecture.file_graph.xoo"), contains("application/file_graph+json"), inputStreamCaptor.capture());
+ verify(context).addAnalysisData(eq("architecture.graph.xoo.file_graph"), contains("application/graph+json"), inputStreamCaptor.capture());
try {
String capturedData = new String(inputStreamCaptor.getValue().readAllBytes(), StandardCharsets.UTF_8);
assertThat(capturedData).contains("\"fileCount\":" + nbFileSensor);
diff --git a/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/rule/XooRulesDefinitionTest.java b/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/rule/XooRulesDefinitionTest.java
index d9a418c69ac..a801fb85ddd 100644
--- a/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/rule/XooRulesDefinitionTest.java
+++ b/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/rule/XooRulesDefinitionTest.java
@@ -70,6 +70,7 @@ public class XooRulesDefinitionTest {
assertThat(rule.securityStandards())
.isNotEmpty()
.containsExactlyInAnyOrder("cwe:1", "cwe:89", "cwe:123", "cwe:863", "owaspTop10:a1", "owaspTop10:a3",
+ "owaspMobileTop10-2024:m4", "owaspMobileTop10-2024:m8",
"owaspTop10-2021:a3", "owaspTop10-2021:a2", "owaspAsvs-4.0:2.8.7", "owaspAsvs-4.0:3.1.1",
"owaspAsvs-4.0:4.2.2", "pciDss-3.2:4.2", "pciDss-3.2:4.2b", "pciDss-3.2:6.5.1",
"pciDss-3.2:6.5a.1b", "pciDss-4.0:4.1", "pciDss-4.0:4.2c", "pciDss-4.0:6.5.1", "pciDss-4.0:6.5a.1",
@@ -98,7 +99,7 @@ public class XooRulesDefinitionTest {
assertThat(rule.securityStandards())
.isNotEmpty()
.containsExactlyInAnyOrder("cwe:89", "cwe:250", "cwe:311", "cwe:546", "cwe:564", "cwe:943", "owaspTop10-2021:a6", "owaspTop10-2021:a9",
- "owaspTop10:a10", "owaspTop10:a9",
+ "owaspTop10:a10", "owaspTop10:a9", "owaspMobileTop10-2024:m3", "owaspMobileTop10-2024:m5",
"owaspAsvs-4.0:11.1.2", "owaspAsvs-4.0:14.5.1", "owaspAsvs-4.0:14.5.4",
"pciDss-3.2:10.1a.2c", "pciDss-3.2:10.2", "pciDss-4.0:10.1", "pciDss-4.0:10.1a.2b",
"stig-ASD_V5R3:V-222596", "stig-ASD_V5R3:V-222608", "stig-ASD_V5R3:V-222653");
diff --git a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/gitlab/GitlabApplicationClientTest.java b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/gitlab/GitlabApplicationClientTest.java
index 4ca698cd35d..43ef628cfdd 100644
--- a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/gitlab/GitlabApplicationClientTest.java
+++ b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/gitlab/GitlabApplicationClientTest.java
@@ -484,7 +484,8 @@ public class GitlabApplicationClientTest {
.hasMessage("Could not validate GitLab read permission. Got an unexpected answer.");
assertThat(logTester.logs(Level.INFO).get(0))
.contains("Gitlab API call to [" + server.url("/projects") + "] " +
- "failed with error message : [Failed to connect to " + server.getHostName());
+ "failed with error message : [Failed to connect to ")
+ .contains(server.getHostName());
}
@Test
@@ -496,7 +497,8 @@ public class GitlabApplicationClientTest {
.hasMessage("Could not validate GitLab token. Got an unexpected answer.");
assertThat(logTester.logs(Level.INFO).get(0))
.contains("Gitlab API call to [" + server.url("user") + "] " +
- "failed with error message : [Failed to connect to " + server.getHostName());
+ "failed with error message : [Failed to connect to ")
+ .contains(server.getHostName());
}
@Test
@@ -508,7 +510,8 @@ public class GitlabApplicationClientTest {
.hasMessage("Could not validate GitLab write permission. Got an unexpected answer.");
assertThat(logTester.logs(Level.INFO).get(0))
.contains("Gitlab API call to [" + server.url("/markdown") + "] " +
- "failed with error message : [Failed to connect to " + server.getHostName());
+ "failed with error message : [Failed to connect to ")
+ .contains(server.getHostName());
}
@Test
@@ -520,7 +523,8 @@ public class GitlabApplicationClientTest {
.hasMessageContaining("Failed to connect to");
assertThat(logTester.logs(Level.INFO).get(0))
.contains("Gitlab API call to [" + server.url("/projects/0") + "] " +
- "failed with error message : [Failed to connect to " + server.getHostName());
+ "failed with error message : [Failed to connect to ")
+ .contains( server.getHostName());
}
@Test
@@ -529,7 +533,8 @@ public class GitlabApplicationClientTest {
assertThatThrownBy(() -> underTest.getBranches(gitlabUrl, "token", 0L))
.isInstanceOf(IllegalStateException.class)
- .hasMessageContaining("Failed to connect to " + server.getHostName());
+ .hasMessageContaining("Failed to connect to ")
+ .hasMessageContaining(server.getHostName());
assertThat(logTester.logs(Level.INFO).get(0))
.contains("Gitlab API call to [" + server.url("/projects/0/repository/branches") + "] " +
"failed with error message : [Failed to connect to " + server.getHostName());
@@ -546,7 +551,8 @@ public class GitlabApplicationClientTest {
.contains(
"Gitlab API call to [" + server.url("/projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=&page=1&per_page=1")
+ "] " +
- "failed with error message : [Failed to connect to " + server.getHostName());
+ "failed with error message : [Failed to connect to ")
+ .contains( server.getHostName());
}
@Test
diff --git a/server/sonar-ce-common/src/main/java/org/sonar/ce/common/scanner/ScannerReportReader.java b/server/sonar-ce-common/src/main/java/org/sonar/ce/common/scanner/ScannerReportReader.java
index 459ba57009a..925d57bb540 100644
--- a/server/sonar-ce-common/src/main/java/org/sonar/ce/common/scanner/ScannerReportReader.java
+++ b/server/sonar-ce-common/src/main/java/org/sonar/ce/common/scanner/ScannerReportReader.java
@@ -74,7 +74,7 @@ public interface ScannerReportReader {
CloseableIterator<ScannerReport.TelemetryEntry> readTelemetryEntries();
- File readDependencyFilesZip();
+ File readDependencyFilesArchive();
CloseableIterator<ScannerReport.AnalysisData> readAnalysisData();
}
diff --git a/server/sonar-ce-common/src/testFixtures/java/org/sonar/ce/common/scanner/ScannerReportReaderRule.java b/server/sonar-ce-common/src/testFixtures/java/org/sonar/ce/common/scanner/ScannerReportReaderRule.java
index 29e718bfe96..5887533d74e 100644
--- a/server/sonar-ce-common/src/testFixtures/java/org/sonar/ce/common/scanner/ScannerReportReaderRule.java
+++ b/server/sonar-ce-common/src/testFixtures/java/org/sonar/ce/common/scanner/ScannerReportReaderRule.java
@@ -64,7 +64,7 @@ public class ScannerReportReaderRule implements TestRule, ScannerReportReader, A
private List<ScannerReport.AnalysisWarning> analysisWarnings = Collections.emptyList();
private byte[] analysisCache;
private List<ScannerReport.TelemetryEntry> telemetryEntries = new ArrayList<>();
- private File dependencyFilesZip;
+ private File dependencyFilesArchive;
private List<ScannerReport.AnalysisData> analysisData = new ArrayList<>();
@Override
@@ -347,15 +347,15 @@ public class ScannerReportReaderRule implements TestRule, ScannerReportReader, A
}
@Override
- public File readDependencyFilesZip() {
- if (dependencyFilesZip == null) {
+ public File readDependencyFilesArchive() {
+ if (dependencyFilesArchive == null) {
return null;
}
- return this.dependencyFilesZip;
+ return this.dependencyFilesArchive;
}
- public ScannerReportReaderRule putDependencyFilesZip(File dependencyFilesZip) {
- this.dependencyFilesZip = dependencyFilesZip;
+ public ScannerReportReaderRule putDependencyFilesArchive(File dependencyFilesArchive) {
+ this.dependencyFilesArchive = dependencyFilesArchive;
return this;
}
}
diff --git a/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryImplIT.java b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryImplIT.java
index 1c44545518d..94fb1d8766a 100644
--- a/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryImplIT.java
+++ b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryImplIT.java
@@ -88,6 +88,45 @@ public class MetricRepositoryImplIT {
}
@Test
+ public void getOptionalByKey_throws_NPE_if_arg_is_null() {
+ assertThatThrownBy(() -> underTest.getOptionalByKey(null))
+ .isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ public void getOptionalByKey_throws_ISE_if_start_has_not_been_called() {
+ assertThatThrownBy(() -> underTest.getOptionalByKey(SOME_KEY))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("Metric cache has not been initialized");
+ }
+
+ @Test
+ public void getOptionalByKey_whenMetricDoesNotExist_thenReturnsOptionalEmpty() {
+ underTest.start();
+ assertThat(underTest.getOptionalByKey(SOME_KEY)).isNotPresent();
+ }
+
+ @Test
+ public void getOptionalByKey_whenMetricIsDisabled_thenReturnsOptionalEmpty() {
+ dbTester.measures().insertMetric(t -> t.setKey("complexity").setEnabled(false));
+
+ underTest.start();
+
+ assertThat(underTest.getOptionalByKey("complexity")).isNotPresent();
+ }
+
+ @Test
+ public void getOptionalByKey_find_enabled_Metrics() {
+ MetricDto ncloc = dbTester.measures().insertMetric(t -> t.setKey("ncloc").setEnabled(true));
+ MetricDto coverage = dbTester.measures().insertMetric(t -> t.setKey("coverage").setEnabled(true));
+
+ underTest.start();
+
+ assertThat(underTest.getOptionalByKey("ncloc").get().getUuid()).isEqualTo(ncloc.getUuid());
+ assertThat(underTest.getOptionalByKey("coverage").get().getUuid()).isEqualTo(coverage.getUuid());
+ }
+
+ @Test
public void getById_throws_ISE_if_start_has_not_been_called() {
assertThatThrownBy(() -> underTest.getByUuid(SOME_UUID))
.isInstanceOf(IllegalStateException.class)
diff --git a/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepIT.java b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepIT.java
index b0aed6b3f10..a3ad472bf94 100644
--- a/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepIT.java
+++ b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepIT.java
@@ -62,7 +62,6 @@ import static org.apache.commons.lang3.RandomStringUtils.secure;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.fail;
-import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
@@ -89,8 +88,7 @@ public class LoadPeriodsStepIT extends BaseStepTest {
private final NewCodePeriodResolver newCodePeriodResolver = new NewCodePeriodResolver(dbTester.getDbClient(), analysisMetadataHolder);
private final ZonedDateTime analysisDate = ZonedDateTime.of(2019, 3, 20, 5, 30, 40, 0, ZoneId.systemDefault());
private final CeTaskMessages ceTaskMessages = mock(CeTaskMessages.class);
- private final LoadPeriodsStep underTest = new LoadPeriodsStep(analysisMetadataHolder, dao, treeRootHolder, periodsHolder, dbTester.getDbClient(), newCodePeriodResolver,
- ceTaskMessages, system2Mock);
+ private final LoadPeriodsStep underTest = new LoadPeriodsStep(analysisMetadataHolder, dao, treeRootHolder, periodsHolder, dbTester.getDbClient(), newCodePeriodResolver);
private ComponentDto project;
@@ -198,7 +196,6 @@ public class LoadPeriodsStepIT extends BaseStepTest {
underTest.execute(new TestComputationStepContext());
assertPeriod(NewCodePeriodType.REFERENCE_BRANCH, newCodeReferenceBranch, null);
- verify(ceTaskMessages).add(any(CeTaskMessages.Message.class));
}
@Test
diff --git a/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepIT.java b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepIT.java
new file mode 100644
index 00000000000..0ede18e3ccf
--- /dev/null
+++ b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepIT.java
@@ -0,0 +1,112 @@
+/*
+ * 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.ce.task.projectanalysis.step;
+
+import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolderRule;
+import org.sonar.ce.task.projectanalysis.analysis.TestBranch;
+import org.sonar.ce.task.projectanalysis.component.Component;
+import org.sonar.ce.task.projectanalysis.component.ReportComponent;
+import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule;
+import org.sonar.ce.task.projectanalysis.period.Period;
+import org.sonar.ce.task.projectanalysis.period.PeriodHolderRule;
+import org.sonar.ce.task.projectanalysis.period.PeriodOrigin;
+import org.sonar.ce.task.step.ComputationStep.Context;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.ProjectData;
+import org.sonar.db.newcodeperiod.NewCodePeriodDto;
+import org.sonar.server.project.Project;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.mockito.Mockito.mock;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH;
+
+class PersistReferenceBranchPeriodStepIT {
+
+ private static final String BRANCH_NAME = "feature";
+
+ @RegisterExtension
+ private final PeriodHolderRule periodHolder = new PeriodHolderRule();
+
+ @RegisterExtension
+ private final AnalysisMetadataHolderRule analysisMetadataHolder = new AnalysisMetadataHolderRule();
+
+ @RegisterExtension
+ private final TreeRootHolderRule treeRootHolderRule = new TreeRootHolderRule();
+
+ @RegisterExtension
+ private final DbTester db = DbTester.create();
+
+ private final PersistReferenceBranchPeriodStep persistReferenceBranchPeriodStep = new PersistReferenceBranchPeriodStep(
+ periodHolder, analysisMetadataHolder, db.getDbClient(), treeRootHolderRule);
+
+ private ProjectData projectData;
+ private String branchUuid;
+
+ @BeforeEach
+ void setUp() {
+ projectData = db.components().insertPrivateProject();
+ BranchDto branchDto = db.components().insertProjectBranch(projectData.getProjectDto(), branch -> branch.setKey(BRANCH_NAME));
+ branchUuid = branchDto.getUuid();
+
+ analysisMetadataHolder.setProject(new Project(projectData.projectUuid(), projectData.projectKey(), projectData.projectKey(), null, List.of()));
+ analysisMetadataHolder.setBranch(new TestBranch(BRANCH_NAME));
+ periodHolder.setPeriod(new Period(REFERENCE_BRANCH.name(), "main", null));
+ periodHolder.setPeriodOrigin(PeriodOrigin.SCANNER);
+
+ ReportComponent reportComponent = ReportComponent
+ .builder(Component.Type.PROJECT, 1)
+ .setUuid(branchUuid)
+ .setKey(branchDto.getKey())
+ .build();
+ treeRootHolderRule.setRoot(reportComponent);
+ }
+
+ @Test
+ void execute_shouldPersistReferenceBranchPeriod() {
+
+ persistReferenceBranchPeriodStep.execute(mock(Context.class));
+
+ NewCodePeriodDto newCodePeriodDto = db.getDbClient().newCodePeriodDao().selectByBranch(db.getSession(), projectData.projectUuid(), branchUuid)
+ .orElseGet(() -> fail("No new code period found for branch"));
+ assertThat(newCodePeriodDto.getBranchUuid()).isEqualTo(branchUuid);
+ assertThat(newCodePeriodDto.getType()).isEqualTo(REFERENCE_BRANCH);
+ assertThat(newCodePeriodDto.getValue()).isEqualTo("main");
+ }
+
+ @Test
+ void execute_shouldUpdateReferenceBranchPeriod() {
+ db.newCodePeriods().insert(projectData.projectUuid(), branchUuid, REFERENCE_BRANCH, "old_value");
+
+ persistReferenceBranchPeriodStep.execute(mock(Context.class));
+
+ NewCodePeriodDto newCodePeriodDto = db.getDbClient().newCodePeriodDao().selectByBranch(db.getSession(), projectData.projectUuid(), branchUuid)
+ .orElseGet(() -> fail("No new code period found for branch"));
+ assertThat(newCodePeriodDto.getBranchUuid()).isEqualTo(branchUuid);
+ assertThat(newCodePeriodDto.getType()).isEqualTo(REFERENCE_BRANCH);
+ assertThat(newCodePeriodDto.getValue()).isEqualTo("main");
+ }
+
+}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/component/TreeRootHolder.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/component/TreeRootHolder.java
index 89287f0d49a..a37bb86bcb1 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/component/TreeRootHolder.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/component/TreeRootHolder.java
@@ -70,14 +70,11 @@ public interface TreeRootHolder {
Component getComponentByUuid(String uuid);
/**
- * Return a component by its batch reference. Returns {@link Optional#empty()} if there's
- * no {@link Component} with the specified reference
+ * Return a component by its scanner reference. Returns {@link Optional#empty()} if there's
+ * no {@link Component} with the specified reference. Note that on PRs, unchanged components are not indexed.
*
* @throws IllegalStateException if the holder is empty (ie. there is no root yet)
- * @deprecated This method was introduced as a quick fix for SONAR-10781. Ultimately one should never manipulate component
- * ref that doesn't exist in the scanner report
*/
- @Deprecated
Optional<Component> getOptionalComponentByRef(int ref);
/**
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/formula/AverageFormula.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/formula/AverageFormula.java
deleted file mode 100644
index 4dab14d2591..00000000000
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/formula/AverageFormula.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * 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.ce.task.projectanalysis.formula;
-
-import java.util.Optional;
-import org.sonar.ce.task.projectanalysis.measure.Measure;
-
-import static java.util.Objects.requireNonNull;
-
-public class AverageFormula implements Formula<AverageFormula.AverageCounter> {
-
- private final String outputMetricKey;
-
- private final String mainMetric;
- private final String byMetric;
-
- private AverageFormula(Builder builder) {
- this.outputMetricKey = builder.outputMetricKey;
- this.mainMetric = builder.mainMetric;
- this.byMetric = builder.byMetric;
- }
-
- @Override
- public AverageCounter createNewCounter() {
- return new AverageCounter();
- }
-
- @Override
- public Optional<Measure> createMeasure(AverageCounter counter, CreateMeasureContext context) {
- Optional<Double> mainValueOptional = counter.getMainValue();
- Optional<Double> byValueOptional = counter.getByValue();
- if (mainValueOptional.isPresent() && byValueOptional.isPresent()) {
- double mainValue = mainValueOptional.get();
- double byValue = byValueOptional.get();
- if (byValue > 0D) {
- return Optional.of(Measure.newMeasureBuilder().create(mainValue / byValue, context.getMetric().getDecimalScale()));
- }
- }
- return Optional.empty();
- }
-
- @Override
- public String[] getOutputMetricKeys() {
- return new String[] {outputMetricKey};
- }
-
- public static class Builder {
- private String outputMetricKey;
- private String mainMetric;
- private String byMetric;
-
- private Builder() {
- // prevents instantiation outside static method
- }
-
- public static Builder newBuilder() {
- return new Builder();
- }
-
- public Builder setOutputMetricKey(String m) {
- this.outputMetricKey = m;
- return this;
- }
-
- public Builder setMainMetricKey(String m) {
- this.mainMetric = m;
- return this;
- }
-
- public Builder setByMetricKey(String m) {
- this.byMetric = m;
- return this;
- }
-
- public AverageFormula build() {
- requireNonNull(outputMetricKey, "Output metric key cannot be null");
- requireNonNull(mainMetric, "Main metric Key cannot be null");
- requireNonNull(byMetric, "By metric Key cannot be null");
- return new AverageFormula(this);
- }
- }
-
- class AverageCounter implements Counter<AverageCounter> {
-
- private boolean initialized = false;
-
- private double mainValue = 0D;
- private double byValue = 0D;
-
- @Override
- public void aggregate(AverageCounter counter) {
- addValuesIfPresent(counter.getMainValue(), counter.getByValue());
- }
-
- @Override
- public void initialize(CounterInitializationContext context) {
- Optional<Double> mainValueOptional = getDoubleValue(context.getMeasure(mainMetric));
- Optional<Double> byValueOptional = getDoubleValue(context.getMeasure(byMetric));
- addValuesIfPresent(mainValueOptional, byValueOptional);
- }
-
- private void addValuesIfPresent(Optional<Double> counterMainValue, Optional<Double> counterByValue) {
- if (counterMainValue.isPresent() && counterByValue.isPresent()) {
- initialized = true;
- mainValue += counterMainValue.get();
- byValue += counterByValue.get();
- }
- }
-
- public Optional<Double> getMainValue() {
- return getValue(mainValue);
- }
-
- public Optional<Double> getByValue() {
- return getValue(byValue);
- }
-
- private Optional<Double> getValue(double value) {
- if (initialized) {
- return Optional.of(value);
- }
- return Optional.empty();
- }
-
- private Optional<Double> getDoubleValue(Optional<Measure> measureOptional) {
- if (!measureOptional.isPresent()) {
- return Optional.empty();
- }
- Measure measure = measureOptional.get();
- switch (measure.getValueType()) {
- case DOUBLE:
- return Optional.of(measure.getDoubleValue());
- case LONG:
- return Optional.of((double) measure.getLongValue());
- case INT:
- return Optional.of((double) measure.getIntValue());
- case NO_VALUE:
- return Optional.empty();
- default:
- throw new IllegalArgumentException(String.format("Measure of type '%s' are not supported", measure.getValueType().name()));
- }
- }
- }
-}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/formula/DistributionFormula.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/formula/DistributionFormula.java
deleted file mode 100644
index d0d0bacd0bc..00000000000
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/formula/DistributionFormula.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * 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.ce.task.projectanalysis.formula;
-
-import java.util.Optional;
-import org.sonar.api.ce.measure.RangeDistributionBuilder;
-import org.sonar.ce.task.projectanalysis.component.Component;
-import org.sonar.ce.task.projectanalysis.component.CrawlerDepthLimit;
-import org.sonar.ce.task.projectanalysis.measure.Measure;
-
-import static java.util.Objects.requireNonNull;
-
-public class DistributionFormula implements Formula<DistributionFormula.DistributionCounter> {
-
- private final String metricKey;
-
- public DistributionFormula(String metricKey) {
- this.metricKey = requireNonNull(metricKey, "Metric key cannot be null");
- }
-
- @Override
- public DistributionCounter createNewCounter() {
- return new DistributionCounter();
- }
-
- @Override
- public Optional<Measure> createMeasure(DistributionCounter counter, CreateMeasureContext context) {
- Component.Type componentType = context.getComponent().getType();
- Optional<String> value = counter.getValue();
- if (value.isPresent() && CrawlerDepthLimit.LEAVES.isDeeperThan(componentType)) {
- return Optional.of(Measure.newMeasureBuilder().create(value.get()));
- }
- return Optional.empty();
- }
-
- @Override
- public String[] getOutputMetricKeys() {
- return new String[] {metricKey};
- }
-
- class DistributionCounter implements Counter<DistributionCounter> {
-
- private final RangeDistributionBuilder distribution = new RangeDistributionBuilder();
- private boolean initialized = false;
-
- @Override
- public void aggregate(DistributionCounter counter) {
- Optional<String> value = counter.getValue();
- if (value.isPresent()) {
- initialized = true;
- distribution.add(value.get());
- }
- }
-
- @Override
- public void initialize(CounterInitializationContext context) {
- Optional<Measure> measureOptional = context.getMeasure(metricKey);
- String data = measureOptional.isPresent() ? measureOptional.get().getData() : null;
- if (data != null) {
- initialized = true;
- distribution.add(data);
- }
- }
-
- public Optional<String> getValue() {
- if (initialized) {
- return Optional.ofNullable(distribution.build());
- }
- return Optional.empty();
- }
- }
-}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java
index dc006242431..8b62a6abaae 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java
@@ -345,7 +345,8 @@ public class TrackerRawInputFactory {
private Optional<DbIssues.Location> convertLocation(ScannerReport.IssueLocation source) {
DbIssues.Location.Builder target = DbIssues.Location.newBuilder();
if (source.getComponentRef() != 0 && source.getComponentRef() != component.getReportAttributes().getRef()) {
- // SONAR-10781 Component might not exist because on PR, only changed components are included in the report
+ // Component might not exist because on PR, only changed components are included in the component tree
+ // See in BuildComponentTreeStep the call to buildChangedComponentTreeRoot
Optional<Component> optionalComponent = treeRootHolder.getOptionalComponentByRef(source.getComponentRef());
if (optionalComponent.isEmpty()) {
return Optional.empty();
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationCheck.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationCheck.java
index 27ffc039b71..9863836e1d0 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationCheck.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationCheck.java
@@ -52,6 +52,8 @@ public interface PostMeasuresComputationCheck {
*/
String getProjectUuid();
+ String getAnalysisUuid();
+
Branch getBranch();
ScannerReportReader getReportReader();
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationChecksStep.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationChecksStep.java
index 4ce9187cc70..fb40cf4ed7c 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationChecksStep.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationChecksStep.java
@@ -77,6 +77,11 @@ public class PostMeasuresComputationChecksStep implements ComputationStep {
}
@Override
+ public String getAnalysisUuid() {
+ return analysisMetadataHolder.getUuid();
+ }
+
+ @Override
public Branch getBranch() {
return analysisMetadataHolder.getBranch();
}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/metric/MetricRepository.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/metric/MetricRepository.java
index 6dc8dd686d9..751bf23ed8d 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/metric/MetricRepository.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/metric/MetricRepository.java
@@ -26,9 +26,11 @@ public interface MetricRepository {
/**
* Gets the {@link Metric} with the specific key.
- * <p>Since it does not make sense to encounter a reference (ie. a key) to a Metric during processing of
+ * <p>Since it <i>mostly</i> does not make sense to encounter a reference (ie. a key) to a Metric during processing of
* a new analysis and not finding it in DB (metrics are never deleted), this method will throw an
* IllegalStateException if the metric with the specified key can not be found.</p>
+ * <p>Core extensions that add their own metrics on commercial versions may not be available everywhere.
+ * Use getOptionalByKey if you need to work with a metric that may not exist in all environments.</p>
*
* @throws IllegalStateException if no Metric with the specified key is found
* @throws NullPointerException if the specified key is {@code null}
@@ -36,6 +38,14 @@ public interface MetricRepository {
Metric getByKey(String key);
/**
+ * Gets the {@link Metric} with the specific key if it exists. Useful if working with a metric that may
+ * not exist in all versions of SonarQube Server.
+ *
+ * @throws NullPointerException if the specified key is {@code null}
+ */
+ Optional<Metric> getOptionalByKey(String key);
+
+ /**
* Gets the {@link Metric} with the specific uuid.
*
* @throws IllegalStateException if no Metric with the specified uuid is found
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryImpl.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryImpl.java
index 6078d8d15bc..1a6a8a16bcf 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryImpl.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryImpl.java
@@ -70,6 +70,16 @@ public class MetricRepositoryImpl implements MetricRepository, Startable {
}
@Override
+ public Optional<Metric> getOptionalByKey(String key) {
+ requireNonNull(key);
+ verifyMetricsInitialized();
+
+ Metric res = this.metricsByKey.get(key);
+
+ return Optional.ofNullable(res);
+ }
+
+ @Override
public Metric getByUuid(String uuid) {
return getOptionalByUuid(uuid)
.orElseThrow(() -> new IllegalStateException(String.format("Metric with uuid '%s' does not exist", uuid)));
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolder.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolder.java
index 1705c9fb3bb..9fe7dadb9f5 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolder.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolder.java
@@ -51,4 +51,10 @@ public interface PeriodHolder {
*/
Period getPeriod();
+ /**
+ * Retrieve the context from which this period is coming from. For example, it can be coming from a scanner parameter, a global setting, etc.
+ * See {@link PeriodOrigin} for the possible values.
+ */
+ PeriodOrigin getPeriodOrigin();
+
}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderImpl.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderImpl.java
index 86ea07a18e2..2150f3f5539 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderImpl.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderImpl.java
@@ -29,6 +29,7 @@ public class PeriodHolderImpl implements PeriodHolder {
@CheckForNull
private Period period = null;
private boolean initialized = false;
+ private PeriodOrigin periodOrigin = null;
/**
* Initializes the periods in the holder.
@@ -41,6 +42,10 @@ public class PeriodHolderImpl implements PeriodHolder {
this.initialized = true;
}
+ public void setPeriodOrigin(PeriodOrigin periodOrigin) {
+ this.periodOrigin = periodOrigin;
+ }
+
@Override
public boolean hasPeriod() {
checkHolderIsInitialized();
@@ -60,6 +65,11 @@ public class PeriodHolderImpl implements PeriodHolder {
return period;
}
+ @Override
+ public PeriodOrigin getPeriodOrigin() {
+ return periodOrigin;
+ }
+
private void checkHolderIsInitialized() {
checkState(initialized, "Period have not been initialized yet");
}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodOrigin.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodOrigin.java
new file mode 100644
index 00000000000..6e2847fe480
--- /dev/null
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodOrigin.java
@@ -0,0 +1,25 @@
+/*
+ * 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.ce.task.projectanalysis.period;
+
+public enum PeriodOrigin {
+ SCANNER,
+ SETTINGS
+}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/scanner/ScannerReportReaderImpl.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/scanner/ScannerReportReaderImpl.java
index 5f2627c31a0..fb39308cd00 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/scanner/ScannerReportReaderImpl.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/scanner/ScannerReportReaderImpl.java
@@ -19,8 +19,6 @@
*/
package org.sonar.ce.task.projectanalysis.scanner;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -38,6 +36,8 @@ import org.sonar.scanner.protocol.output.FileStructure;
import org.sonar.scanner.protocol.output.ScannerReport;
import org.sonar.scanner.protocol.output.ScannerReport.LineSgnificantCode;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
public class ScannerReportReaderImpl implements ScannerReportReader {
private final ScannerReportDirectoryHolder scannerReportDirectoryHolder;
@@ -235,9 +235,9 @@ public class ScannerReportReaderImpl implements ScannerReportReader {
}
@Override
- public File readDependencyFilesZip() {
+ public File readDependencyFilesArchive() {
ensureInitialized();
- return delegate.readDependencyFilesZip();
+ return delegate.readDependencyFilesArchive();
}
@Override
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java
index 8d9026c42af..84b93149e2c 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java
@@ -20,15 +20,13 @@
package org.sonar.ce.task.projectanalysis.step;
import java.util.Optional;
-import org.sonar.api.utils.System2;
-import org.sonar.ce.task.log.CeTaskMessages;
-import org.sonar.ce.task.log.CeTaskMessages.Message;
import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
import org.sonar.ce.task.projectanalysis.period.NewCodePeriodResolver;
import org.sonar.ce.task.projectanalysis.period.Period;
import org.sonar.ce.task.projectanalysis.period.PeriodHolder;
import org.sonar.ce.task.projectanalysis.period.PeriodHolderImpl;
+import org.sonar.ce.task.projectanalysis.period.PeriodOrigin;
import org.sonar.ce.task.step.ComputationStep;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
@@ -53,19 +51,15 @@ public class LoadPeriodsStep implements ComputationStep {
private final PeriodHolderImpl periodsHolder;
private final DbClient dbClient;
private final NewCodePeriodResolver resolver;
- private final CeTaskMessages ceTaskMessages;
- private final System2 system2;
public LoadPeriodsStep(AnalysisMetadataHolder analysisMetadataHolder, NewCodePeriodDao newCodePeriodDao, TreeRootHolder treeRootHolder,
- PeriodHolderImpl periodsHolder, DbClient dbClient, NewCodePeriodResolver resolver, CeTaskMessages ceTaskMessages, System2 system2) {
+ PeriodHolderImpl periodsHolder, DbClient dbClient, NewCodePeriodResolver resolver) {
this.analysisMetadataHolder = analysisMetadataHolder;
this.newCodePeriodDao = newCodePeriodDao;
this.treeRootHolder = treeRootHolder;
this.periodsHolder = periodsHolder;
this.dbClient = dbClient;
this.resolver = resolver;
- this.ceTaskMessages = ceTaskMessages;
- this.system2 = system2;
}
@Override
@@ -89,6 +83,8 @@ public class LoadPeriodsStep implements ComputationStep {
.map(b -> new NewCodePeriodDto().setType(REFERENCE_BRANCH).setValue(b))
.orElse(null);
+ PeriodOrigin periodOrigin = newCodePeriod == null ? PeriodOrigin.SETTINGS : PeriodOrigin.SCANNER;
+
try (DbSession dbSession = dbClient.openSession(false)) {
Optional<NewCodePeriodDto> branchSpecificSetting = getBranchSetting(dbSession, projectUuid, branchUuid);
@@ -102,13 +98,11 @@ public class LoadPeriodsStep implements ComputationStep {
periodsHolder.setPeriod(null);
return;
}
- } else if (branchSpecificSetting.isPresent()) {
- ceTaskMessages.add(new Message("A scanner parameter is defining a new code reference branch, but this conflicts with the New Code Period"
- + " setting of your branch. Please check your project configuration. You should use either one or the other but not both.", system2.now()));
}
Period period = resolver.resolve(dbSession, branchUuid, newCodePeriod, projectVersion);
periodsHolder.setPeriod(period);
+ periodsHolder.setPeriodOrigin(periodOrigin);
}
}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStep.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStep.java
new file mode 100644
index 00000000000..96f471946ae
--- /dev/null
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStep.java
@@ -0,0 +1,113 @@
+/*
+ * 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.ce.task.projectanalysis.step;
+
+import java.util.Objects;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
+import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
+import org.sonar.ce.task.projectanalysis.period.PeriodHolder;
+import org.sonar.ce.task.projectanalysis.period.PeriodOrigin;
+import org.sonar.ce.task.step.ComputationStep;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.newcodeperiod.NewCodePeriodDto;
+import org.sonar.db.newcodeperiod.NewCodePeriodType;
+
+public class PersistReferenceBranchPeriodStep implements ComputationStep {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(PersistReferenceBranchPeriodStep.class);
+
+ private final PeriodHolder periodHolder;
+ private final AnalysisMetadataHolder analysisMetadataHolder;
+ private final DbClient dbClient;
+ private final TreeRootHolder treeRootHolder;
+
+ public PersistReferenceBranchPeriodStep(PeriodHolder periodHolder, AnalysisMetadataHolder analysisMetadataHolder, DbClient dbClient, TreeRootHolder treeRootHolder) {
+ this.periodHolder = periodHolder;
+ this.analysisMetadataHolder = analysisMetadataHolder;
+ this.dbClient = dbClient;
+ this.treeRootHolder = treeRootHolder;
+ }
+
+ @Override
+ public String getDescription() {
+ return "Persist or update reference branch new code period";
+ }
+
+ @Override
+ public void execute(Context context) {
+ if (shouldExecute()) {
+ executePersistPeriodStep();
+ }
+ }
+
+ private boolean shouldExecute() {
+ return analysisMetadataHolder.isBranch() && periodHolder.hasPeriod()
+ && periodHolder.getPeriodOrigin() == PeriodOrigin.SCANNER;
+ }
+
+ void executePersistPeriodStep() {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ String projectUuid = analysisMetadataHolder.getProject().getUuid();
+ String branchUuid = treeRootHolder.getRoot().getUuid();
+
+ dbClient.newCodePeriodDao()
+ .selectByBranch(dbSession, projectUuid, branchUuid)
+ .ifPresentOrElse(
+ existingNewCodePeriod -> updateNewCodePeriodIfNeeded(dbSession, existingNewCodePeriod),
+ () -> createNewCodePeriod(dbSession, branchUuid)
+ );
+ }
+ }
+
+ private void updateNewCodePeriodIfNeeded(DbSession dbSession, NewCodePeriodDto newCodePeriodDto) {
+ if (shouldUpdateNewCodePeriod(newCodePeriodDto)) {
+ LOGGER.debug("Updating reference branch new code period '{}' for project '{}' and branch '{}'",
+ periodHolder.getPeriod().getModeParameter(), analysisMetadataHolder.getProject().getName(), analysisMetadataHolder.getBranch().getName());
+ newCodePeriodDto.setValue(periodHolder.getPeriod().getModeParameter());
+ newCodePeriodDto.setType(NewCodePeriodType.REFERENCE_BRANCH);
+ dbClient.newCodePeriodDao().update(dbSession, newCodePeriodDto);
+ dbSession.commit();
+ }
+ }
+
+ private boolean shouldUpdateNewCodePeriod(NewCodePeriodDto existingNewCodePeriod) {
+ return existingNewCodePeriod.getType() != NewCodePeriodType.REFERENCE_BRANCH
+ || !Objects.equals(existingNewCodePeriod.getValue(), periodHolder.getPeriod().getModeParameter());
+ }
+
+ private void createNewCodePeriod(DbSession dbSession, String branchUuid) {
+ LOGGER.debug("Persisting reference branch new code period '{}' for project '{}' and branch '{}'",
+ periodHolder.getPeriod().getModeParameter(), analysisMetadataHolder.getProject().getName(), analysisMetadataHolder.getBranch().getName());
+ dbClient.newCodePeriodDao().insert(dbSession, buildNewCodePeriodDto(branchUuid));
+ dbSession.commit();
+ }
+
+ private NewCodePeriodDto buildNewCodePeriodDto(String branchUuid) {
+ return new NewCodePeriodDto()
+ .setProjectUuid(analysisMetadataHolder.getProject().getUuid())
+ .setBranchUuid(branchUuid)
+ .setType(NewCodePeriodType.REFERENCE_BRANCH)
+ .setValue(periodHolder.getPeriod().getModeParameter());
+ }
+
+}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java
index 775f3c5f036..b121df6e657 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java
@@ -52,9 +52,6 @@ public class ReportComputationSteps extends AbstractComputationSteps {
ValidateProjectStep.class,
LoadQualityProfilesStep.class,
- // Dependencies
- ScaStep.class,
-
// Pre analysis operations
PreMeasuresComputationChecksStep.class,
SqUpgradeDetectionEventsStep.class,
@@ -71,6 +68,9 @@ public class ReportComputationSteps extends AbstractComputationSteps {
LoadDuplicationsFromReportStep.class,
LoadCrossProjectDuplicationsRepositoryStep.class,
+ // Dependencies
+ ScaStep.class,
+
// data computation
SizeMeasuresStep.class,
NewCoverageMeasuresStep.class,
@@ -106,6 +106,7 @@ public class ReportComputationSteps extends AbstractComputationSteps {
// Persist data
PersistScannerAnalysisCacheStep.class,
PersistComponentsStep.class,
+ PersistReferenceBranchPeriodStep.class,
PersistAnalysisStep.class,
PersistAnalysisPropertiesStep.class,
PersistProjectMeasuresStep.class,
@@ -137,8 +138,7 @@ public class ReportComputationSteps extends AbstractComputationSteps {
TriggerViewRefreshStep.class,
// send analysis and steps statistics telemetry
- SendAnalysisTelemetryStep.class
- );
+ SendAnalysisTelemetryStep.class);
public ReportComputationSteps(TaskContainer taskContainer) {
super(taskContainer);
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/AverageFormulaExecutionTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/AverageFormulaExecutionTest.java
deleted file mode 100644
index 519312ef9cb..00000000000
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/AverageFormulaExecutionTest.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * 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.ce.task.projectanalysis.formula;
-
-import com.google.common.collect.Lists;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.sonar.api.measures.CoreMetrics;
-import org.sonar.ce.task.projectanalysis.component.Component;
-import org.sonar.ce.task.projectanalysis.component.PathAwareCrawler;
-import org.sonar.ce.task.projectanalysis.component.ReportComponent;
-import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule;
-import org.sonar.ce.task.projectanalysis.measure.MeasureRepositoryRule;
-import org.sonar.ce.task.projectanalysis.metric.MetricRepositoryRule;
-import org.sonar.ce.task.projectanalysis.period.PeriodHolderRule;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.sonar.api.measures.CoreMetrics.COMPLEXITY_IN_FUNCTIONS_KEY;
-import static org.sonar.api.measures.CoreMetrics.FUNCTIONS_KEY;
-import static org.sonar.api.measures.CoreMetrics.FUNCTION_COMPLEXITY_KEY;
-import static org.sonar.ce.task.projectanalysis.component.Component.Type.DIRECTORY;
-import static org.sonar.ce.task.projectanalysis.component.Component.Type.PROJECT;
-import static org.sonar.ce.task.projectanalysis.component.ReportComponent.builder;
-import static org.sonar.ce.task.projectanalysis.measure.Measure.newMeasureBuilder;
-import static org.sonar.ce.task.projectanalysis.measure.MeasureRepoEntry.entryOf;
-import static org.sonar.ce.task.projectanalysis.measure.MeasureRepoEntry.toEntries;
-
-public class AverageFormulaExecutionTest {
-
- @Rule
- public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule();
- @Rule
- public MetricRepositoryRule metricRepository = new MetricRepositoryRule()
- .add(CoreMetrics.FUNCTION_COMPLEXITY)
- .add(CoreMetrics.COMPLEXITY_IN_FUNCTIONS)
- .add(CoreMetrics.FUNCTIONS);
- @Rule
- public MeasureRepositoryRule measureRepository = MeasureRepositoryRule.create(treeRootHolder, metricRepository);
- @Rule
- public PeriodHolderRule periodsHolder = new PeriodHolderRule();
-
- private FormulaExecutorComponentVisitor underTest;
-
- @Before
- public void setUp() {
- underTest = FormulaExecutorComponentVisitor.newBuilder(metricRepository, measureRepository)
- .buildFor(Lists.newArrayList(
- AverageFormula.Builder.newBuilder()
- .setOutputMetricKey(FUNCTION_COMPLEXITY_KEY)
- .setMainMetricKey(COMPLEXITY_IN_FUNCTIONS_KEY)
- .setByMetricKey(FUNCTIONS_KEY)
- .build()));
- }
-
- @Test
- public void add_measures() {
- ReportComponent project = builder(PROJECT, 1)
- .addChildren(
- builder(DIRECTORY, 111)
- .addChildren(
- builder(Component.Type.FILE, 1111).build(),
- builder(Component.Type.FILE, 1112).build())
- .build(),
- builder(DIRECTORY, 121)
- .addChildren(
- builder(Component.Type.FILE, 1211).build())
- .build())
- .build();
-
- treeRootHolder.setRoot(project);
-
- measureRepository.addRawMeasure(1111, COMPLEXITY_IN_FUNCTIONS_KEY, newMeasureBuilder().create(5));
- measureRepository.addRawMeasure(1111, FUNCTIONS_KEY, newMeasureBuilder().create(2));
-
- measureRepository.addRawMeasure(1112, COMPLEXITY_IN_FUNCTIONS_KEY, newMeasureBuilder().create(1));
- measureRepository.addRawMeasure(1112, FUNCTIONS_KEY, newMeasureBuilder().create(1));
-
- measureRepository.addRawMeasure(1211, COMPLEXITY_IN_FUNCTIONS_KEY, newMeasureBuilder().create(9));
- measureRepository.addRawMeasure(1211, FUNCTIONS_KEY, newMeasureBuilder().create(2));
-
- new PathAwareCrawler<>(underTest).visit(project);
-
- assertThat(toEntries(measureRepository.getAddedRawMeasures(1))).containsOnly(entryOf(FUNCTION_COMPLEXITY_KEY, newMeasureBuilder().create(3d, 1)));
- assertThat(toEntries(measureRepository.getAddedRawMeasures(111))).containsOnly(entryOf(FUNCTION_COMPLEXITY_KEY, newMeasureBuilder().create(2d, 1)));
- assertThat(toEntries(measureRepository.getAddedRawMeasures(1111))).containsOnly(entryOf(FUNCTION_COMPLEXITY_KEY, newMeasureBuilder().create(2.5d, 1)));
- assertThat(toEntries(measureRepository.getAddedRawMeasures(1112))).containsOnly(entryOf(FUNCTION_COMPLEXITY_KEY, newMeasureBuilder().create(1d, 1)));
- assertThat(toEntries(measureRepository.getAddedRawMeasures(121))).containsOnly(entryOf(FUNCTION_COMPLEXITY_KEY, newMeasureBuilder().create(4.5d, 1)));
- assertThat(toEntries(measureRepository.getAddedRawMeasures(1211))).containsOnly(entryOf(FUNCTION_COMPLEXITY_KEY, newMeasureBuilder().create(4.5d, 1)));
- }
-
- @Test
- public void not_add_measures_when_no_data_on_file() {
- ReportComponent project = builder(PROJECT, 1)
- .addChildren(
- builder(DIRECTORY, 111)
- .addChildren(
- builder(Component.Type.FILE, 1111).build())
- .build())
- .build();
-
- treeRootHolder.setRoot(project);
-
- new PathAwareCrawler<>(underTest).visit(project);
-
- assertThat(measureRepository.getAddedRawMeasures(1)).isEmpty();
- assertThat(measureRepository.getAddedRawMeasures(111)).isEmpty();
- assertThat(measureRepository.getAddedRawMeasures(1111)).isEmpty();
- }
-
-}
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/AverageFormulaTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/AverageFormulaTest.java
deleted file mode 100644
index 7d830eac3d5..00000000000
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/AverageFormulaTest.java
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
- * 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.ce.task.projectanalysis.formula;
-
-import java.util.Optional;
-import org.junit.Test;
-import org.sonar.ce.task.projectanalysis.component.Component;
-import org.sonar.ce.task.projectanalysis.component.ReportComponent;
-import org.sonar.ce.task.projectanalysis.measure.Measure;
-import org.sonar.ce.task.projectanalysis.metric.Metric;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.sonar.api.measures.CoreMetrics.COMPLEXITY_IN_FUNCTIONS_KEY;
-import static org.sonar.api.measures.CoreMetrics.FUNCTIONS_KEY;
-import static org.sonar.api.measures.CoreMetrics.FUNCTION_COMPLEXITY_KEY;
-import static org.sonar.ce.task.projectanalysis.formula.AverageFormula.Builder;
-
-public class AverageFormulaTest {
-
- private static final AverageFormula BASIC_AVERAGE_FORMULA = Builder.newBuilder()
- .setOutputMetricKey(FUNCTION_COMPLEXITY_KEY)
- .setMainMetricKey(COMPLEXITY_IN_FUNCTIONS_KEY)
- .setByMetricKey(FUNCTIONS_KEY)
- .build();
-
- CounterInitializationContext counterInitializationContext = mock(CounterInitializationContext.class);
- CreateMeasureContext createMeasureContext = new DumbCreateMeasureContext(
- ReportComponent.builder(Component.Type.PROJECT, 1).build(), mock(Metric.class));
-
-
- @Test
- public void fail_with_NPE_when_building_formula_without_output_metric() {
- assertThatThrownBy(() -> {
- Builder.newBuilder()
- .setOutputMetricKey(null)
- .setMainMetricKey(COMPLEXITY_IN_FUNCTIONS_KEY)
- .setByMetricKey(FUNCTIONS_KEY)
- .build();
- })
- .isInstanceOf(NullPointerException.class)
- .hasMessage("Output metric key cannot be null");
- }
-
- @Test
- public void fail_with_NPE_when_building_formula_without_main_metric() {
- assertThatThrownBy(() -> {
- Builder.newBuilder()
- .setOutputMetricKey(FUNCTION_COMPLEXITY_KEY)
- .setMainMetricKey(null)
- .setByMetricKey(FUNCTIONS_KEY)
- .build();
- })
- .isInstanceOf(NullPointerException.class)
- .hasMessage("Main metric Key cannot be null");
- }
-
- @Test
- public void fail_with_NPE_when_building_formula_without_by_metric() {
- assertThatThrownBy(() -> {
- Builder.newBuilder()
- .setOutputMetricKey(FUNCTION_COMPLEXITY_KEY)
- .setMainMetricKey(COMPLEXITY_IN_FUNCTIONS_KEY)
- .setByMetricKey(null)
- .build();
- })
- .isInstanceOf(NullPointerException.class)
- .hasMessage("By metric Key cannot be null");
- }
-
- @Test
- public void check_new_counter_class() {
- assertThat(BASIC_AVERAGE_FORMULA.createNewCounter().getClass()).isEqualTo(AverageFormula.AverageCounter.class);
- }
-
- @Test
- public void check_output_metric_key_is_function_complexity_key() {
- assertThat(BASIC_AVERAGE_FORMULA.getOutputMetricKeys()).containsOnly(FUNCTION_COMPLEXITY_KEY);
- }
-
- @Test
- public void create_measure_when_counter_is_aggregated_from_context() {
- AverageFormula.AverageCounter counter = BASIC_AVERAGE_FORMULA.createNewCounter();
- addMeasure(COMPLEXITY_IN_FUNCTIONS_KEY, 10d);
- addMeasure(FUNCTIONS_KEY, 2d);
- counter.initialize(counterInitializationContext);
-
- assertThat(BASIC_AVERAGE_FORMULA.createMeasure(counter, createMeasureContext).get().getDoubleValue()).isEqualTo(5d);
- }
-
- @Test
- public void create_measure_when_counter_is_aggregated_from_another_counter() {
- AverageFormula.AverageCounter anotherCounter = BASIC_AVERAGE_FORMULA.createNewCounter();
- addMeasure(COMPLEXITY_IN_FUNCTIONS_KEY, 10d);
- addMeasure(FUNCTIONS_KEY, 2d);
- anotherCounter.initialize(counterInitializationContext);
-
- AverageFormula.AverageCounter counter = BASIC_AVERAGE_FORMULA.createNewCounter();
- counter.aggregate(anotherCounter);
-
- assertThat(BASIC_AVERAGE_FORMULA.createMeasure(counter, createMeasureContext).get().getDoubleValue()).isEqualTo(5d);
- }
-
- @Test
- public void create_double_measure() {
- AverageFormula.AverageCounter counter = BASIC_AVERAGE_FORMULA.createNewCounter();
- addMeasure(COMPLEXITY_IN_FUNCTIONS_KEY, 10d);
- addMeasure(FUNCTIONS_KEY, 2d);
- counter.initialize(counterInitializationContext);
-
- assertThat(BASIC_AVERAGE_FORMULA.createMeasure(counter, createMeasureContext).get().getDoubleValue()).isEqualTo(5d);
- }
-
- @Test
- public void create_integer_measure() {
- AverageFormula.AverageCounter counter = BASIC_AVERAGE_FORMULA.createNewCounter();
- addMeasure(COMPLEXITY_IN_FUNCTIONS_KEY, 10);
- addMeasure(FUNCTIONS_KEY, 2);
- counter.initialize(counterInitializationContext);
-
- assertThat(BASIC_AVERAGE_FORMULA.createMeasure(counter, createMeasureContext).get().getDoubleValue()).isEqualTo(5);
- }
-
- @Test
- public void create_long_measure() {
- AverageFormula.AverageCounter counter = BASIC_AVERAGE_FORMULA.createNewCounter();
- addMeasure(COMPLEXITY_IN_FUNCTIONS_KEY, 10L);
- addMeasure(FUNCTIONS_KEY, 2L);
- counter.initialize(counterInitializationContext);
-
- assertThat(BASIC_AVERAGE_FORMULA.createMeasure(counter, createMeasureContext).get().getDoubleValue()).isEqualTo(5L);
- }
-
- @Test
- public void not_create_measure_when_aggregated_measure_has_no_value() {
- AverageFormula.AverageCounter counter = BASIC_AVERAGE_FORMULA.createNewCounter();
- addMeasure(COMPLEXITY_IN_FUNCTIONS_KEY, 10L);
- when(counterInitializationContext.getMeasure(FUNCTIONS_KEY)).thenReturn(Optional.of(Measure.newMeasureBuilder().createNoValue()));
- counter.initialize(counterInitializationContext);
-
- assertThat(BASIC_AVERAGE_FORMULA.createMeasure(counter, createMeasureContext)).isNotPresent();
- }
-
- @Test
- public void fail_with_IAE_when_aggregate_from_component_and_context_with_not_numeric_measures() {
- assertThatThrownBy(() -> {
- AverageFormula.AverageCounter counter = BASIC_AVERAGE_FORMULA.createNewCounter();
- addMeasure(COMPLEXITY_IN_FUNCTIONS_KEY, 10L);
- when(counterInitializationContext.getMeasure(FUNCTIONS_KEY)).thenReturn(Optional.of(Measure.newMeasureBuilder().create("data")));
- counter.initialize(counterInitializationContext);
-
- BASIC_AVERAGE_FORMULA.createMeasure(counter, createMeasureContext);
- })
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Measure of type 'STRING' are not supported");
- }
-
- @Test
- public void no_measure_created_when_counter_has_no_value() {
- AverageFormula.AverageCounter counter = BASIC_AVERAGE_FORMULA.createNewCounter();
- when(counterInitializationContext.getMeasure(anyString())).thenReturn(Optional.empty());
- counter.initialize(counterInitializationContext);
-
- assertThat(BASIC_AVERAGE_FORMULA.createMeasure(counter, createMeasureContext)).isNotPresent();
- }
-
- @Test
- public void not_create_measure_when_only_one_measure() {
- AverageFormula.AverageCounter counter = BASIC_AVERAGE_FORMULA.createNewCounter();
- addMeasure(COMPLEXITY_IN_FUNCTIONS_KEY, 10L);
- when(counterInitializationContext.getMeasure(FUNCTIONS_KEY)).thenReturn(Optional.empty());
- counter.initialize(counterInitializationContext);
-
- assertThat(BASIC_AVERAGE_FORMULA.createMeasure(counter, createMeasureContext)).isNotPresent();
- }
-
- @Test
- public void not_create_measure_when_by_value_is_zero() {
- AverageFormula.AverageCounter counter = BASIC_AVERAGE_FORMULA.createNewCounter();
- addMeasure(COMPLEXITY_IN_FUNCTIONS_KEY, 10d);
- addMeasure(FUNCTIONS_KEY, 0d);
- counter.initialize(counterInitializationContext);
-
- assertThat(BASIC_AVERAGE_FORMULA.createMeasure(counter, createMeasureContext)).isNotPresent();
- }
-
- private void addMeasure(String metricKey, double value) {
- when(counterInitializationContext.getMeasure(metricKey)).thenReturn(Optional.of(Measure.newMeasureBuilder().create(value, 1)));
- }
-
- private void addMeasure(String metricKey, int value) {
- when(counterInitializationContext.getMeasure(metricKey)).thenReturn(Optional.of(Measure.newMeasureBuilder().create(value)));
- }
-
- private void addMeasure(String metricKey, long value) {
- when(counterInitializationContext.getMeasure(metricKey)).thenReturn(Optional.of(Measure.newMeasureBuilder().create(value)));
- }
-
-}
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/DistributionFormulaExecutionTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/DistributionFormulaExecutionTest.java
deleted file mode 100644
index a406c0dc6f5..00000000000
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/DistributionFormulaExecutionTest.java
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * 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.ce.task.projectanalysis.formula;
-
-import com.google.common.collect.Lists;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.sonar.api.measures.CoreMetrics;
-import org.sonar.ce.task.projectanalysis.component.Component;
-import org.sonar.ce.task.projectanalysis.component.PathAwareCrawler;
-import org.sonar.ce.task.projectanalysis.component.ReportComponent;
-import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule;
-import org.sonar.ce.task.projectanalysis.measure.MeasureRepositoryRule;
-import org.sonar.ce.task.projectanalysis.metric.MetricRepositoryRule;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.sonar.api.measures.CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION_KEY;
-import static org.sonar.ce.task.projectanalysis.component.Component.Type.DIRECTORY;
-import static org.sonar.ce.task.projectanalysis.component.Component.Type.PROJECT;
-import static org.sonar.ce.task.projectanalysis.component.ReportComponent.builder;
-import static org.sonar.ce.task.projectanalysis.measure.Measure.newMeasureBuilder;
-import static org.sonar.ce.task.projectanalysis.measure.MeasureRepoEntry.entryOf;
-import static org.sonar.ce.task.projectanalysis.measure.MeasureRepoEntry.toEntries;
-
-public class DistributionFormulaExecutionTest {
-
- @Rule
- public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule();
- @Rule
- public MetricRepositoryRule metricRepository = new MetricRepositoryRule().add(CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION);
- @Rule
- public MeasureRepositoryRule measureRepository = MeasureRepositoryRule.create(treeRootHolder, metricRepository);
-
- FormulaExecutorComponentVisitor underTest;
-
- @Before
- public void setUp() throws Exception {
- underTest = FormulaExecutorComponentVisitor.newBuilder(metricRepository, measureRepository)
- .buildFor(Lists.newArrayList(new DistributionFormula(FUNCTION_COMPLEXITY_DISTRIBUTION_KEY)));
- }
-
- @Test
- public void add_measures() {
- ReportComponent project = builder(PROJECT, 1)
- .addChildren(
- builder(DIRECTORY, 11)
- .addChildren(
- builder(DIRECTORY, 111)
- .addChildren(
- builder(Component.Type.FILE, 1111).build(),
- builder(Component.Type.FILE, 1112).build())
- .build())
- .build(),
- builder(DIRECTORY, 12)
- .addChildren(
- builder(DIRECTORY, 121)
- .addChildren(
- builder(Component.Type.FILE, 1211).build())
- .build())
- .build())
- .build();
-
- treeRootHolder.setRoot(project);
-
- measureRepository.addRawMeasure(1111, FUNCTION_COMPLEXITY_DISTRIBUTION_KEY, newMeasureBuilder().create("0.5=3;3.5=5;6.5=9"));
- measureRepository.addRawMeasure(1112, FUNCTION_COMPLEXITY_DISTRIBUTION_KEY, newMeasureBuilder().create("0.5=0;3.5=2;6.5=1"));
- measureRepository.addRawMeasure(1211, FUNCTION_COMPLEXITY_DISTRIBUTION_KEY, newMeasureBuilder().create("0.5=1;3.5=3;6.5=2"));
-
- new PathAwareCrawler<>(underTest).visit(project);
-
- assertThat(toEntries(measureRepository.getAddedRawMeasures(1))).containsOnly(entryOf(FUNCTION_COMPLEXITY_DISTRIBUTION_KEY, newMeasureBuilder().create("0.5=4;3.5=10;6.5=12")));
- assertThat(toEntries(measureRepository.getAddedRawMeasures(11))).containsOnly(entryOf(FUNCTION_COMPLEXITY_DISTRIBUTION_KEY, newMeasureBuilder().create("0.5=3;3.5=7;6.5=10")));
- assertThat(toEntries(measureRepository.getAddedRawMeasures(111))).containsOnly(entryOf(FUNCTION_COMPLEXITY_DISTRIBUTION_KEY, newMeasureBuilder().create("0.5=3;3.5=7;6.5=10")));
- assertThat(measureRepository.getAddedRawMeasures(1111)).isEmpty();
- assertThat(measureRepository.getAddedRawMeasures(1112)).isEmpty();
- assertThat(toEntries(measureRepository.getAddedRawMeasures(12))).containsOnly(entryOf(FUNCTION_COMPLEXITY_DISTRIBUTION_KEY, newMeasureBuilder().create("0.5=1;3.5=3;6.5=2")));
- assertThat(toEntries(measureRepository.getAddedRawMeasures(121))).containsOnly(entryOf(FUNCTION_COMPLEXITY_DISTRIBUTION_KEY, newMeasureBuilder().create("0.5=1;3.5=3;6.5=2")));
- assertThat(measureRepository.getAddedRawMeasures(1211)).isEmpty();
- }
-
- @Test
- public void not_add_measures_when_no_data_on_file() {
- ReportComponent project = builder(PROJECT, 1)
- .addChildren(
- builder(DIRECTORY, 11)
- .addChildren(
- builder(DIRECTORY, 111)
- .addChildren(
- builder(Component.Type.FILE, 1111).build())
- .build())
- .build())
- .build();
-
- treeRootHolder.setRoot(project);
-
- new PathAwareCrawler<>(underTest).visit(project);
-
- assertThat(measureRepository.getAddedRawMeasures(1)).isEmpty();
- assertThat(measureRepository.getAddedRawMeasures(11)).isEmpty();
- assertThat(measureRepository.getAddedRawMeasures(111)).isEmpty();
- assertThat(measureRepository.getAddedRawMeasures(1111)).isEmpty();
- }
-
-}
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/DistributionFormulaTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/DistributionFormulaTest.java
deleted file mode 100644
index 89df9c43f79..00000000000
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/DistributionFormulaTest.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * 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.ce.task.projectanalysis.formula;
-
-import java.util.Optional;
-import org.junit.Test;
-import org.sonar.ce.task.projectanalysis.component.Component;
-import org.sonar.ce.task.projectanalysis.component.ReportComponent;
-import org.sonar.ce.task.projectanalysis.measure.Measure;
-import org.sonar.ce.task.projectanalysis.metric.Metric;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.sonar.api.measures.CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION_KEY;
-
-public class DistributionFormulaTest {
-
- private static final DistributionFormula BASIC_DISTRIBUTION_FORMULA = new DistributionFormula(FUNCTION_COMPLEXITY_DISTRIBUTION_KEY);
-
-
- CounterInitializationContext counterInitializationContext = mock(CounterInitializationContext.class);
- CreateMeasureContext projectCreateMeasureContext = new DumbCreateMeasureContext(
- ReportComponent.builder(Component.Type.PROJECT, 1).build(), mock(Metric.class));
- CreateMeasureContext fileCreateMeasureContext = new DumbCreateMeasureContext(
- ReportComponent.builder(Component.Type.FILE, 1).build(), mock(Metric.class));
-
- @Test
- public void check_new_counter_class() {
- assertThat(BASIC_DISTRIBUTION_FORMULA.createNewCounter().getClass()).isEqualTo(DistributionFormula.DistributionCounter.class);
- }
-
- @Test
- public void fail_with_NPE_when_creating_counter_with_null_metric() {
- assertThatThrownBy(() -> new DistributionFormula(null))
- .isInstanceOf(NullPointerException.class)
- .hasMessage("Metric key cannot be null");
- }
-
- @Test
- public void check_output_metric_key_is_function_complexity_distribution() {
- assertThat(BASIC_DISTRIBUTION_FORMULA.getOutputMetricKeys()).containsOnly(FUNCTION_COMPLEXITY_DISTRIBUTION_KEY);
- }
-
- @Test
- public void create_measure() {
- DistributionFormula.DistributionCounter counter = BASIC_DISTRIBUTION_FORMULA.createNewCounter();
- addMeasure(FUNCTION_COMPLEXITY_DISTRIBUTION_KEY, "0=3;3=7;6=10");
- counter.initialize(counterInitializationContext);
-
- assertThat(BASIC_DISTRIBUTION_FORMULA.createMeasure(counter, projectCreateMeasureContext).get().getData()).isEqualTo("0=3;3=7;6=10");
- }
-
- @Test
- public void create_measure_when_counter_is_aggregating_from_another_counter() {
- DistributionFormula.DistributionCounter anotherCounter = BASIC_DISTRIBUTION_FORMULA.createNewCounter();
- addMeasure(FUNCTION_COMPLEXITY_DISTRIBUTION_KEY, "0=3;3=7;6=10");
- anotherCounter.initialize(counterInitializationContext);
-
- DistributionFormula.DistributionCounter counter = BASIC_DISTRIBUTION_FORMULA.createNewCounter();
- counter.aggregate(anotherCounter);
-
- assertThat(BASIC_DISTRIBUTION_FORMULA.createMeasure(counter, projectCreateMeasureContext).get().getData()).isEqualTo("0=3;3=7;6=10");
- }
-
- @Test
- public void create_no_measure_when_no_value() {
- DistributionFormula.DistributionCounter counter = BASIC_DISTRIBUTION_FORMULA.createNewCounter();
- when(counterInitializationContext.getMeasure(FUNCTION_COMPLEXITY_DISTRIBUTION_KEY)).thenReturn(Optional.empty());
- counter.initialize(counterInitializationContext);
-
- assertThat(BASIC_DISTRIBUTION_FORMULA.createMeasure(counter, projectCreateMeasureContext)).isNotPresent();
- }
-
- @Test
- public void not_create_measure_when_on_file() {
- DistributionFormula.DistributionCounter counter = BASIC_DISTRIBUTION_FORMULA.createNewCounter();
- addMeasure(FUNCTION_COMPLEXITY_DISTRIBUTION_KEY, "0=3;3=7;6=10");
- counter.initialize(counterInitializationContext);
-
- assertThat(BASIC_DISTRIBUTION_FORMULA.createMeasure(counter, fileCreateMeasureContext)).isNotPresent();
- }
-
- private void addMeasure(String metricKey, String value) {
- when(counterInitializationContext.getMeasure(metricKey)).thenReturn(Optional.of(Measure.newMeasureBuilder().create(value)));
- }
-
-}
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationChecksStepTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationChecksStepTest.java
index eb80fcfb603..e5ea57b4333 100644
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationChecksStepTest.java
+++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationChecksStepTest.java
@@ -19,6 +19,7 @@
*/
package org.sonar.ce.task.projectanalysis.measure;
+import java.util.UUID;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
@@ -40,6 +41,7 @@ import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import static org.sonar.api.measures.CoreMetrics.NCLOC;
import static org.sonar.ce.task.projectanalysis.component.ReportComponent.DUMB_PROJECT;
import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto;
@@ -105,6 +107,19 @@ public class PostMeasuresComputationChecksStepTest {
}
@Test
+ public void whenOnCheck_thenAnalysisUuidIsPresent() {
+ String analysisUuid = "analysisUuid";
+ PostMeasuresComputationCheck check = mock(PostMeasuresComputationCheck.class);
+ analysisMetadataHolder.setUuid(analysisUuid);
+
+ newStep(check).execute(new TestComputationStepContext());
+
+ ArgumentCaptor<Context> contextArgumentCaptor = ArgumentCaptor.forClass(Context.class);
+ verify(check).onCheck(contextArgumentCaptor.capture());
+ assertThat(contextArgumentCaptor.getValue().getAnalysisUuid()).isEqualTo(analysisUuid);
+ }
+
+ @Test
public void do_nothing_if_no_extensions() {
// no failure
newStep().execute(new TestComputationStepContext());
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderRule.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderRule.java
index f47424267a9..7c9b454e8c2 100644
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderRule.java
+++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderRule.java
@@ -73,4 +73,14 @@ public class PeriodHolderRule implements TestRule, PeriodHolder, AfterEachCallba
return delegate.getPeriod();
}
+ @Override
+ public PeriodOrigin getPeriodOrigin() {
+ return delegate.getPeriodOrigin();
+ }
+
+ public PeriodHolderRule setPeriodOrigin(PeriodOrigin periodOrigin) {
+ delegate.setPeriodOrigin(periodOrigin);
+ return this;
+ }
+
}
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/scanner/ScannerReportReaderImplTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/scanner/ScannerReportReaderImplTest.java
index 6abf97d1ba9..d954d5eb819 100644
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/scanner/ScannerReportReaderImplTest.java
+++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/scanner/ScannerReportReaderImplTest.java
@@ -303,9 +303,9 @@ public class ScannerReportReaderImplTest {
}
@Test
- public void verify_readDependencyFilesZip() throws IOException {
+ public void verify_readDependencyFilesArchive() throws IOException {
File tempDir = tempFolder.newDir();
- File tempFile = new File(tempDir, "dependency-files.zip");
+ File tempFile = new File(tempDir, "dependency-files.tar.xz");
byte[] expectedBytes = "hello world!".getBytes();
try (FileOutputStream fos = new FileOutputStream(tempFile)) {
fos.write(expectedBytes);
@@ -313,8 +313,8 @@ public class ScannerReportReaderImplTest {
writer.writeScaFile(tempFile);
- assertThat(underTest.readDependencyFilesZip()).isNotNull();
- var returnBytes = FileUtils.readFileToByteArray(underTest.readDependencyFilesZip());
+ assertThat(underTest.readDependencyFilesArchive()).isNotNull();
+ var returnBytes = FileUtils.readFileToByteArray(underTest.readDependencyFilesArchive());
assertThat(returnBytes).isEqualTo(expectedBytes);
}
}
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepTest.java
new file mode 100644
index 00000000000..84c3cc6d42d
--- /dev/null
+++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepTest.java
@@ -0,0 +1,194 @@
+/*
+ * 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.ce.task.projectanalysis.step;
+
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mockito.ArgumentCaptor;
+import org.slf4j.event.Level;
+import org.sonar.api.testfixtures.log.LogTesterJUnit5;
+import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
+import org.sonar.ce.task.projectanalysis.analysis.TestBranch;
+import org.sonar.ce.task.projectanalysis.component.Component;
+import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
+import org.sonar.ce.task.projectanalysis.period.Period;
+import org.sonar.ce.task.projectanalysis.period.PeriodHolder;
+import org.sonar.ce.task.projectanalysis.period.PeriodOrigin;
+import org.sonar.ce.task.step.ComputationStep;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.newcodeperiod.NewCodePeriodDao;
+import org.sonar.db.newcodeperiod.NewCodePeriodDto;
+import org.sonar.db.newcodeperiod.NewCodePeriodType;
+import org.sonar.server.project.Project;
+
+import static java.util.Collections.emptyList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.slf4j.event.Level.DEBUG;
+
+class PersistReferenceBranchPeriodStepTest {
+
+ private static final String MAIN_BRANCH = "main";
+ private static final String FEATURE_BRANCH = "feature";
+ private static final String BRANCH_UUID = "branch-uuid";
+ private static final String PROJECT_NAME = "project-name";
+ private static final String PROJECT_UUID = "project-uuid";
+
+ private final PeriodHolder periodHolder = mock(PeriodHolder.class);
+
+ private final AnalysisMetadataHolder analysisMetadataHolder = mock(AnalysisMetadataHolder.class);
+
+ private final DbClient dbClient = mock(DbClient.class);
+
+ private final TreeRootHolder treeRootHolder = mock(TreeRootHolder.class);
+
+ @RegisterExtension
+ private final LogTesterJUnit5 logs = new LogTesterJUnit5().setLevel(Level.DEBUG);
+
+ private final PersistReferenceBranchPeriodStep persistReferenceBranchPeriodStep = new PersistReferenceBranchPeriodStep(
+ periodHolder, analysisMetadataHolder, dbClient, treeRootHolder);
+
+ private final DbSession dbSession = mock(DbSession.class);
+ private final NewCodePeriodDao newCodePeriodeDao = mock(NewCodePeriodDao.class);
+
+ private final ComputationStep.Context context = mock(ComputationStep.Context.class);
+
+ @BeforeEach
+ void setUp() {
+ Project project = new Project(PROJECT_UUID, "project-key", PROJECT_NAME, "project-description", emptyList());
+ when(analysisMetadataHolder.isBranch()).thenReturn(true);
+ when(analysisMetadataHolder.getProject()).thenReturn(project);
+ when(analysisMetadataHolder.getBranch()).thenReturn(new TestBranch(FEATURE_BRANCH));
+
+ when(periodHolder.hasPeriod()).thenReturn(true);
+ Period period = new Period(NewCodePeriodType.REFERENCE_BRANCH.name(), MAIN_BRANCH, null);
+ when(periodHolder.getPeriod()).thenReturn(period);
+ when(periodHolder.getPeriodOrigin()).thenReturn(PeriodOrigin.SCANNER);
+
+ when(dbClient.openSession(false)).thenReturn(dbSession);
+ when(dbClient.newCodePeriodDao()).thenReturn(newCodePeriodeDao);
+
+ Component root = mock(Component.class);
+ when(treeRootHolder.getRoot()).thenReturn(root);
+ when(root.getUuid()).thenReturn(BRANCH_UUID);
+
+ }
+
+ @Test
+ void getDescription() {
+ assertThat(persistReferenceBranchPeriodStep.getDescription()).isEqualTo("Persist or update reference branch new code period");
+ }
+
+ @Test
+ void execute_shouldDoNothing_whenNotABranch() {
+ when(analysisMetadataHolder.isBranch()).thenReturn(false);
+ verifyExecuteNotCalled();
+ }
+
+ @Test
+ void execute_shouldDoNothing_whenNoPeriods() {
+ when(periodHolder.hasPeriod()).thenReturn(false);
+ verifyExecuteNotCalled();
+ }
+
+ @Test
+ void execute_shouldDoNothing_whenNotReferenceBranchPeriod() {
+ Period period = new Period("not-ref-branch", MAIN_BRANCH, null);
+ when(periodHolder.getPeriod()).thenReturn(period);
+ when(periodHolder.getPeriodOrigin()).thenReturn(PeriodOrigin.SETTINGS);
+ verifyExecuteNotCalled();
+ }
+
+ private void verifyExecuteNotCalled() {
+ PersistReferenceBranchPeriodStep spyStep = spy(persistReferenceBranchPeriodStep);
+
+ spyStep.execute(context);
+
+ verify(spyStep, never()).executePersistPeriodStep();
+ }
+
+ @Test
+ void execute_shouldCreateNewCodePeriod_whenItDoesNotExists() {
+ NewCodePeriodDto expectedNewCodePeriod = new NewCodePeriodDto()
+ .setBranchUuid(BRANCH_UUID)
+ .setProjectUuid(PROJECT_UUID)
+ .setType(NewCodePeriodType.REFERENCE_BRANCH)
+ .setValue(MAIN_BRANCH);
+ when(newCodePeriodeDao.selectByBranch(dbSession, PROJECT_UUID, BRANCH_UUID)).thenReturn(Optional.empty());
+
+ persistReferenceBranchPeriodStep.execute(context);
+
+ assertThat(logs.logs(DEBUG)).contains(
+ String.format("Persisting reference branch new code period '%s' for project '%s' and branch '%s'",MAIN_BRANCH, PROJECT_NAME, FEATURE_BRANCH));
+ ArgumentCaptor<NewCodePeriodDto> newCodePeriodCaptor = ArgumentCaptor.forClass(NewCodePeriodDto.class);
+ verify(newCodePeriodeDao).insert(eq(dbSession), newCodePeriodCaptor.capture());
+ assertThat(newCodePeriodCaptor.getValue()).usingRecursiveComparison().isEqualTo(expectedNewCodePeriod);
+ }
+
+ @Test
+ void execute_shouldUpdateNewCodePeriod_whenItExistsAndItChanged() {
+ NewCodePeriodDto expectedNewCodePeriod = new NewCodePeriodDto()
+ .setBranchUuid(BRANCH_UUID)
+ .setProjectUuid(PROJECT_UUID)
+ .setType(NewCodePeriodType.REFERENCE_BRANCH)
+ .setValue(MAIN_BRANCH);
+ var newCodePeriodInBase = new NewCodePeriodDto()
+ .setBranchUuid(BRANCH_UUID)
+ .setProjectUuid(PROJECT_UUID)
+ .setType(NewCodePeriodType.REFERENCE_BRANCH)
+ .setValue("old-value");
+
+ when(newCodePeriodeDao.selectByBranch(dbSession, PROJECT_UUID, BRANCH_UUID)).thenReturn(Optional.of(newCodePeriodInBase));
+
+ persistReferenceBranchPeriodStep.execute(context);
+
+ assertThat(logs.logs(DEBUG)).contains(
+ String.format("Updating reference branch new code period '%s' for project '%s' and branch '%s'", MAIN_BRANCH ,PROJECT_NAME, FEATURE_BRANCH));
+ ArgumentCaptor<NewCodePeriodDto> newCodePeriodCaptor = ArgumentCaptor.forClass(NewCodePeriodDto.class);
+ verify(newCodePeriodeDao).update(eq(dbSession), newCodePeriodCaptor.capture());
+ assertThat(newCodePeriodCaptor.getValue()).usingRecursiveComparison().isEqualTo(expectedNewCodePeriod);
+ }
+
+ @Test
+ void execute_shouldDoNothing_whenItExistsAndItDidNotChanged() {
+ NewCodePeriodDto expectedNewCodePeriod = new NewCodePeriodDto()
+ .setBranchUuid(BRANCH_UUID)
+ .setProjectUuid(PROJECT_UUID)
+ .setType(NewCodePeriodType.REFERENCE_BRANCH)
+ .setValue(MAIN_BRANCH);
+
+ when(newCodePeriodeDao.selectByBranch(dbSession, PROJECT_UUID, BRANCH_UUID)).thenReturn(Optional.of(expectedNewCodePeriod));
+
+ persistReferenceBranchPeriodStep.execute(context);
+
+ verify(newCodePeriodeDao, never()).update(any(), any());
+ verify(newCodePeriodeDao, never()).insert(any(), any());
+ }
+
+}
diff --git a/server/sonar-ce-task-projectanalysis/src/testFixtures/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryRule.java b/server/sonar-ce-task-projectanalysis/src/testFixtures/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryRule.java
index 8a8d9ac2512..0b9ce68bc0f 100644
--- a/server/sonar-ce-task-projectanalysis/src/testFixtures/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryRule.java
+++ b/server/sonar-ce-task-projectanalysis/src/testFixtures/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryRule.java
@@ -100,6 +100,11 @@ public class MetricRepositoryRule extends ExternalResource implements MetricRepo
}
@Override
+ public Optional<Metric> getOptionalByKey(String key) {
+ return Optional.ofNullable(metricsByKey.get(key));
+ }
+
+ @Override
public Metric getByUuid(String uuid) {
Metric res = metricsByUuid.get(uuid);
checkState(res != null, format("No Metric can be found for uuid %s", uuid));
diff --git a/server/sonar-ce-task/src/test/java/org/sonar/ce/task/step/ComputationStepExecutorTest.java b/server/sonar-ce-task/src/test/java/org/sonar/ce/task/step/ComputationStepExecutorTest.java
index 5ed7e5bbba0..ebbd74c6ab4 100644
--- a/server/sonar-ce-task/src/test/java/org/sonar/ce/task/step/ComputationStepExecutorTest.java
+++ b/server/sonar-ce-task/src/test/java/org/sonar/ce/task/step/ComputationStepExecutorTest.java
@@ -103,7 +103,6 @@ public class ComputationStepExecutorTest {
assertThat(stepsTelemetryHolder.getTelemetryMetrics()).containsEntry("prefix.step.foo", "100");
assertThat(stepsTelemetryHolder.getTelemetryMetrics()).containsEntry("prefix.step.bar", "20");
List<String> infoLogs = logTester.logs(Level.INFO);
- System.out.println("infoLogs = " + infoLogs);
assertThat(infoLogs).hasSize(1);
assertThat(infoLogs.get(0)).contains("Step | step.foo=100 | step.bar=20 | status=SUCCESS | time=");
}
diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java
index b2ac7a8746a..58af306669b 100644
--- a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java
+++ b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java
@@ -117,6 +117,7 @@ import org.sonar.server.log.ServerLogging;
import org.sonar.server.measure.index.ProjectMeasuresIndexer;
import org.sonar.server.metric.IssueCountMetrics;
import org.sonar.server.metric.UnanalyzedLanguageMetrics;
+import org.sonar.server.network.NetworkInterfaceProvider;
import org.sonar.server.notification.DefaultNotificationManager;
import org.sonar.server.notification.NotificationService;
import org.sonar.server.notification.email.EmailNotificationChannel;
@@ -288,6 +289,8 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer {
Paths2Impl.getInstance(),
Clock.systemDefaultZone(),
+ NetworkInterfaceProvider.class,
+
// DB
new DaoModule(),
DBSessionsImpl.class,
diff --git a/server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java b/server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java
index 8f54baa76c7..40981cd7d13 100644
--- a/server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java
+++ b/server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java
@@ -108,9 +108,16 @@ public final class SqTables {
"rules_parameters",
"rules_profiles",
"rule_repositories",
+ "sca_analyses",
"sca_dependencies",
+ "sca_encountered_licenses",
"sca_issues",
"sca_issues_releases",
+ "sca_issue_rels_changes",
+ "sca_lic_prof_categories",
+ "sca_lic_prof_customs",
+ "sca_lic_prof_projects",
+ "sca_license_profiles",
"sca_releases",
"sca_vulnerability_issues",
"scanner_analysis_cache",
diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/alm/setting/ProjectAlmSettingDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/alm/setting/ProjectAlmSettingDaoIT.java
index 0ece24490e0..51585695dbc 100644
--- a/server/sonar-db-dao/src/it/java/org/sonar/db/alm/setting/ProjectAlmSettingDaoIT.java
+++ b/server/sonar-db-dao/src/it/java/org/sonar/db/alm/setting/ProjectAlmSettingDaoIT.java
@@ -92,23 +92,16 @@ class ProjectAlmSettingDaoIT {
@Test
void select_by_alm_setting_and_slugs() {
- when(uuidFactory.create()).thenReturn(A_UUID);
AlmSettingDto almSettingsDto = db.almSettings().insertBitbucketAlmSetting();
- ProjectDto project = db.components().insertPrivateProject().getProjectDto();
- ProjectAlmSettingDto bitbucketProjectAlmSettingDto = newBitbucketProjectAlmSettingDto(almSettingsDto, project);
- bitbucketProjectAlmSettingDto.setAlmSlug("slug1");
- underTest.insertOrUpdate(dbSession, bitbucketProjectAlmSettingDto, almSettingsDto.getKey(), project.getName(), project.getKey());
- ProjectAlmSettingDto bitbucketProjectAlmSettingDto2 = newBitbucketProjectAlmSettingDto(almSettingsDto,
- db.components().insertPrivateProject().getProjectDto());
- bitbucketProjectAlmSettingDto2.setAlmSlug("slug2");
- when(uuidFactory.create()).thenReturn(A_UUID + 1);
- underTest.insertOrUpdate(dbSession, bitbucketProjectAlmSettingDto2, almSettingsDto.getKey(), project.getName(), project.getKey());
-
- Set<String> slugs = new HashSet<>();
- slugs.add("slug1");
- assertThat(underTest.selectByAlmSettingAndSlugs(dbSession, almSettingsDto, slugs))
+ ProjectAlmSettingDto matchingProject = createBitbucketProject(almSettingsDto, dto -> dto.setAlmSlug("slug1"));
+ createBitbucketProject(almSettingsDto, dto -> dto.setAlmSlug("slug2"));
+
+ Set<String> slugs = Set.of("slug1");
+ List<ProjectAlmSettingDto> results = underTest.selectByAlmSettingAndSlugs(dbSession, almSettingsDto, slugs);
+
+ assertThat(results)
.extracting(ProjectAlmSettingDto::getProjectUuid, ProjectAlmSettingDto::getSummaryCommentEnabled, ProjectAlmSettingDto::getInlineAnnotationsEnabled)
- .containsExactly(tuple(project.getUuid(), bitbucketProjectAlmSettingDto2.getSummaryCommentEnabled(), bitbucketProjectAlmSettingDto2.getInlineAnnotationsEnabled()));
+ .containsExactly(tuple(matchingProject.getProjectUuid(), matchingProject.getSummaryCommentEnabled(), matchingProject.getInlineAnnotationsEnabled()));
}
@Test
@@ -142,12 +135,7 @@ class ProjectAlmSettingDaoIT {
}
private ProjectAlmSettingDto createAlmProject(AlmSettingDto almSettingsDto, Consumer<ProjectAlmSettingDto>... populators) {
- ProjectDto project = db.components().insertPrivateProject().getProjectDto();
- when(uuidFactory.create()).thenReturn(project.getUuid() + "_set");
- ProjectAlmSettingDto projectAlmSettingDto = newGithubProjectAlmSettingDto(almSettingsDto, project);
- stream(populators).forEach(p -> p.accept(projectAlmSettingDto));
- underTest.insertOrUpdate(dbSession, projectAlmSettingDto, almSettingsDto.getKey(), project.getName(), project.getKey());
- return projectAlmSettingDto;
+ return createAlmProject(almSettingsDto, dto -> stream(populators).forEach(p -> p.accept(dto)), project -> newGithubProjectAlmSettingDto(almSettingsDto, project));
}
@Test
@@ -160,23 +148,16 @@ class ProjectAlmSettingDaoIT {
@Test
void select_by_alm_setting_and_repos() {
- when(uuidFactory.create()).thenReturn(A_UUID);
AlmSettingDto almSettingsDto = db.almSettings().insertGitHubAlmSetting();
- ProjectDto project = db.components().insertPrivateProject().getProjectDto();
- ProjectAlmSettingDto githubProjectAlmSettingDto = newGithubProjectAlmSettingDto(almSettingsDto, project);
- githubProjectAlmSettingDto.setAlmRepo("repo1");
- underTest.insertOrUpdate(dbSession, githubProjectAlmSettingDto, almSettingsDto.getKey(), project.getName(), project.getKey());
- ProjectAlmSettingDto githubProjectAlmSettingDto2 = newGithubProjectAlmSettingDto(almSettingsDto,
- db.components().insertPrivateProject().getProjectDto());
- githubProjectAlmSettingDto2.setAlmRepo("repo2");
- when(uuidFactory.create()).thenReturn(A_UUID + 1);
- underTest.insertOrUpdate(dbSession, githubProjectAlmSettingDto2, almSettingsDto.getKey(), project.getName(), project.getKey());
-
- Set<String> repos = new HashSet<>();
- repos.add("repo1");
- assertThat(underTest.selectByAlmSettingAndRepos(dbSession, almSettingsDto, repos))
+ ProjectAlmSettingDto matchingProject = createAlmProject(almSettingsDto, dto -> dto.setAlmRepo("repo1"));
+ createAlmProject(almSettingsDto, dto -> dto.setAlmRepo("repo2"));
+
+ Set<String> repos = Set.of("repo1");
+ List<ProjectAlmSettingDto> results = underTest.selectByAlmSettingAndRepos(dbSession, almSettingsDto, repos);
+
+ assertThat(results)
.extracting(ProjectAlmSettingDto::getProjectUuid, ProjectAlmSettingDto::getSummaryCommentEnabled)
- .containsExactly(tuple(project.getUuid(), githubProjectAlmSettingDto.getSummaryCommentEnabled()));
+ .containsExactly(tuple(matchingProject.getProjectUuid(), matchingProject.getSummaryCommentEnabled()));
}
@Test
@@ -208,8 +189,8 @@ class ProjectAlmSettingDaoIT {
ProjectAlmKeyAndProject::getProjectUuid,
ProjectAlmKeyAndProject::getAlmId,
ProjectAlmKeyAndProject::getUrl,
- ProjectAlmKeyAndProject::getMonorepo
- ).containsExactlyInAnyOrder(
+ ProjectAlmKeyAndProject::getMonorepo)
+ .containsExactlyInAnyOrder(
tuple(project1.getUuid(), almSettingsDto.getAlm().getId(), almSettingsDto.getUrl(), false),
tuple(project2.getUuid(), almSettingsDto.getAlm().getId(), almSettingsDto.getUrl(), true));
}
@@ -243,18 +224,20 @@ class ProjectAlmSettingDaoIT {
AlmSettingDto matchingAlmSettingDto = db.almSettings().insertGitHubAlmSetting();
AlmSettingDto notMatchingAlmSettingDto = db.almSettings().insertGitHubAlmSetting();
ProjectAlmSettingDto matchingRepo = createAlmProject(matchingAlmSettingDto, dto -> dto.setAlmRepo("matchingRepo"));
- ProjectAlmSettingDto notMatchingRepo = createAlmProject(matchingAlmSettingDto, dto -> dto.setAlmRepo("whatever"));
ProjectAlmSettingDto matchingAlmSetting = createAlmProject(matchingAlmSettingDto, dto -> dto.setAlmRepo("matchingRepo"));
- ProjectAlmSettingDto notMatchingAlmSetting = createAlmProject(notMatchingAlmSettingDto, dto -> dto.setAlmRepo("matchingRepo"));
+ createAlmProject(matchingAlmSettingDto, dto -> dto.setAlmRepo("whatever")); // not matching repo
+ createAlmProject(notMatchingAlmSettingDto, dto -> dto.setAlmRepo("matchingRepo")); // not matching alm setting
- List<ProjectAlmSettingDto> dtos = underTest.selectProjectAlmSettings(dbSession, new ProjectAlmSettingQuery("matchingRepo", matchingAlmSettingDto.getUuid()), 1, 100);
- assertThat(dtos)
+ ProjectAlmSettingQuery query = new ProjectAlmSettingQuery("matchingRepo", matchingAlmSettingDto.getUuid());
+ List<ProjectAlmSettingDto> results = underTest.selectProjectAlmSettings(dbSession, query, 1, 100);
+
+ assertThat(results)
.usingRecursiveFieldByFieldElementComparator()
.containsExactlyInAnyOrder(matchingRepo, matchingAlmSetting);
}
private static Object[][] paginationTestCases() {
- return new Object[][]{
+ return new Object[][] {
{100, 1, 5},
{100, 3, 18},
{2075, 41, 50},
@@ -265,7 +248,7 @@ class ProjectAlmSettingDaoIT {
@ParameterizedTest
@MethodSource("paginationTestCases")
void selectProjectAlmSettings_whenUsingPagination_findsTheRightResults(int numberToGenerate, int offset, int limit) {
- when(uuidFactory.create()).thenAnswer(answer -> UUID.randomUUID().toString());
+ when(uuidFactory.create()).thenAnswer(answer -> UUID.randomUUID().toString());
Map<String, ProjectAlmSettingDto> allProjectAlmSettingsDtos = generateProjectAlmSettingsDtos(numberToGenerate);
@@ -284,10 +267,10 @@ class ProjectAlmSettingDaoIT {
}
Map<String, ProjectAlmSettingDto> result = IntStream.range(1000, 1000 + numberToGenerate)
.mapToObj(i -> underTest.insertOrUpdate(dbSession, new ProjectAlmSettingDto()
- .setAlmRepo("repo_" + i)
- .setAlmSettingUuid("almSettingUuid_" + i)
- .setProjectUuid("projectUuid_" + i)
- .setMonorepo(false),
+ .setAlmRepo("repo_" + i)
+ .setAlmSettingUuid("almSettingUuid_" + i)
+ .setProjectUuid("projectUuid_" + i)
+ .setMonorepo(false),
"key_" + i, "projectName_" + i, "projectKey_" + i))
.collect(toMap(ProjectAlmSettingDto::getAlmRepo, Function.identity()));
db.commit();
@@ -384,4 +367,83 @@ class ProjectAlmSettingDaoIT {
assertThat(underTest.countByAlmSetting(dbSession, githubAlmSetting1)).isOne();
}
+ @Test
+ void selectProjectAlmSettings_whenSearchingByAlmRepo_returnsMatchingResults() {
+ AlmSettingDto almSetting = db.almSettings().insertGitHubAlmSetting();
+ ProjectAlmSettingDto matchingProject = createAlmProject(almSetting, dto -> dto.setAlmRepo("target-repo"));
+ createAlmProject(almSetting, dto -> dto.setAlmRepo("other-repo"));
+ createAlmProject(almSetting, dto -> dto.setAlmSlug("target-repo")); // slug should not match
+
+ ProjectAlmSettingQuery query = ProjectAlmSettingQuery.forAlmRepo("target-repo");
+ List<ProjectAlmSettingDto> results = underTest.selectProjectAlmSettings(dbSession, query, 1, 100);
+
+ assertThat(results)
+ .usingRecursiveFieldByFieldElementComparator()
+ .containsExactly(matchingProject);
+ }
+
+ @Test
+ void selectProjectAlmSettings_whenSearchingByAlmRepoAndSlug_returnsMatchingResults() {
+ AlmSettingDto almSetting = db.almSettings().insertAzureAlmSetting();
+ ProjectAlmSettingDto matchingProject = createAzureProject(almSetting, "target-repo", "target-project");
+ createAzureProject(almSetting, "target-repo", "other-project"); // different slug
+ createAzureProject(almSetting, "other-repo", "target-project"); // different repo
+ createAlmProject(almSetting, dto -> dto.setAlmRepo("target-repo")); // missing slug
+
+ ProjectAlmSettingQuery query = ProjectAlmSettingQuery.forAlmRepoAndSlug("target-repo", "target-project");
+ List<ProjectAlmSettingDto> results = underTest.selectProjectAlmSettings(dbSession, query, 1, 100);
+
+ assertThat(results)
+ .usingRecursiveFieldByFieldElementComparator()
+ .containsExactly(matchingProject);
+ }
+
+ @Test
+ void selectProjectAlmSettings_whenSearchingByCaseInsensitiveAlmRepo_returnsMatchingResults() {
+ AlmSettingDto almSetting = db.almSettings().insertGitHubAlmSetting();
+ ProjectAlmSettingDto upperCaseProject = createAlmProject(almSetting, dto -> dto.setAlmRepo("TARGET-REPO"));
+ ProjectAlmSettingDto lowerCaseProject = createAlmProject(almSetting, dto -> dto.setAlmRepo("target-repo"));
+ createAlmProject(almSetting, dto -> dto.setAlmRepo("other-repo"));
+
+ ProjectAlmSettingQuery query = ProjectAlmSettingQuery.forAlmRepo("Target-Repo");
+ List<ProjectAlmSettingDto> results = underTest.selectProjectAlmSettings(dbSession, query, 1, 100);
+
+ assertThat(results)
+ .usingRecursiveFieldByFieldElementComparator()
+ .containsExactlyInAnyOrder(upperCaseProject, lowerCaseProject);
+ }
+
+ @Test
+ void countProjectAlmSettings_whenSearchingByAlmRepo_returnsCorrectCount() {
+ AlmSettingDto almSetting = db.almSettings().insertGitHubAlmSetting();
+ createAlmProject(almSetting, dto -> dto.setAlmRepo("target-repo"));
+ createAlmProject(almSetting, dto -> dto.setAlmRepo("target-repo"));
+ createAlmProject(almSetting, dto -> dto.setAlmRepo("other-repo"));
+
+ ProjectAlmSettingQuery query = ProjectAlmSettingQuery.forAlmRepo("target-repo");
+ int count = underTest.countProjectAlmSettings(dbSession, query);
+
+ assertThat(count).isEqualTo(2);
+ }
+
+ private ProjectAlmSettingDto createAzureProject(AlmSettingDto almSettingsDto, String almRepo, String almSlug) {
+ return createAlmProject(almSettingsDto, dto -> {
+ dto.setAlmRepo(almRepo);
+ dto.setAlmSlug(almSlug);
+ }, project -> newAzureProjectAlmSettingDto(almSettingsDto, project));
+ }
+
+ private ProjectAlmSettingDto createBitbucketProject(AlmSettingDto almSettingsDto, Consumer<ProjectAlmSettingDto> customizer) {
+ return createAlmProject(almSettingsDto, customizer, project -> newBitbucketProjectAlmSettingDto(almSettingsDto, project));
+ }
+
+ private ProjectAlmSettingDto createAlmProject(AlmSettingDto almSettingsDto, Consumer<ProjectAlmSettingDto> customizer, Function<ProjectDto, ProjectAlmSettingDto> dtoFactory) {
+ ProjectDto project = db.components().insertPrivateProject().getProjectDto();
+ when(uuidFactory.create()).thenReturn(project.getUuid() + "_set");
+ ProjectAlmSettingDto projectAlmSettingDto = dtoFactory.apply(project);
+ customizer.accept(projectAlmSettingDto);
+ underTest.insertOrUpdate(dbSession, projectAlmSettingDto, almSettingsDto.getKey(), project.getName(), project.getKey());
+ return projectAlmSettingDto;
+ }
+
}
diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/component/BranchDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/component/BranchDaoIT.java
index 2b1cb099fca..feae0d3f20c 100644
--- a/server/sonar-db-dao/src/it/java/org/sonar/db/component/BranchDaoIT.java
+++ b/server/sonar-db-dao/src/it/java/org/sonar/db/component/BranchDaoIT.java
@@ -906,6 +906,41 @@ class BranchDaoIT {
tuple(branch1.getUuid(), projectData1.projectUuid(), true));
}
+ @Test
+ void selectPullRequestsTargetingBranch() {
+ BranchDto mainBranch = new BranchDto();
+ mainBranch.setProjectUuid("U1");
+ mainBranch.setUuid("U1");
+ mainBranch.setIsMain(true);
+ mainBranch.setBranchType(BranchType.BRANCH);
+ mainBranch.setKey("master");
+ underTest.insert(dbSession, mainBranch);
+
+ BranchDto prBranch = new BranchDto();
+ prBranch.setProjectUuid("U1");
+ prBranch.setUuid("U2");
+ prBranch.setIsMain(false);
+ prBranch.setBranchType(PULL_REQUEST);
+ prBranch.setKey("1234");
+ prBranch.setMergeBranchUuid("U1");
+ underTest.insert(dbSession, prBranch);
+
+ // make a second PR also targeting main branch
+ prBranch.setUuid("U3");
+ prBranch.setKey("4321");
+ prBranch.setMergeBranchUuid("U1");
+ underTest.insert(dbSession, prBranch);
+
+ // make a third PR NOT targeting main branch to be sure we filter it out
+ prBranch.setUuid("U4");
+ prBranch.setKey("5678");
+ prBranch.setMergeBranchUuid("U42");
+ underTest.insert(dbSession, prBranch);
+
+ var result = underTest.selectPullRequestsTargetingBranch(dbSession, "U1", "U1");
+ assertThat(result.stream().map(BranchDto::getUuid).toList()).containsExactlyInAnyOrder("U2", "U3");
+ }
+
private void insertBranchesForProjectUuids(boolean mainBranch, String... uuids) {
for (String uuid : uuids) {
BranchDto dto = new BranchDto();
diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java
index 58b21ee2c96..f468306e8f8 100644
--- a/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java
+++ b/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java
@@ -46,7 +46,6 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.slf4j.event.Level;
import org.sonar.api.issue.Issue;
-import org.sonar.db.component.ComponentQualifiers;
import org.sonar.api.testfixtures.log.LogAndArguments;
import org.sonar.api.testfixtures.log.LogTesterJUnit5;
import org.sonar.api.utils.System2;
@@ -69,6 +68,7 @@ import org.sonar.db.component.BranchDto;
import org.sonar.db.component.BranchType;
import org.sonar.db.component.ComponentDbTester;
import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ComponentQualifiers;
import org.sonar.db.component.ComponentTesting;
import org.sonar.db.component.ProjectData;
import org.sonar.db.component.SnapshotDto;
@@ -1980,26 +1980,51 @@ oldCreationDate));
"license_expression", "MIT",
"known", true,
"known_package", true,
+ "is_new", false,
"created_at", 0L, "updated_at", 0L);
db.executeInsert("sca_releases", merge(releaseBase, Map.of("uuid", "release-uuid1", "component_uuid", branch1Uuid)));
db.executeInsert("sca_releases", merge(releaseBase, Map.of("uuid", "release-uuid2", "component_uuid", branch2Uuid)));
assertThat(db.countRowsOfTable(dbSession, "sca_releases")).isEqualTo(2);
var dependencyBase = Map.of("created_at", 0L, "updated_at", 0L,
- "direct", true, "scope", "compile", "new_in_pull_request", true);
+ "direct", true, "scope", "compile", "is_new", true);
db.executeInsert("sca_dependencies", merge(dependencyBase, Map.of("uuid", "dependency-uuid1", "sca_release_uuid", "release-uuid1")));
db.executeInsert("sca_dependencies", merge(dependencyBase, Map.of("uuid", "dependency-uuid2", "sca_release_uuid", "release-uuid2")));
assertThat(db.countRowsOfTable(dbSession, "sca_dependencies")).isEqualTo(2);
// the issue uuids here don't even exist but doesn't matter, we don't delete issues so not testing that
var issueReleaseBase = Map.of("created_at", 0L, "updated_at", 0L,
- "severity", "INFO", "severity_sort_key", 42);
+ "severity", "INFO", "original_severity", "INFO", "manual_severity", false,
+ "severity_sort_key", 42, "status", "TO_REVIEW");
db.executeInsert("sca_issues_releases", merge(issueReleaseBase, Map.of("uuid", "issue-release-uuid1",
"sca_issue_uuid", "issue-uuid1", "sca_release_uuid", "release-uuid1")));
db.executeInsert("sca_issues_releases", merge(issueReleaseBase, Map.of("uuid", "issue-release-uuid2",
"sca_issue_uuid", "issue-uuid2", "sca_release_uuid", "release-uuid2")));
assertThat(db.countRowsOfTable(dbSession, "sca_issues_releases")).isEqualTo(2);
+
+ var issueReleaseChangeBase = Map.of("created_at", 0L, "updated_at", 0L);
+ db.executeInsert("sca_issue_rels_changes", merge(issueReleaseChangeBase, Map.of("uuid", "issue-release-change-uuid1",
+ "sca_issues_releases_uuid", "issue-release-uuid1")));
+ db.executeInsert("sca_issue_rels_changes", merge(issueReleaseChangeBase, Map.of("uuid", "issue-release-change-uuid2",
+ "sca_issues_releases_uuid", "issue-release-uuid2")));
+
+ assertThat(db.countRowsOfTable(dbSession, "sca_issue_rels_changes")).isEqualTo(2);
+
+ var analysisBase = Map.of(
+ "created_at", 0L,
+ "updated_at", 0L,
+ "status", "COMPLETED",
+ "errors", "[]",
+ "parsed_files", "[]",
+ "failed_reason", "something");
+ db.executeInsert("sca_analyses", merge(analysisBase, Map.of(
+ "uuid", "analysis-uuid1",
+ "component_uuid", branch1Uuid)));
+ db.executeInsert("sca_analyses", merge(analysisBase, Map.of(
+ "uuid", "analysis-uuid2",
+ "component_uuid", branch2Uuid)));
+ assertThat(db.countRowsOfTable(dbSession, "sca_analyses")).isEqualTo(2);
}
@Test
@@ -2015,6 +2040,49 @@ oldCreationDate));
assertThat(db.countRowsOfTable(dbSession, "sca_releases")).isEqualTo(1);
assertThat(db.countRowsOfTable(dbSession, "sca_dependencies")).isEqualTo(1);
assertThat(db.countRowsOfTable(dbSession, "sca_issues_releases")).isEqualTo(1);
+ assertThat(db.countRowsOfTable(dbSession, "sca_issue_rels_changes")).isEqualTo(1);
+ assertThat(db.countRowsOfTable(dbSession, "sca_analyses")).isEqualTo(1);
+ }
+
+ @Test
+ void deleteProject_purgesScaLicenseProfiles() {
+ ProjectDto project = db.components().insertPublicProject().getProjectDto();
+
+ var scaLicenseProfileProjectBase = Map.of(
+ "sca_license_profile_uuid", "sca-license-profile-uuid1",
+ "created_at", 0L,
+ "updated_at", 0L);
+
+ db.executeInsert("sca_lic_prof_projects", merge(scaLicenseProfileProjectBase, Map.of(
+ "uuid", "sca-lic-prof-project-uuid1",
+ "project_uuid", project.getUuid())));
+
+ db.executeInsert("sca_lic_prof_projects", merge(scaLicenseProfileProjectBase, Map.of(
+ "uuid", "sca-lic-prof-project-uuid2",
+ "project_uuid", "other-project-uuid")));
+
+ assertThat(db.countRowsOfTable(dbSession, "sca_lic_prof_projects")).isEqualTo(2);
+
+ underTest.deleteProject(dbSession, project.getUuid(), project.getQualifier(), project.getName(), project.getKey());
+
+ assertThat(db.countRowsOfTable(dbSession, "sca_lic_prof_projects")).isEqualTo(1);
+ }
+
+ @Test
+ void whenDeleteBranch_thenPurgeArchitectureGraphs() {
+ ProjectDto project = db.components().insertPublicProject().getProjectDto();
+ BranchDto branch1 = db.components().insertProjectBranch(project);
+ BranchDto branch2 = db.components().insertProjectBranch(project);
+
+ db.executeInsert("architecture_graphs", Map.of("uuid", "12345", "branch_uuid", branch1.getUuid(), "ecosystem", "xoo", "type", "file_graph", "graph_data", "{}"));
+ db.executeInsert("architecture_graphs", Map.of("uuid", "123456", "branch_uuid", branch1.getUuid(), "ecosystem", "xoo", "type", "class_graph", "graph_data", "{}"));
+ db.executeInsert("architecture_graphs", Map.of("uuid", "1234567", "branch_uuid", branch2.getUuid(), "ecosystem", "xoo", "type", "file_graph", "graph_data", "{}"));
+
+ assertThat(db.countRowsOfTable(dbSession, "architecture_graphs")).isEqualTo(3);
+ underTest.deleteBranch(dbSession, branch1.getUuid());
+ assertThat(db.countRowsOfTable(dbSession, "architecture_graphs")).isEqualTo(1);
+ underTest.deleteBranch(dbSession, branch2.getUuid());
+ assertThat(db.countRowsOfTable(dbSession, "architecture_graphs")).isZero();
}
private AnticipatedTransitionDto getAnticipatedTransitionsDto(String uuid, String projectUuid, Date creationDate) {
diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/report/RegulatoryReportDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/report/RegulatoryReportDaoIT.java
index 3b3d9ca49cc..88440d187a1 100644
--- a/server/sonar-db-dao/src/it/java/org/sonar/db/report/RegulatoryReportDaoIT.java
+++ b/server/sonar-db-dao/src/it/java/org/sonar/db/report/RegulatoryReportDaoIT.java
@@ -90,7 +90,7 @@ class RegulatoryReportDaoIT {
List<IssueFindingDto> issues = new ArrayList<>();
underTest.scrollIssues(db.getSession(), PROJECT_UUID, result -> issues.add(result.getResultObject()));
- assertThat(issues).extracting(IssueFindingDto::getKey).containsOnly(issue1.getKey(), issue2.getKey(), issue3.getKey());
+ assertThat(issues).extracting(IssueFindingDto::getKey).containsOnly(issue1.getKey(), issue2.getKey(), issue3.getKey(), issueCodeSmell.getKey());
// check fields
IssueFindingDto issue = issues.stream().filter(i -> i.getKey().equals(issue1.getKey())).findFirst().get();
diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/user/GroupDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/user/GroupDaoIT.java
index 9b52c0d7264..16e86bcd2e5 100644
--- a/server/sonar-db-dao/src/it/java/org/sonar/db/user/GroupDaoIT.java
+++ b/server/sonar-db-dao/src/it/java/org/sonar/db/user/GroupDaoIT.java
@@ -223,6 +223,47 @@ class GroupDaoIT {
}
@Test
+ void findByQuery_withUserId_countAndFindExpectedUsers() {
+ GroupDto group1 = db.users().insertGroup("sonar-users");
+ GroupDto group2 = db.users().insertGroup("SONAR-ADMINS");
+ GroupDto group3 = db.users().insertGroup("customers-group1");
+
+ UserDto user = db.users().insertUser();
+ UserDto user2 = db.users().insertUser();
+ db.users().insertMember(group1, user);
+ db.users().insertMember(group2, user);
+ db.users().insertMember(group2, user2);
+ db.users().insertMember(group3, user2);
+
+ GroupQuery groupQueryIncludingUser = GroupQuery.builder().userId(user.getUuid()).build();
+
+ assertThat(underTest.countByQuery(dbSession, groupQueryIncludingUser)).isEqualTo(2);
+ assertThat(underTest.selectByQuery(dbSession, groupQueryIncludingUser, 1, 100))
+ .extracting(GroupDto::getUuid)
+ .containsExactlyInAnyOrder(group1.getUuid(), group2.getUuid());
+ }
+
+ @Test
+ void findByQuery_withExcludedUserId_countAndFindExpectedUsers() {
+ GroupDto group1 = db.users().insertGroup("sonar-users");
+ GroupDto group2 = db.users().insertGroup("SONAR-ADMINS");
+ GroupDto group3 = db.users().insertGroup("customers-group1");
+
+ UserDto user = db.users().insertUser();
+ UserDto user2 = db.users().insertUser();
+ db.users().insertMember(group1, user);
+ db.users().insertMember(group2, user);
+ db.users().insertMember(group2, user2);
+ db.users().insertMember(group3, user2);
+
+ GroupQuery groupQueryExcludingUser = GroupQuery.builder().excludedUserId(user.getUuid()).build();
+ assertThat(underTest.countByQuery(dbSession, groupQueryExcludingUser)).isEqualTo(1);
+ assertThat(underTest.selectByQuery(dbSession, groupQueryExcludingUser, 1, 100))
+ .extracting(GroupDto::getUuid)
+ .containsExactlyInAnyOrder(group3.getUuid());
+ }
+
+ @Test
void deleteByUuid() {
db.getDbClient().groupDao().insert(dbSession, aGroup);
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingDto.java
index 95f83df37d7..2d0e5766eb9 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingDto.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingDto.java
@@ -19,6 +19,7 @@
*/
package org.sonar.db.alm.setting;
+import java.util.Objects;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
@@ -165,5 +166,21 @@ public class ProjectAlmSettingDto {
this.createdAt = createdAt;
}
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ProjectAlmSettingDto that = (ProjectAlmSettingDto) o;
+ return Objects.equals(uuid, that.uuid);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(uuid);
+ }
}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingQuery.java b/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingQuery.java
index ea3d8fd1b47..54c7f6621f5 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingQuery.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingQuery.java
@@ -21,6 +21,73 @@ package org.sonar.db.alm.setting;
import javax.annotation.Nullable;
-public record ProjectAlmSettingQuery(@Nullable String repository, @Nullable String almSettingUuid
-) {
+public record ProjectAlmSettingQuery(
+ @Nullable String repository,
+ @Nullable String almSettingUuid,
+ @Nullable String almRepo,
+ @Nullable String almSlug) {
+
+ // Existing constructor for backward compatibility (repository search in both alm_repo and alm_slug)
+ public ProjectAlmSettingQuery(String repository, String almSettingUuid) {
+ this(repository, almSettingUuid, null, null);
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ private String repository;
+ private String almSettingUuid;
+ private String almRepo;
+ private String almSlug;
+
+ private Builder() {
+ }
+
+ public Builder repository(String repository) {
+ if (almRepo != null || almSlug != null) {
+ throw new IllegalStateException("Cannot use repository with almRepo or almSlug");
+ }
+ this.repository = repository;
+ return this;
+ }
+
+ public Builder almSettingUuid(String almSettingUuid) {
+ if (almRepo != null || almSlug != null) {
+ throw new IllegalStateException("Cannot use almSettingUuid with almRepo or almSlug");
+ }
+ this.almSettingUuid = almSettingUuid;
+ return this;
+ }
+
+ public Builder almRepo(String almRepo) {
+ if (repository != null || almSettingUuid != null) {
+ throw new IllegalStateException("Cannot use almRepo with repository or almSettingUuid");
+ }
+ this.almRepo = almRepo;
+ return this;
+ }
+
+ public Builder almSlug(String almSlug) {
+ if (repository != null || almSettingUuid != null) {
+ throw new IllegalStateException("Cannot use almSlug with repository or almSettingUuid");
+ }
+ this.almSlug = almSlug;
+ return this;
+ }
+
+ public ProjectAlmSettingQuery build() {
+ return new ProjectAlmSettingQuery(repository, almSettingUuid, almRepo, almSlug);
+ }
+ }
+
+ public static ProjectAlmSettingQuery forAlmRepo(String almRepo) {
+ return builder().almRepo(almRepo).build();
+ }
+
+ public static ProjectAlmSettingQuery forAlmRepoAndSlug(String almRepo, String almSlug) {
+ return builder().almRepo(almRepo).almSlug(almSlug).build();
+ }
+
}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java
index 1f72e4a4002..faa712d555d 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java
@@ -214,4 +214,8 @@ public class BranchDao implements Dao {
public List<BranchDto> selectMainBranchesAssociatedToDefaultQualityProfile(DbSession dbSession) {
return mapper(dbSession).selectMainBranchesAssociatedToDefaultQualityProfile();
}
+
+ public List<BranchDto> selectPullRequestsTargetingBranch(DbSession dbSession, String projectUuid, String branchUuid) {
+ return mapper(dbSession).selectPullRequestsTargetingBranch(projectUuid, branchUuid);
+ }
}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java
index 04c4642834f..dbd11313580 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java
@@ -81,4 +81,6 @@ public interface BranchMapper {
List<BranchDto> selectMainBranches();
List<BranchDto> selectMainBranchesAssociatedToDefaultQualityProfile();
+
+ List<BranchDto> selectPullRequestsTargetingBranch(@Param("projectUuid") String projectUuid, @Param("branchUuid") String branchUuid);
}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeCommands.java b/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeCommands.java
index 230d9aff010..fca991f9f28 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeCommands.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeCommands.java
@@ -510,6 +510,13 @@ class PurgeCommands {
profiler.stop();
}
+ public void deleteArchitectureGraphs(String branchUuid) {
+ profiler.start("deleteArchitectureGraphs (architecture_graphs)");
+ purgeMapper.deleteArchitectureGraphsByBranchUuid(branchUuid);
+ session.commit();
+ profiler.stop();
+ }
+
public void deleteAnticipatedTransitions(String projectUuid, long createdAt) {
profiler.start("deleteAnticipatedTransitions (anticipated_transitions)");
purgeMapper.deleteAnticipatedTransitionsByProjectUuidAndCreationDate(projectUuid, createdAt);
@@ -525,11 +532,24 @@ class PurgeCommands {
}
public void deleteScaActivity(String componentUuid) {
+ // delete sca_analyses first since it sort of marks the analysis as valid/existing
+ profiler.start("deleteScaAnalyses (sca_analyses)");
+ purgeMapper.deleteScaAnalysesByComponentUuid(componentUuid);
+ session.commit();
+ profiler.stop();
+
profiler.start("deleteScaDependencies (sca_dependencies)");
purgeMapper.deleteScaDependenciesByComponentUuid(componentUuid);
session.commit();
profiler.stop();
+ // this must be done before deleting sca_issues_releases or we won't
+ // be able to find the rows
+ profiler.start("deleteScaIssuesReleasesChanges (sca_issue_rels_changes)");
+ purgeMapper.deleteScaIssuesReleasesChangesByComponentUuid(componentUuid);
+ session.commit();
+ profiler.stop();
+
profiler.start("deleteScaIssuesReleases (sca_issues_releases)");
purgeMapper.deleteScaIssuesReleasesByComponentUuid(componentUuid);
session.commit();
@@ -542,4 +562,10 @@ class PurgeCommands {
session.commit();
profiler.stop();
}
+
+ public void deleteScaLicenseProfiles(String projectUuid) {
+ profiler.start("deleteScaLicenseProfileProjects (sca_lic_prof_projects)");
+ purgeMapper.deleteScaLicenseProfileProjectsByProjectUuid(projectUuid);
+ profiler.stop();
+ }
}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeDao.java
index ce5e0cf5e70..ff34ce5783d 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeDao.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeDao.java
@@ -281,6 +281,7 @@ public class PurgeDao implements Dao {
commands.deleteReportSubscriptions(branchUuid);
commands.deleteIssuesFixed(branchUuid);
commands.deleteScaActivity(branchUuid);
+ commands.deleteArchitectureGraphs(branchUuid);
}
private static void deleteProject(String projectUuid, PurgeMapper mapper, PurgeCommands commands) {
@@ -313,6 +314,7 @@ public class PurgeDao implements Dao {
commands.deleteOutdatedProperties(projectUuid);
commands.deleteReportSchedules(projectUuid);
commands.deleteReportSubscriptions(projectUuid);
+ commands.deleteScaLicenseProfiles(projectUuid);
}
/**
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeMapper.java
index 5ca08a12d7a..ab4b369aef6 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeMapper.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeMapper.java
@@ -195,9 +195,17 @@ public interface PurgeMapper {
void deleteIssuesFixedByBranchUuid(@Param("branchUuid") String branchUuid);
+ void deleteScaAnalysesByComponentUuid(@Param("componentUuid") String componentUuid);
+
void deleteScaDependenciesByComponentUuid(@Param("componentUuid") String componentUuid);
void deleteScaIssuesReleasesByComponentUuid(@Param("componentUuid") String componentUuid);
+ void deleteScaIssuesReleasesChangesByComponentUuid(@Param("componentUuid") String componentUuid);
+
void deleteScaReleasesByComponentUuid(@Param("componentUuid") String componentUuid);
+
+ void deleteScaLicenseProfileProjectsByProjectUuid(@Param("projectUuid") String projectUuid);
+
+ void deleteArchitectureGraphsByBranchUuid(@Param("branchUuid") String branchUuid);
}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualitygate/QualityGateFindingDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualitygate/QualityGateFindingDto.java
index ebefc919835..17e310f3887 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/qualitygate/QualityGateFindingDto.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/qualitygate/QualityGateFindingDto.java
@@ -27,6 +27,7 @@ public class QualityGateFindingDto {
private String operator = null;
private String valueType = null;
private String errorThreshold = null;
+ private String qualityGateName = null;
public String getDescription() {
return description;
@@ -52,6 +53,10 @@ public class QualityGateFindingDto {
return errorThreshold;
}
+ public String getQualityGateName() {
+ return qualityGateName;
+ }
+
private String getOperator() {
return operator;
}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupQuery.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupQuery.java
index a2185648d6b..b54c25da22d 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupQuery.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupQuery.java
@@ -29,10 +29,14 @@ import org.sonar.db.WildcardPosition;
public class GroupQuery {
private final String searchText;
private final String isManagedSqlClause;
+ private final String userId;
+ private final String excludedUserId;
- GroupQuery(@Nullable String searchText, @Nullable String isManagedSqlClause) {
+ GroupQuery(@Nullable String searchText, @Nullable String isManagedSqlClause, String userId, String excludedUserId) {
this.searchText = searchTextToSearchTextSql(searchText);
this.isManagedSqlClause = isManagedSqlClause;
+ this.userId = userId;
+ this.excludedUserId = excludedUserId;
}
private static String searchTextToSearchTextSql(@Nullable String text) {
@@ -54,6 +58,16 @@ public class GroupQuery {
return isManagedSqlClause;
}
+ @CheckForNull
+ public String getUserId() {
+ return userId;
+ }
+
+ @CheckForNull
+ public String getExcludedUserId() {
+ return excludedUserId;
+ }
+
public static GroupQueryBuilder builder() {
return new GroupQueryBuilder();
}
@@ -61,6 +75,8 @@ public class GroupQuery {
public static final class GroupQueryBuilder {
private String searchText = null;
private String isManagedSqlClause = null;
+ private String userId = null;
+ private String excludedUserId = null;
private GroupQueryBuilder() {
}
@@ -70,14 +86,23 @@ public class GroupQuery {
return this;
}
-
public GroupQuery.GroupQueryBuilder isManagedClause(@Nullable String isManagedSqlClause) {
this.isManagedSqlClause = isManagedSqlClause;
return this;
}
+ public GroupQuery.GroupQueryBuilder userId(@Nullable String userId) {
+ this.userId = userId;
+ return this;
+ }
+
+ public GroupQuery.GroupQueryBuilder excludedUserId(@Nullable String excludedUserId) {
+ this.excludedUserId = excludedUserId;
+ return this;
+ }
+
public GroupQuery build() {
- return new GroupQuery(searchText, isManagedSqlClause);
+ return new GroupQuery(searchText, isManagedSqlClause, userId, excludedUserId);
}
}
}
diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/alm/setting/ProjectAlmSettingMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/alm/setting/ProjectAlmSettingMapper.xml
index 9f015b2d22e..e5de9d5a3e1 100644
--- a/server/sonar-db-dao/src/main/resources/org/sonar/db/alm/setting/ProjectAlmSettingMapper.xml
+++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/alm/setting/ProjectAlmSettingMapper.xml
@@ -215,6 +215,12 @@
<if test="query.almSettingUuid != null">
AND p.alm_setting_uuid = #{query.almSettingUuid, jdbcType=VARCHAR}
</if>
+ <if test="query.almRepo != null">
+ AND lower(p.alm_repo) = lower(#{query.almRepo, jdbcType=VARCHAR})
+ </if>
+ <if test="query.almSlug != null">
+ AND lower(p.alm_slug) = lower(#{query.almSlug, jdbcType=VARCHAR})
+ </if>
</where>
</sql>
diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml
index f96a116d7c1..acf970ba7ac 100644
--- a/server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml
+++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml
@@ -322,4 +322,13 @@
and p.uuid not in (select project_uuid from project_qprofiles)
</select>
+ <select id="selectPullRequestsTargetingBranch" resultType="org.sonar.db.component.BranchDto">
+ select <include refid="columns"/>
+ from project_branches pb
+ where
+ pb.project_uuid = #{projectUuid, jdbcType=VARCHAR}
+ and pb.merge_branch_uuid = #{branchUuid, jdbcType=VARCHAR}
+ and pb.branch_type = 'PULL_REQUEST'
+ </select>
+
</mapper>
diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/purge/PurgeMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/purge/PurgeMapper.xml
index 4a64f3cdeab..bc5c066d6b4 100644
--- a/server/sonar-db-dao/src/main/resources/org/sonar/db/purge/PurgeMapper.xml
+++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/purge/PurgeMapper.xml
@@ -670,14 +670,28 @@
delete from issues_fixed where pull_request_uuid = #{branchUuid,jdbcType=VARCHAR}
</delete>
+ <delete id="deleteArchitectureGraphsByBranchUuid">
+ delete from architecture_graphs where branch_uuid = #{branchUuid,jdbcType=VARCHAR}
+ </delete>
+ <delete id="deleteScaAnalysesByComponentUuid">
+ delete from sca_analyses where component_uuid = #{componentUuid,jdbcType=VARCHAR}
+ </delete>
<delete id="deleteScaDependenciesByComponentUuid">
delete from sca_dependencies where sca_release_uuid in (select uuid from sca_releases where component_uuid = #{componentUuid,jdbcType=VARCHAR})
</delete>
<delete id="deleteScaIssuesReleasesByComponentUuid">
delete from sca_issues_releases where sca_release_uuid in (select uuid from sca_releases where component_uuid = #{componentUuid,jdbcType=VARCHAR})
</delete>
+ <delete id="deleteScaIssuesReleasesChangesByComponentUuid">
+ delete from sca_issue_rels_changes where sca_issues_releases_uuid in
+ (select sca_issues_releases.uuid from sca_issues_releases join sca_releases on sca_releases.uuid = sca_issues_releases.sca_release_uuid
+ where sca_releases.component_uuid = #{componentUuid,jdbcType=VARCHAR})
+ </delete>
<delete id="deleteScaReleasesByComponentUuid">
delete from sca_releases where component_uuid = #{componentUuid,jdbcType=VARCHAR}
</delete>
+ <delete id="deleteScaLicenseProfileProjectsByProjectUuid">
+ delete from sca_lic_prof_projects where project_uuid = #{projectUuid,jdbcType=VARCHAR}
+ </delete>
</mapper>
diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/qualitygate/QualityGateMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/qualitygate/QualityGateMapper.xml
index d4dd06bf35d..16df5308d01 100644
--- a/server/sonar-db-dao/src/main/resources/org/sonar/db/qualitygate/QualityGateMapper.xml
+++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/qualitygate/QualityGateMapper.xml
@@ -7,6 +7,12 @@
</sql>
<sql id="qualityGateFindingColumns">
+ <!--
+ If a row's columns are all `null`, MyBatis, by default, will return `null`
+ instead of an instantiated object with all its properties set to `null`.
+ This case expression, for the QG name, is designed to preserve that behavior.
+ -->
+ CASE WHEN qgc.operator IS NULL THEN NULL ELSE qg.name END AS qualityGateName,
m.short_name as description,
qgc.operator as operator,
m.val_type as valueType,
diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/report/RegulatoryReportMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/report/RegulatoryReportMapper.xml
index 7506b52f060..6281acad179 100644
--- a/server/sonar-db-dao/src/main/resources/org/sonar/db/report/RegulatoryReportMapper.xml
+++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/report/RegulatoryReportMapper.xml
@@ -69,8 +69,8 @@
left outer join issues_impacts ii on i.kee = ii.issue_key
where i.project_uuid=#{branchUuid,jdbcType=VARCHAR}
and i.status !='CLOSED'
- <!--BUG, VULNERABILITY, SECURITY_HOTSPOT -->
- and i.issue_type in (2, 3, 4)
+ <!--CODE_SMELL, BUG, VULNERABILITY, SECURITY_HOTSPOT -->
+ and i.issue_type in (1, 2, 3, 4)
order by i.kee, ic.issue_change_creation_date
</select>
</mapper>
diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/user/GroupMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/user/GroupMapper.xml
index 7adc671389e..87ed608b98a 100644
--- a/server/sonar-db-dao/src/main/resources/org/sonar/db/user/GroupMapper.xml
+++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/user/GroupMapper.xml
@@ -113,6 +113,12 @@
<if test="query.isManagedSqlClause != null">
AND ${query.isManagedSqlClause}
</if>
+ <if test="query.userId != null">
+ AND g.uuid in (select group_uuid from groups_users gu where gu.user_uuid = #{query.userId,jdbcType=VARCHAR})
+ </if>
+ <if test="query.excludedUserId != null">
+ AND g.uuid not in (select group_uuid from groups_users gu where gu.user_uuid = #{query.excludedUserId,jdbcType=VARCHAR})
+ </if>
</where>
</sql>
</mapper>
diff --git a/server/sonar-db-dao/src/schema/schema-sq.ddl b/server/sonar-db-dao/src/schema/schema-sq.ddl
index 4bbc5c40fd7..3a18fcb87e4 100644
--- a/server/sonar-db-dao/src/schema/schema-sq.ddl
+++ b/server/sonar-db-dao/src/schema/schema-sq.ddl
@@ -121,12 +121,15 @@ CREATE INDEX "IDX_APP_PROJ_PROJECT_UUID" ON "APP_PROJECTS"("PROJECT_UUID" NULLS
CREATE TABLE "ARCHITECTURE_GRAPHS"(
"UUID" CHARACTER VARYING(40) NOT NULL,
"BRANCH_UUID" CHARACTER VARYING(40) NOT NULL,
- "SOURCE" CHARACTER VARYING(255) NOT NULL,
+ "ECOSYSTEM" CHARACTER VARYING(255) NOT NULL,
"TYPE" CHARACTER VARYING(255) NOT NULL,
- "GRAPH_DATA" CHARACTER LARGE OBJECT NOT NULL
+ "GRAPH_DATA" CHARACTER LARGE OBJECT NOT NULL,
+ "ANALYSIS_UUID" CHARACTER VARYING(40),
+ "PERSPECTIVE_KEY" CHARACTER VARYING(255),
+ "GRAPH_VERSION" CHARACTER VARYING(255) DEFAULT '1.0.0' NOT NULL
);
ALTER TABLE "ARCHITECTURE_GRAPHS" ADD CONSTRAINT "PK_ARCHITECTURE_GRAPHS" PRIMARY KEY("UUID");
-CREATE UNIQUE NULLS NOT DISTINCT INDEX "UQ_IDX_AG_BRANCH_TYPE_SOURCE" ON "ARCHITECTURE_GRAPHS"("BRANCH_UUID" NULLS FIRST, "TYPE" NULLS FIRST, "SOURCE" NULLS FIRST);
+CREATE UNIQUE NULLS NOT DISTINCT INDEX "UQ_IDX_AG_BRCH_TP_SRC_PSPCTV" ON "ARCHITECTURE_GRAPHS"("BRANCH_UUID" NULLS FIRST, "TYPE" NULLS FIRST, "ECOSYSTEM" NULLS FIRST, "PERSPECTIVE_KEY" NULLS FIRST);
CREATE TABLE "AUDITS"(
"UUID" CHARACTER VARYING(40) NOT NULL,
@@ -699,6 +702,7 @@ ALTER TABLE "PROJECT_ALM_SETTINGS" ADD CONSTRAINT "PK_PROJECT_ALM_SETTINGS" PRIM
CREATE UNIQUE NULLS NOT DISTINCT INDEX "UNIQ_PROJECT_ALM_SETTINGS" ON "PROJECT_ALM_SETTINGS"("PROJECT_UUID" NULLS FIRST);
CREATE INDEX "PROJECT_ALM_SETTINGS_ALM" ON "PROJECT_ALM_SETTINGS"("ALM_SETTING_UUID" NULLS FIRST);
CREATE INDEX "PROJECT_ALM_SETTINGS_SLUG" ON "PROJECT_ALM_SETTINGS"("ALM_SLUG" NULLS FIRST);
+CREATE INDEX "PROJECT_ALM_SETTINGS_ALM_REPO" ON "PROJECT_ALM_SETTINGS"("ALM_REPO" NULLS FIRST);
CREATE TABLE "PROJECT_BADGE_TOKEN"(
"UUID" CHARACTER VARYING(40) NOT NULL,
@@ -963,7 +967,7 @@ CREATE UNIQUE NULLS NOT DISTINCT INDEX "RULE_TAGS_RULE_UUID" ON "RULE_TAGS"("RUL
CREATE TABLE "RULES"(
"UUID" CHARACTER VARYING(40) NOT NULL,
- "NAME" CHARACTER VARYING(200),
+ "NAME" CHARACTER VARYING(255),
"PLUGIN_RULE_KEY" CHARACTER VARYING(200) NOT NULL,
"PLUGIN_KEY" CHARACTER VARYING(200),
"PLUGIN_CONFIG_KEY" CHARACTER VARYING(200),
@@ -1042,6 +1046,20 @@ 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_ANALYSES"(
+ "UUID" CHARACTER VARYING(40) NOT NULL,
+ "COMPONENT_UUID" CHARACTER VARYING(40) NOT NULL,
+ "STATUS" CHARACTER VARYING(40) NOT NULL,
+ "FAILED_REASON" CHARACTER VARYING(255),
+ "ERRORS" CHARACTER LARGE OBJECT NOT NULL,
+ "PARSED_FILES" CHARACTER LARGE OBJECT NOT NULL,
+ "CREATED_AT" BIGINT NOT NULL,
+ "UPDATED_AT" BIGINT NOT NULL,
+ "ANALYSIS_PARAMETERS" CHARACTER LARGE OBJECT
+);
+ALTER TABLE "SCA_ANALYSES" ADD CONSTRAINT "PK_SCA_ANALYSES" PRIMARY KEY("UUID");
+CREATE UNIQUE NULLS NOT DISTINCT INDEX "SCA_ANALYSES_COMPONENT_UNIQ" ON "SCA_ANALYSES"("COMPONENT_UUID" NULLS FIRST);
+
CREATE TABLE "SCA_DEPENDENCIES"(
"UUID" CHARACTER VARYING(40) NOT NULL,
"SCA_RELEASE_UUID" CHARACTER VARYING(40) NOT NULL,
@@ -1052,12 +1070,33 @@ CREATE TABLE "SCA_DEPENDENCIES"(
"CHAINS" CHARACTER LARGE OBJECT,
"CREATED_AT" BIGINT NOT NULL,
"UPDATED_AT" BIGINT NOT NULL,
- "NEW_IN_PULL_REQUEST" BOOLEAN DEFAULT FALSE NOT NULL,
- "PRODUCTION_SCOPE" BOOLEAN DEFAULT TRUE NOT NULL
+ "PRODUCTION_SCOPE" BOOLEAN DEFAULT TRUE NOT NULL,
+ "IS_NEW" BOOLEAN DEFAULT FALSE NOT NULL
);
ALTER TABLE "SCA_DEPENDENCIES" ADD CONSTRAINT "PK_SCA_DEPENDENCIES" PRIMARY KEY("UUID");
CREATE INDEX "SCA_DEPENDENCIES_RELEASE_UUID" ON "SCA_DEPENDENCIES"("SCA_RELEASE_UUID" NULLS FIRST);
+CREATE TABLE "SCA_ENCOUNTERED_LICENSES"(
+ "UUID" CHARACTER VARYING(40) NOT NULL,
+ "LICENSE_POLICY_ID" CHARACTER VARYING(127) NOT NULL,
+ "CREATED_AT" BIGINT NOT NULL,
+ "UPDATED_AT" BIGINT NOT NULL
+);
+ALTER TABLE "SCA_ENCOUNTERED_LICENSES" ADD CONSTRAINT "PK_SCA_ENCOUNTERED_LICENSES" PRIMARY KEY("UUID");
+CREATE UNIQUE NULLS NOT DISTINCT INDEX "SCA_ENCOUNTERED_LIC_UNIQ" ON "SCA_ENCOUNTERED_LICENSES"("LICENSE_POLICY_ID" NULLS FIRST);
+
+CREATE TABLE "SCA_ISSUE_RELS_CHANGES"(
+ "UUID" CHARACTER VARYING(40) NOT NULL,
+ "SCA_ISSUES_RELEASES_UUID" CHARACTER VARYING(40) NOT NULL,
+ "USER_UUID" CHARACTER VARYING(40),
+ "CHANGE_DATA" CHARACTER LARGE OBJECT,
+ "CREATED_AT" BIGINT NOT NULL,
+ "UPDATED_AT" BIGINT NOT NULL,
+ "CHANGE_COMMENT" CHARACTER VARYING(4000)
+);
+ALTER TABLE "SCA_ISSUE_RELS_CHANGES" ADD CONSTRAINT "PK_SCA_ISSUE_RELS_CHANGES" PRIMARY KEY("UUID");
+CREATE INDEX "SCA_ISS_RELS_CHANGES_IR_UUID" ON "SCA_ISSUE_RELS_CHANGES"("SCA_ISSUES_RELEASES_UUID" NULLS FIRST);
+
CREATE TABLE "SCA_ISSUES"(
"UUID" CHARACTER VARYING(40) NOT NULL,
"SCA_ISSUE_TYPE" CHARACTER VARYING(40) NOT NULL,
@@ -1077,13 +1116,62 @@ CREATE TABLE "SCA_ISSUES_RELEASES"(
"SEVERITY" CHARACTER VARYING(15) NOT NULL,
"SEVERITY_SORT_KEY" INTEGER NOT NULL,
"CREATED_AT" BIGINT NOT NULL,
- "UPDATED_AT" BIGINT NOT NULL
+ "UPDATED_AT" BIGINT NOT NULL,
+ "STATUS" CHARACTER VARYING(40) NOT NULL,
+ "ASSIGNEE_UUID" CHARACTER VARYING(40),
+ "PREVIOUS_MANUAL_STATUS" CHARACTER VARYING(40),
+ "ORIGINAL_SEVERITY" CHARACTER VARYING(15) NOT NULL,
+ "MANUAL_SEVERITY" CHARACTER VARYING(15),
+ "SHOW_INCREASED_SEVERITY_WARNING" BOOLEAN DEFAULT FALSE NOT NULL
);
ALTER TABLE "SCA_ISSUES_RELEASES" ADD CONSTRAINT "PK_SCA_ISSUES_RELEASES" PRIMARY KEY("UUID");
CREATE INDEX "SCA_ISSUES_RELEASES_SCA_ISSUE" ON "SCA_ISSUES_RELEASES"("SCA_ISSUE_UUID" NULLS FIRST);
CREATE INDEX "SCA_ISSUES_RELEASES_SCA_RELEAS" ON "SCA_ISSUES_RELEASES"("SCA_RELEASE_UUID" NULLS FIRST);
CREATE UNIQUE NULLS NOT DISTINCT INDEX "SCA_ISSUES_RELEASES_UNIQUE" ON "SCA_ISSUES_RELEASES"("SCA_ISSUE_UUID" NULLS FIRST, "SCA_RELEASE_UUID" NULLS FIRST);
+CREATE TABLE "SCA_LIC_PROF_CATEGORIES"(
+ "UUID" CHARACTER VARYING(40) NOT NULL,
+ "SCA_LICENSE_PROFILE_UUID" CHARACTER VARYING(40) NOT NULL,
+ "CATEGORY" CHARACTER VARYING(40) NOT NULL,
+ "CREATED_AT" BIGINT NOT NULL,
+ "UPDATED_AT" BIGINT NOT NULL
+);
+ALTER TABLE "SCA_LIC_PROF_CATEGORIES" ADD CONSTRAINT "PK_SCA_LIC_PROF_CATEGORIES" PRIMARY KEY("UUID");
+CREATE UNIQUE NULLS NOT DISTINCT INDEX "SCA_LIC_PROF_CATEGORIES_UNIQ" ON "SCA_LIC_PROF_CATEGORIES"("SCA_LICENSE_PROFILE_UUID" NULLS FIRST, "CATEGORY" NULLS FIRST);
+
+CREATE TABLE "SCA_LIC_PROF_CUSTOMS"(
+ "UUID" CHARACTER VARYING(40) NOT NULL,
+ "SCA_LICENSE_PROFILE_UUID" CHARACTER VARYING(40) NOT NULL,
+ "LICENSE_POLICY_ID" CHARACTER VARYING(127) NOT NULL,
+ "STATUS" CHARACTER VARYING(40) NOT NULL,
+ "CREATED_AT" BIGINT NOT NULL,
+ "UPDATED_AT" BIGINT NOT NULL
+);
+ALTER TABLE "SCA_LIC_PROF_CUSTOMS" ADD CONSTRAINT "PK_SCA_LIC_PROF_CUSTOMS" PRIMARY KEY("UUID");
+CREATE UNIQUE NULLS NOT DISTINCT INDEX "SCA_LIC_PROF_CUSTOMS_UNIQ" ON "SCA_LIC_PROF_CUSTOMS"("SCA_LICENSE_PROFILE_UUID" NULLS FIRST, "LICENSE_POLICY_ID" NULLS FIRST);
+
+CREATE TABLE "SCA_LIC_PROF_PROJECTS"(
+ "UUID" CHARACTER VARYING(40) NOT NULL,
+ "PROJECT_UUID" CHARACTER VARYING(40) NOT NULL,
+ "SCA_LICENSE_PROFILE_UUID" CHARACTER VARYING(40) NOT NULL,
+ "CREATED_AT" BIGINT NOT NULL,
+ "UPDATED_AT" BIGINT NOT NULL
+);
+ALTER TABLE "SCA_LIC_PROF_PROJECTS" ADD CONSTRAINT "PK_SCA_LIC_PROF_PROJECTS" PRIMARY KEY("UUID");
+CREATE UNIQUE NULLS NOT DISTINCT INDEX "SCA_LIC_PROF_PROJECTS_UNIQ" ON "SCA_LIC_PROF_PROJECTS"("PROJECT_UUID" NULLS FIRST);
+
+CREATE TABLE "SCA_LICENSE_PROFILES"(
+ "UUID" CHARACTER VARYING(40) NOT NULL,
+ "IS_DEFAULT_PROFILE" BOOLEAN NOT NULL,
+ "NAME" CHARACTER VARYING(400) NOT NULL,
+ "CREATED_AT" BIGINT NOT NULL,
+ "UPDATED_AT" BIGINT NOT NULL,
+ "POLICY_UPDATED_AT" BIGINT NOT NULL,
+ "ORGANIZATION_UUID" CHARACTER VARYING(40) DEFAULT '00000000-0000-4000-0000-000000000000' NOT NULL
+);
+ALTER TABLE "SCA_LICENSE_PROFILES" ADD CONSTRAINT "PK_SCA_LICENSE_PROFILES" PRIMARY KEY("UUID");
+CREATE UNIQUE NULLS NOT DISTINCT INDEX "SCA_LICENSE_PROFILES_UNIQ" ON "SCA_LICENSE_PROFILES"("ORGANIZATION_UUID" NULLS FIRST, "NAME" NULLS FIRST);
+
CREATE TABLE "SCA_RELEASES"(
"UUID" CHARACTER VARYING(40) NOT NULL,
"COMPONENT_UUID" CHARACTER VARYING(40) NOT NULL,
@@ -1095,12 +1183,13 @@ CREATE TABLE "SCA_RELEASES"(
"KNOWN" BOOLEAN NOT NULL,
"CREATED_AT" BIGINT NOT NULL,
"UPDATED_AT" BIGINT NOT NULL,
- "NEW_IN_PULL_REQUEST" BOOLEAN DEFAULT FALSE NOT NULL,
"DECLARED_LICENSE_EXPRESSION" CHARACTER VARYING(400) DEFAULT 'NOASSERTION' NOT NULL,
- "KNOWN_PACKAGE" BOOLEAN NOT NULL
+ "KNOWN_PACKAGE" BOOLEAN NOT NULL,
+ "IS_NEW" BOOLEAN DEFAULT FALSE NOT NULL
);
ALTER TABLE "SCA_RELEASES" ADD CONSTRAINT "PK_SCA_RELEASES" PRIMARY KEY("UUID");
CREATE INDEX "SCA_RELEASES_COMP_UUID_UUID" ON "SCA_RELEASES"("COMPONENT_UUID" NULLS FIRST, "UUID" NULLS FIRST);
+CREATE UNIQUE NULLS NOT DISTINCT INDEX "SCA_RELEASES_PACKAGE_URL_UNIQ" ON "SCA_RELEASES"("PACKAGE_URL" NULLS FIRST, "COMPONENT_UUID" NULLS FIRST);
CREATE TABLE "SCA_VULNERABILITY_ISSUES"(
"UUID" CHARACTER VARYING(40) NOT NULL,
@@ -1108,7 +1197,8 @@ CREATE TABLE "SCA_VULNERABILITY_ISSUES"(
"CWE_IDS" CHARACTER VARYING(255) NOT NULL,
"CVSS_SCORE" DOUBLE PRECISION,
"CREATED_AT" BIGINT NOT NULL,
- "UPDATED_AT" BIGINT NOT NULL
+ "UPDATED_AT" BIGINT NOT NULL,
+ "WITHDRAWN" BOOLEAN DEFAULT FALSE NOT NULL
);
ALTER TABLE "SCA_VULNERABILITY_ISSUES" ADD CONSTRAINT "PK_SCA_VULNERABILITY_ISSUES" PRIMARY KEY("UUID");
diff --git a/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java b/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java
index 26dcfbab451..d1ee49895e2 100644
--- a/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java
+++ b/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java
@@ -208,7 +208,7 @@ public class UserDbTester {
}
public int countAllGroups() {
- return db.getDbClient().groupDao().countByQuery(db.getSession(), new GroupQuery(null, null));
+ return db.getDbClient().groupDao().countByQuery(db.getSession(), new GroupQuery(null, null, null, null));
}
public Optional<ExternalGroupDto> selectExternalGroupByGroupUuid(String groupUuid) {
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddAnalysisUuidOnArchitectureGraphsIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddAnalysisUuidOnArchitectureGraphsIT.java
new file mode 100644
index 00000000000..167b797de15
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddAnalysisUuidOnArchitectureGraphsIT.java
@@ -0,0 +1,55 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+
+class AddAnalysisUuidOnArchitectureGraphsIT {
+ private static final String TABLE_NAME = "architecture_graphs";
+ private static final String COLUMN_NAME = "analysis_uuid";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddAnalysisUuidOnArchitectureGraphs.class);
+ private final DdlChange underTest = new AddAnalysisUuidOnArchitectureGraphs(db.database());
+
+ @Test
+ void execute_shouldAddColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, VarcharColumnDef.UUID_SIZE, true);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, VarcharColumnDef.UUID_SIZE, true);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddAssigneeNameToScaIssuesReleasesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddAssigneeNameToScaIssuesReleasesIT.java
new file mode 100644
index 00000000000..190c168adca
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddAssigneeNameToScaIssuesReleasesIT.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class AddAssigneeNameToScaIssuesReleasesIT {
+ private static final String TABLE_NAME = "sca_issues_releases";
+ private static final String COLUMN_NAME = "assignee_name";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddAssigneeNameToScaIssuesReleases.class);
+ private final DdlChange underTest = new AddAssigneeNameToScaIssuesReleases(db.database());
+
+ @Test
+ void execute_shouldAddColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 200, true);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 200, true);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddAssigneeToScaIssuesReleasesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddAssigneeToScaIssuesReleasesIT.java
new file mode 100644
index 00000000000..eefcd39fef4
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddAssigneeToScaIssuesReleasesIT.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class AddAssigneeToScaIssuesReleasesIT {
+ private static final String TABLE_NAME = "sca_issues_releases";
+ private static final String COLUMN_NAME = "assignee_uuid";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddAssigneeToScaIssuesReleases.class);
+ private final DdlChange underTest = new AddAssigneeToScaIssuesReleases(db.database());
+
+ @Test
+ void execute_shouldAddColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 40, true);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 40, true);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddCommentToScaIssuesReleasesChangesTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddCommentToScaIssuesReleasesChangesTableIT.java
new file mode 100644
index 00000000000..b1df6436185
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddCommentToScaIssuesReleasesChangesTableIT.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class AddCommentToScaIssuesReleasesChangesTableIT {
+ private static final String TABLE_NAME = "sca_issue_rels_changes";
+ private static final String COLUMN_NAME = "change_comment";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddCommentToScaIssuesReleasesChangesTable.class);
+ private final DdlChange underTest = new AddCommentToScaIssuesReleasesChangesTable(db.database());
+
+ @Test
+ void execute_shouldAddColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 4000, true);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 4000, true);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddGraphVersionOnArchitectureGraphsIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddGraphVersionOnArchitectureGraphsIT.java
new file mode 100644
index 00000000000..79cf8e29606
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddGraphVersionOnArchitectureGraphsIT.java
@@ -0,0 +1,54 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+
+class AddGraphVersionOnArchitectureGraphsIT {
+ private static final String TABLE_NAME = "architecture_graphs";
+ private static final String COLUMN_NAME = "graph_version";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddGraphVersionOnArchitectureGraphsTable.class);
+ private final DdlChange underTest = new AddGraphVersionOnArchitectureGraphsTable(db.database());
+
+ @Test
+ void execute_shouldAddColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 255, false);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 255, false);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddIsNewToScaDependenciesTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddIsNewToScaDependenciesTableIT.java
new file mode 100644
index 00000000000..e619add3bb7
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddIsNewToScaDependenciesTableIT.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.BOOLEAN;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class AddIsNewToScaDependenciesTableIT {
+ private static final String TABLE_NAME = "sca_dependencies";
+ private static final String COLUMN_NAME = "is_new";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddIsNewToScaDependenciesTable.class);
+ private final DdlChange underTest = new AddIsNewToScaDependenciesTable(db.database());
+
+ @Test
+ void execute_shouldAddColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BOOLEAN, null, false);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BOOLEAN, null, false);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddIsNewToScaReleasesTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddIsNewToScaReleasesTableIT.java
new file mode 100644
index 00000000000..eca405f0738
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddIsNewToScaReleasesTableIT.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.BOOLEAN;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class AddIsNewToScaReleasesTableIT {
+ private static final String TABLE_NAME = "sca_releases";
+ private static final String COLUMN_NAME = "is_new";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddIsNewToScaReleasesTable.class);
+ private final DdlChange underTest = new AddIsNewToScaReleasesTable(db.database());
+
+ @Test
+ void execute_shouldAddColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BOOLEAN, null, false);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BOOLEAN, null, false);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddPerspectiveKeyOnArchitectureGraphsIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddPerspectiveKeyOnArchitectureGraphsIT.java
new file mode 100644
index 00000000000..22f757cd152
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddPerspectiveKeyOnArchitectureGraphsIT.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class AddPerspectiveKeyOnArchitectureGraphsIT {
+ private static final String TABLE_NAME = "architecture_graphs";
+ private static final String COLUMN_NAME = "perspective_key";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddPerspectiveKeyOnArchitectureGraphs.class);
+ private final DdlChange underTest = new AddPerspectiveKeyOnArchitectureGraphs(db.database());
+
+ @Test
+ void execute_shouldAddColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 255, true);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 255, true);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddPolicyUpdatedAtToScaLicenseProfilesTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddPolicyUpdatedAtToScaLicenseProfilesTableIT.java
new file mode 100644
index 00000000000..cc4798f73d2
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddPolicyUpdatedAtToScaLicenseProfilesTableIT.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.BIGINT;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class AddPolicyUpdatedAtToScaLicenseProfilesTableIT {
+ private static final String TABLE_NAME = "sca_license_profiles";
+ private static final String COLUMN_NAME = "policy_updated_at";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddPolicyUpdatedAtToScaLicenseProfilesTable.class);
+ private final DdlChange underTest = new AddPolicyUpdatedAtToScaLicenseProfilesTable(db.database());
+
+ @Test
+ void execute_shouldAddColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BIGINT, null, true);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BIGINT, null, true);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddPreviousManualStatusToScaIssuesReleasesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddPreviousManualStatusToScaIssuesReleasesIT.java
new file mode 100644
index 00000000000..cf293d4c9bd
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddPreviousManualStatusToScaIssuesReleasesIT.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class AddPreviousManualStatusToScaIssuesReleasesIT {
+ private static final String TABLE_NAME = "sca_issues_releases";
+ private static final String COLUMN_NAME = "previous_manual_status";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddPreviousManualStatusToScaIssuesReleases.class);
+ private final DdlChange underTest = new AddPreviousManualStatusToScaIssuesReleases(db.database());
+
+ @Test
+ void execute_shouldAddColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 40, true);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 40, true);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddStatusToScaIssuesReleasesTableTestIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddStatusToScaIssuesReleasesTableTestIT.java
new file mode 100644
index 00000000000..481490978bc
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddStatusToScaIssuesReleasesTableTestIT.java
@@ -0,0 +1,57 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+
+import static java.sql.Types.VARCHAR;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+class AddStatusToScaIssuesReleasesTableTestIT {
+ static final String TABLE_NAME = "sca_issues_releases";
+ static final String COLUMN_NAME = "status";
+ static final int COLUMN_SIZE = 40;
+
+ @RegisterExtension
+ public final MigrationDbTester db = MigrationDbTester.createForMigrationStep(AddStatusToScaIssuesReleasesTable.class);
+
+ private final AddStatusToScaIssuesReleasesTable underTest = new AddStatusToScaIssuesReleasesTable(db.database());
+
+ @Test
+ void execute_whenColumnDoesNotExist_shouldCreateColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ assertColumnExists();
+ }
+
+ @Test
+ void execute_whenColumnsAlreadyExists_shouldNotFail() throws SQLException {
+ underTest.execute();
+ assertColumnExists();
+ assertThatCode(underTest::execute).doesNotThrowAnyException();
+ }
+
+ private void assertColumnExists() {
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, COLUMN_SIZE, true);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/BackfillRemoveAssigneeNameFromIssueReleaseChangesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/BackfillRemoveAssigneeNameFromIssueReleaseChangesIT.java
new file mode 100644
index 00000000000..e8c1735e3df
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/BackfillRemoveAssigneeNameFromIssueReleaseChangesIT.java
@@ -0,0 +1,89 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import javax.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DataChange;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class BackfillRemoveAssigneeNameFromIssueReleaseChangesIT {
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(BackfillRemoveAssigneeNameFromIssueReleaseChanges.class);
+ private final DataChange underTest = new BackfillRemoveAssigneeNameFromIssueReleaseChanges(db.database());
+
+ @Test
+ void execute_withAssigneeName_modifiesRecords() throws SQLException {
+ // Record with only assigneeName field - should result in empty object
+ insertIssueReleaseChange("1", "{\"assigneeName\": [null, \"alice\"]}");
+
+ // Record with mixed fields - should remove only assigneeName
+ insertIssueReleaseChange("2",
+ "{" +
+ "\"assigneeUuid\":[\"user1-uuid\",\"user2-uuid\"]," +
+ "\"assigneeName\":[\"alice\",\"bob\"]}");
+
+ underTest.execute();
+
+ assertThat(getChangeData("1")).isEqualTo("{}");
+ assertThat(getChangeData("2")).isEqualTo("{\"assigneeUuid\":[\"user1-uuid\",\"user2-uuid\"]}");
+ }
+
+ @Test
+ void execute_withoutAssigneeName_doesNotModifyRecords() throws SQLException {
+ var changeData1 = "{\"assigneeUuid\":[\"user1-uuid\",\"user2-uuid\"]}";
+ insertIssueReleaseChange("1", changeData1);
+
+ underTest.execute();
+
+ assertThat(getChangeData("1")).isEqualTo(changeData1);
+ }
+
+ @Test
+ void execute_withNullChangeData_doesNotModifyRecords() throws SQLException {
+ insertIssueReleaseChange("1", null);
+
+ underTest.execute();
+
+ assertThat(getChangeData("1")).isNull();
+ }
+
+ private void insertIssueReleaseChange(String suffix, @Nullable String changeData) {
+ db.executeInsert("sca_issue_rels_changes",
+ "uuid", "scaIssueReleaseChangeUuid" + suffix,
+ "sca_issues_releases_uuid", "scaIssueReleaseUuid" + suffix,
+ "user_uuid", "user1-uuid",
+ "change_data", changeData,
+ "created_at", 1L,
+ "updated_at", 2L,
+ "change_comment", "my comment");
+ }
+
+ private String getChangeData(String suffix) {
+ var uuid = "scaIssueReleaseChangeUuid" + suffix;
+ return (String) db.selectFirst("select change_data from sca_issue_rels_changes where uuid = '" + uuid + "'")
+ .get("change_data");
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateIndexOnScaIssuesReleaseChangesReleaseIdIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateIndexOnScaIssuesReleaseChangesReleaseIdIT.java
new file mode 100644
index 00000000000..0bd955cf64e
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateIndexOnScaIssuesReleaseChangesReleaseIdIT.java
@@ -0,0 +1,52 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateIndexOnScaIssuesReleaseChangesReleaseId.COLUMN_NAME_SCA_RELEASE_UUID;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateIndexOnScaIssuesReleaseChangesReleaseId.INDEX_NAME;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateIndexOnScaIssuesReleaseChangesReleaseId.TABLE_NAME;
+
+class CreateIndexOnScaIssuesReleaseChangesReleaseIdIT {
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(CreateIndexOnScaIssuesReleaseChangesReleaseId.class);
+ private final DdlChange underTest = new CreateIndexOnScaIssuesReleaseChangesReleaseId(db.database());
+
+ @Test
+ void execute_shouldCreateIndex() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ db.assertIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_SCA_RELEASE_UUID);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_SCA_RELEASE_UUID);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaAnalysesTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaAnalysesTableIT.java
new file mode 100644
index 00000000000..f67d12fb20c
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaAnalysesTableIT.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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.BIGINT;
+import static java.sql.Types.CLOB;
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+
+class CreateScaAnalysesTableIT {
+ private static final String TABLE_NAME = "sca_analyses";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(CreateScaAnalysesTable.class);
+ private final DdlChange underTest = new CreateScaAnalysesTable(db.database());
+
+ @Test
+ void execute_shouldCreateTable() throws SQLException {
+ db.assertTableDoesNotExist(TABLE_NAME);
+ underTest.execute();
+ db.assertTableExists(TABLE_NAME);
+ db.assertPrimaryKey(TABLE_NAME, "pk_sca_analyses", "uuid");
+ db.assertColumnDefinition(TABLE_NAME, "uuid", VARCHAR, UUID_SIZE, false);
+ db.assertColumnDefinition(TABLE_NAME, "component_uuid", VARCHAR, UUID_SIZE, false);
+ db.assertColumnDefinition(TABLE_NAME, "status", VARCHAR, 40, false);
+ db.assertColumnDefinition(TABLE_NAME, "failed_reason", VARCHAR, 255, true);
+ db.assertColumnDefinition(TABLE_NAME, "errors", CLOB, null, false);
+ db.assertColumnDefinition(TABLE_NAME, "parsed_files", CLOB, null, false);
+ db.assertColumnDefinition(TABLE_NAME, "created_at", BIGINT, null, false);
+ db.assertColumnDefinition(TABLE_NAME, "updated_at", BIGINT, null, false);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertTableDoesNotExist(TABLE_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertTableExists(TABLE_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaEncounteredLicensesTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaEncounteredLicensesTableIT.java
new file mode 100644
index 00000000000..d0ab99a5797
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaEncounteredLicensesTableIT.java
@@ -0,0 +1,59 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.BIGINT;
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+
+class CreateScaEncounteredLicensesTableIT {
+ private static final String TABLE_NAME = "sca_encountered_licenses";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(CreateScaEncounteredLicensesTable.class);
+ private final DdlChange underTest = new CreateScaEncounteredLicensesTable(db.database());
+
+ @Test
+ void execute_shouldCreateTable() throws SQLException {
+ db.assertTableDoesNotExist(TABLE_NAME);
+ underTest.execute();
+ db.assertTableExists(TABLE_NAME);
+ db.assertPrimaryKey(TABLE_NAME, "pk_sca_encountered_licenses", "uuid");
+ db.assertColumnDefinition(TABLE_NAME, "uuid", VARCHAR, UUID_SIZE, false);
+ db.assertColumnDefinition(TABLE_NAME, "license_policy_id", VARCHAR, 127, false);
+ db.assertColumnDefinition(TABLE_NAME, "created_at", BIGINT, null, false);
+ db.assertColumnDefinition(TABLE_NAME, "updated_at", BIGINT, null, false);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertTableDoesNotExist(TABLE_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertTableExists(TABLE_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaIssuesReleasesChangesTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaIssuesReleasesChangesTableIT.java
new file mode 100644
index 00000000000..34d87e0b2b7
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaIssuesReleasesChangesTableIT.java
@@ -0,0 +1,63 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.BIGINT;
+import static java.sql.Types.CLOB;
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+
+class CreateScaIssuesReleasesChangesTableIT {
+ private static final String TABLE_NAME = "sca_issue_rels_changes";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(CreateScaIssuesReleasesChangesTable.class);
+ private final DdlChange underTest = new CreateScaIssuesReleasesChangesTable(db.database());
+
+ @Test
+ void execute_shouldCreateTable() throws SQLException {
+ db.assertTableDoesNotExist(TABLE_NAME);
+ underTest.execute();
+ db.assertTableExists(TABLE_NAME);
+ db.assertPrimaryKey(TABLE_NAME, "pk_sca_issue_rels_changes", "uuid");
+ db.assertColumnDefinition(TABLE_NAME, "uuid", VARCHAR, UUID_SIZE, false);
+ db.assertColumnDefinition(TABLE_NAME, "sca_issues_releases_uuid", VARCHAR, UUID_SIZE, false);
+ db.assertColumnDefinition(TABLE_NAME, "user_uuid", VARCHAR, UUID_SIZE, true);
+ db.assertColumnDefinition(TABLE_NAME, "change_type", VARCHAR, 40, false);
+ db.assertColumnDefinition(TABLE_NAME, "change_data", CLOB, null, true);
+ db.assertColumnDefinition(TABLE_NAME, "created_at", BIGINT, null, false);
+ db.assertColumnDefinition(TABLE_NAME, "updated_at", BIGINT, null, false);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertTableDoesNotExist(TABLE_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertTableExists(TABLE_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfileCategoriesTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfileCategoriesTableIT.java
new file mode 100644
index 00000000000..3c6ce03ff16
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfileCategoriesTableIT.java
@@ -0,0 +1,60 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.BIGINT;
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+
+class CreateScaLicenseProfileCategoriesTableIT {
+ private static final String TABLE_NAME = "sca_lic_prof_categories";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(CreateScaLicenseProfileCategoriesTable.class);
+ private final DdlChange underTest = new CreateScaLicenseProfileCategoriesTable(db.database());
+
+ @Test
+ void execute_shouldCreateTable() throws SQLException {
+ db.assertTableDoesNotExist(TABLE_NAME);
+ underTest.execute();
+ db.assertTableExists(TABLE_NAME);
+ db.assertPrimaryKey(TABLE_NAME, "pk_sca_lic_prof_categories", "uuid");
+ db.assertColumnDefinition(TABLE_NAME, "uuid", VARCHAR, UUID_SIZE, false);
+ db.assertColumnDefinition(TABLE_NAME, "sca_license_profile_uuid", VARCHAR, UUID_SIZE, false);
+ db.assertColumnDefinition(TABLE_NAME, "category", VARCHAR, 40, false);
+ db.assertColumnDefinition(TABLE_NAME, "created_at", BIGINT, null, false);
+ db.assertColumnDefinition(TABLE_NAME, "updated_at", BIGINT, null, false);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertTableDoesNotExist(TABLE_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertTableExists(TABLE_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfileCustomizationsTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfileCustomizationsTableIT.java
new file mode 100644
index 00000000000..04a0005ee80
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfileCustomizationsTableIT.java
@@ -0,0 +1,61 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.BIGINT;
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+
+class CreateScaLicenseProfileCustomizationsTableIT {
+ private static final String TABLE_NAME = "sca_lic_prof_customs";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(CreateScaLicenseProfileCustomizationsTable.class);
+ private final DdlChange underTest = new CreateScaLicenseProfileCustomizationsTable(db.database());
+
+ @Test
+ void execute_shouldCreateTable() throws SQLException {
+ db.assertTableDoesNotExist(TABLE_NAME);
+ underTest.execute();
+ db.assertTableExists(TABLE_NAME);
+ db.assertPrimaryKey(TABLE_NAME, "pk_sca_lic_prof_customs", "uuid");
+ db.assertColumnDefinition(TABLE_NAME, "uuid", VARCHAR, UUID_SIZE, false);
+ db.assertColumnDefinition(TABLE_NAME, "sca_license_profile_uuid", VARCHAR, UUID_SIZE, false);
+ db.assertColumnDefinition(TABLE_NAME, "license_policy_id", VARCHAR, 127, false);
+ db.assertColumnDefinition(TABLE_NAME, "status", VARCHAR, 40, false);
+ db.assertColumnDefinition(TABLE_NAME, "created_at", BIGINT, null, false);
+ db.assertColumnDefinition(TABLE_NAME, "updated_at", BIGINT, null, false);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertTableDoesNotExist(TABLE_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertTableExists(TABLE_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfileProjectsTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfileProjectsTableIT.java
new file mode 100644
index 00000000000..5964fc4507d
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfileProjectsTableIT.java
@@ -0,0 +1,60 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.BIGINT;
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+
+class CreateScaLicenseProfileProjectsTableIT {
+ private static final String TABLE_NAME = "sca_lic_prof_projects";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(CreateScaLicenseProfileProjectsTable.class);
+ private final DdlChange underTest = new CreateScaLicenseProfileProjectsTable(db.database());
+
+ @Test
+ void execute_shouldCreateTable() throws SQLException {
+ db.assertTableDoesNotExist(TABLE_NAME);
+ underTest.execute();
+ db.assertTableExists(TABLE_NAME);
+ db.assertPrimaryKey(TABLE_NAME, "pk_sca_lic_prof_projects", "uuid");
+ db.assertColumnDefinition(TABLE_NAME, "uuid", VARCHAR, UUID_SIZE, false);
+ db.assertColumnDefinition(TABLE_NAME, "project_uuid", VARCHAR, UUID_SIZE, false);
+ db.assertColumnDefinition(TABLE_NAME, "sca_license_profile_uuid", VARCHAR, UUID_SIZE, false);
+ db.assertColumnDefinition(TABLE_NAME, "created_at", BIGINT, null, false);
+ db.assertColumnDefinition(TABLE_NAME, "updated_at", BIGINT, null, false);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertTableDoesNotExist(TABLE_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertTableExists(TABLE_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfilesTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfilesTableIT.java
new file mode 100644
index 00000000000..8b0be344cd7
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfilesTableIT.java
@@ -0,0 +1,61 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.BIGINT;
+import static java.sql.Types.BOOLEAN;
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+
+class CreateScaLicenseProfilesTableIT {
+ private static final String TABLE_NAME = "sca_license_profiles";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(CreateScaLicenseProfilesTable.class);
+ private final DdlChange underTest = new CreateScaLicenseProfilesTable(db.database());
+
+ @Test
+ void execute_shouldCreateTable() throws SQLException {
+ db.assertTableDoesNotExist(TABLE_NAME);
+ underTest.execute();
+ db.assertTableExists(TABLE_NAME);
+ db.assertPrimaryKey(TABLE_NAME, "pk_sca_license_profiles", "uuid");
+ db.assertColumnDefinition(TABLE_NAME, "uuid", VARCHAR, UUID_SIZE, false);
+ db.assertColumnDefinition(TABLE_NAME, "is_default_profile", BOOLEAN, null, false);
+ db.assertColumnDefinition(TABLE_NAME, "name", VARCHAR, 400, false);
+ db.assertColumnDefinition(TABLE_NAME, "created_at", BIGINT, null, false);
+ db.assertColumnDefinition(TABLE_NAME, "updated_at", BIGINT, null, false);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertTableDoesNotExist(TABLE_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertTableExists(TABLE_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnArchitectureGraphsIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnArchitectureGraphsIT.java
new file mode 100644
index 00000000000..8cd566df9ce
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnArchitectureGraphsIT.java
@@ -0,0 +1,56 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class CreateUniqueIndexOnArchitectureGraphsIT {
+ private static final String TABLE_NAME = "architecture_graphs";
+ private static final String INDEX_NAME = "uq_idx_ag_brch_tp_src_pspctv";
+ private static final String COLUMN_NAME_BRANCH_UUID = "branch_uuid";
+ private static final String COLUMN_NAME_TYPE = "type";
+ private static final String COLUMN_NAME_ECOSYSTEM = "ecosystem";
+ private static final String COLUMN_NAME_PERSPECTIVE_KEY = "perspective_key";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(CreateUniqueIndexOnArchitectureGraphs.class);
+ private final DdlChange underTest = new CreateUniqueIndexOnArchitectureGraphs(db.database());
+
+ @Test
+ void execute_shouldCreateIndex() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_BRANCH_UUID, COLUMN_NAME_TYPE, COLUMN_NAME_ECOSYSTEM, COLUMN_NAME_PERSPECTIVE_KEY);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_BRANCH_UUID, COLUMN_NAME_TYPE, COLUMN_NAME_ECOSYSTEM, COLUMN_NAME_PERSPECTIVE_KEY);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaAnalysesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaAnalysesIT.java
new file mode 100644
index 00000000000..7ccc34ec67e
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaAnalysesIT.java
@@ -0,0 +1,52 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaAnalyses.COLUMN_NAME_COMPONENT_UUID;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaAnalyses.INDEX_NAME;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaAnalyses.TABLE_NAME;
+
+class CreateUniqueIndexOnScaAnalysesIT {
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(CreateUniqueIndexOnScaAnalyses.class);
+ private final DdlChange underTest = new CreateUniqueIndexOnScaAnalyses(db.database());
+
+ @Test
+ void execute_shouldCreateIndex() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_COMPONENT_UUID);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_COMPONENT_UUID);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaEncounteredLicensesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaEncounteredLicensesIT.java
new file mode 100644
index 00000000000..25c4f28626a
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaEncounteredLicensesIT.java
@@ -0,0 +1,52 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaEncounteredLicenses.COLUMN_LICENSE_POLICY_ID_NAME;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaEncounteredLicenses.INDEX_NAME;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaEncounteredLicenses.TABLE_NAME;
+
+class CreateUniqueIndexOnScaEncounteredLicensesIT {
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(CreateUniqueIndexOnScaEncounteredLicenses.class);
+ private final DdlChange underTest = new CreateUniqueIndexOnScaEncounteredLicenses(db.database());
+
+ @Test
+ void execute_shouldCreateIndex() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_LICENSE_POLICY_ID_NAME);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_LICENSE_POLICY_ID_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfileCategoriesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfileCategoriesIT.java
new file mode 100644
index 00000000000..99b78361007
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfileCategoriesIT.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaLicenseProfileCategories.COLUMN_CATEGORY_NAME;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaLicenseProfileCategories.COLUMN_LICENSE_PROFILE_UUID_NAME;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaLicenseProfileCategories.INDEX_NAME;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaLicenseProfileCategories.TABLE_NAME;
+
+class CreateUniqueIndexOnScaLicenseProfileCategoriesIT {
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(CreateUniqueIndexOnScaLicenseProfileCategories.class);
+ private final DdlChange underTest = new CreateUniqueIndexOnScaLicenseProfileCategories(db.database());
+
+ @Test
+ void execute_shouldCreateIndex() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_LICENSE_PROFILE_UUID_NAME, COLUMN_CATEGORY_NAME);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_LICENSE_PROFILE_UUID_NAME, COLUMN_CATEGORY_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfileCustomizationsIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfileCustomizationsIT.java
new file mode 100644
index 00000000000..dd6e291e9fa
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfileCustomizationsIT.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaLicenseProfileCustomizations.COLUMN_LICENSE_POLICY_ID_NAME;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaLicenseProfileCustomizations.COLUMN_LICENSE_PROFILE_UUID_NAME;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaLicenseProfileCustomizations.INDEX_NAME;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaLicenseProfileCustomizations.TABLE_NAME;
+
+class CreateUniqueIndexOnScaLicenseProfileCustomizationsIT {
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(CreateUniqueIndexOnScaLicenseProfileCustomizations.class);
+ private final DdlChange underTest = new CreateUniqueIndexOnScaLicenseProfileCustomizations(db.database());
+
+ @Test
+ void execute_shouldCreateIndex() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_LICENSE_PROFILE_UUID_NAME, COLUMN_LICENSE_POLICY_ID_NAME);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_LICENSE_PROFILE_UUID_NAME, COLUMN_LICENSE_POLICY_ID_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfileProjectsIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfileProjectsIT.java
new file mode 100644
index 00000000000..cadfe576a35
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfileProjectsIT.java
@@ -0,0 +1,52 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaLicenseProfileProjects.COLUMN_PROJECT_UUID_NAME;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaLicenseProfileProjects.INDEX_NAME;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaLicenseProfileProjects.TABLE_NAME;
+
+class CreateUniqueIndexOnScaLicenseProfileProjectsIT {
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(CreateUniqueIndexOnScaLicenseProfileProjects.class);
+ private final DdlChange underTest = new CreateUniqueIndexOnScaLicenseProfileProjects(db.database());
+
+ @Test
+ void execute_shouldCreateIndex() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_PROJECT_UUID_NAME);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_PROJECT_UUID_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfilesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfilesIT.java
new file mode 100644
index 00000000000..9058a6f851d
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfilesIT.java
@@ -0,0 +1,52 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaLicenseProfiles.COLUMN_NAME_NAME;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaLicenseProfiles.INDEX_NAME;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaLicenseProfiles.TABLE_NAME;
+
+class CreateUniqueIndexOnScaLicenseProfilesIT {
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(CreateUniqueIndexOnScaLicenseProfiles.class);
+ private final DdlChange underTest = new CreateUniqueIndexOnScaLicenseProfiles(db.database());
+
+ @Test
+ void execute_shouldCreateIndex() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_NAME);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaReleasesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaReleasesIT.java
new file mode 100644
index 00000000000..9588ff8fe88
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaReleasesIT.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaReleases.COLUMN_NAME_COMPONENT_UUID;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaReleases.COLUMN_NAME_PACKAGE_URL;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaReleases.INDEX_NAME;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaReleases.TABLE_NAME;
+
+class CreateUniqueIndexOnScaReleasesIT {
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(CreateUniqueIndexOnScaReleases.class);
+ private final DdlChange underTest = new CreateUniqueIndexOnScaReleases(db.database());
+
+ @Test
+ void execute_shouldCreateIndex() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_PACKAGE_URL, COLUMN_NAME_COMPONENT_UUID);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_PACKAGE_URL, COLUMN_NAME_COMPONENT_UUID);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropAssigneeNameFromScaIssuesReleasesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropAssigneeNameFromScaIssuesReleasesIT.java
new file mode 100644
index 00000000000..472b716adac
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropAssigneeNameFromScaIssuesReleasesIT.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class DropAssigneeNameFromScaIssuesReleasesIT {
+ private static final String TABLE_NAME = "sca_issues_releases";
+ private static final String COLUMN_NAME = "assignee_name";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(DropAssigneeNameFromScaIssuesReleases.class);
+ private final DdlChange underTest = new DropAssigneeNameFromScaIssuesReleases(db.database());
+
+ @Test
+ void execute_shouldDropColumn() throws SQLException {
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 200, true);
+ underTest.execute();
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 200, true);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropChangeTypeFromScaIssuesReleasesChangesTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropChangeTypeFromScaIssuesReleasesChangesTableIT.java
new file mode 100644
index 00000000000..a16ebb131a4
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropChangeTypeFromScaIssuesReleasesChangesTableIT.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class DropChangeTypeFromScaIssuesReleasesChangesTableIT {
+ private static final String TABLE_NAME = "sca_issue_rels_changes";
+ private static final String COLUMN_NAME = "change_type";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(DropChangeTypeFromScaIssuesReleasesChangesTable.class);
+ private final DdlChange underTest = new DropChangeTypeFromScaIssuesReleasesChangesTable(db.database());
+
+ @Test
+ void execute_shouldDropColumn() throws SQLException {
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 40, false);
+ underTest.execute();
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 40, false);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropIndexOnArchitectureGraphsIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropIndexOnArchitectureGraphsIT.java
new file mode 100644
index 00000000000..646be1f2f7c
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropIndexOnArchitectureGraphsIT.java
@@ -0,0 +1,59 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class DropIndexOnArchitectureGraphsIT {
+
+ private static final String TABLE_NAME = "architecture_graphs";
+ private static final String COLUMN_NAME_BRANCH_UUID = "branch_uuid";
+ private static final String COLUMN_NAME_TYPE = "type";
+ private static final String COLUMN_NAME_SOURCE = "source";
+ private static final String INDEX_NAME = "uq_idx_ag_branch_type_source";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(DropIndexOnArchitectureGraphs.class);
+ private final DropIndexOnArchitectureGraphs underTest = new DropIndexOnArchitectureGraphs(db.database());
+
+ @Test
+ void index_is_dropped() throws SQLException {
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_BRANCH_UUID, COLUMN_NAME_TYPE, COLUMN_NAME_SOURCE);
+
+ underTest.execute();
+
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ }
+
+ @Test
+ void migration_is_reentrant() throws SQLException {
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_BRANCH_UUID, COLUMN_NAME_TYPE, COLUMN_NAME_SOURCE);
+
+ underTest.execute();
+ underTest.execute();
+
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaDependenciesTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaDependenciesTableIT.java
new file mode 100644
index 00000000000..1c64c708ad8
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaDependenciesTableIT.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.BOOLEAN;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class DropNewInPullRequestFromScaDependenciesTableIT {
+ private static final String TABLE_NAME = "sca_dependencies";
+ private static final String COLUMN_NAME = "new_in_pull_request";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(DropNewInPullRequestFromScaDependenciesTable.class);
+ private final DdlChange underTest = new DropNewInPullRequestFromScaDependenciesTable(db.database());
+
+ @Test
+ void execute_shouldDropColumn() throws SQLException {
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BOOLEAN, null, false);
+ underTest.execute();
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BOOLEAN, null, false);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaReleasesTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaReleasesTableIT.java
new file mode 100644
index 00000000000..b0a579b14cb
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaReleasesTableIT.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.BOOLEAN;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class DropNewInPullRequestFromScaReleasesTableIT {
+ private static final String TABLE_NAME = "sca_releases";
+ private static final String COLUMN_NAME = "new_in_pull_request";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(DropNewInPullRequestFromScaReleasesTable.class);
+ private final DdlChange underTest = new DropNewInPullRequestFromScaReleasesTable(db.database());
+
+ @Test
+ void execute_shouldDropColumn() throws SQLException {
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BOOLEAN, null, false);
+ underTest.execute();
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BOOLEAN, null, false);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/MigrateRemoveDuplicateScaReleasesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/MigrateRemoveDuplicateScaReleasesIT.java
new file mode 100644
index 00000000000..0daf88d66d8
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/MigrateRemoveDuplicateScaReleasesIT.java
@@ -0,0 +1,138 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.MigrationStep;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class MigrateRemoveDuplicateScaReleasesIT {
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(MigrateRemoveDuplicateScaReleases.class);
+ private final MigrationStep underTest = new MigrateRemoveDuplicateScaReleases(db.database());
+
+ @Test
+ void test_removesDuplicates() throws SQLException {
+ // we should keep this one
+ insertRelease("0", "componentUuid1", "packageUrlNotDuplicated", 1L);
+ // we should keep these rows associated with release 0
+ insertDependency("0", "scaReleaseUuid0");
+ insertIssueRelease("0", "scaReleaseUuid0");
+ insertIssueReleaseChange("0");
+ // we should keep the first (oldest) packageUrl1 entry on componentUuid1
+ insertRelease("1", "componentUuid1", "packageUrl1", 2L);
+ insertRelease("2", "componentUuid1", "packageUrl1", 3L);
+ insertRelease("3", "componentUuid1", "packageUrl1", 4L);
+ // we should delete these rows associated with release 3 that we delete
+ insertDependency("3", "scaReleaseUuid3");
+ insertIssueRelease("3", "scaReleaseUuid3");
+ insertIssueReleaseChange("3");
+ // we should keep the first (oldest) packageUrl2 entry on componentUuid1
+ insertRelease("4", "componentUuid1", "packageUrl2", 5L);
+ insertRelease("5", "componentUuid1", "packageUrl2", 6L);
+ // we should keep the first (oldest) packageUrl1 entry on componentUuid2
+ insertRelease("6", "componentUuid2", "packageUrl1", 7L);
+ insertRelease("7", "componentUuid2", "packageUrl1", 8L);
+ // we should keep these rows associated with release 6
+ insertDependency("6", "scaReleaseUuid6");
+ insertIssueRelease("6", "scaReleaseUuid6");
+ insertIssueReleaseChange("6");
+ // we should delete these rows associated with release 7 that we delete
+ insertDependency("7", "scaReleaseUuid7");
+ insertIssueRelease("7", "scaReleaseUuid7");
+ insertIssueReleaseChange("7");
+
+ assertThat(db.countSql("select count(*) from sca_releases")).isEqualTo(8);
+ assertThat(db.countSql("select count(*) from sca_dependencies")).isEqualTo(4);
+ assertThat(db.countSql("select count(*) from sca_issues_releases")).isEqualTo(4);
+ assertThat(db.countSql("select count(*) from sca_issue_rels_changes")).isEqualTo(4);
+ underTest.execute();
+
+ assertThat(db.select("select uuid from sca_releases")).map(row -> row.get("uuid"))
+ .containsExactlyInAnyOrder("scaReleaseUuid0", "scaReleaseUuid1", "scaReleaseUuid4", "scaReleaseUuid6");
+ assertThat(db.select("select uuid from sca_dependencies")).map(row -> row.get("uuid"))
+ .containsExactlyInAnyOrder("scaDependencyUuid0", "scaDependencyUuid6");
+ assertThat(db.select("select uuid from sca_issues_releases")).map(row -> row.get("uuid"))
+ .containsExactlyInAnyOrder("scaIssueReleaseUuid0", "scaIssueReleaseUuid6");
+ assertThat(db.select("select uuid from sca_issue_rels_changes")).map(row -> row.get("uuid"))
+ .containsExactlyInAnyOrder("scaIssueReleaseChangeUuid0", "scaIssueReleaseChangeUuid6");
+ }
+
+ @Test
+ void test_canRunMultipleTimesOnEmptyTable() throws SQLException {
+ assertThat(db.countSql("select count(*) from sca_releases")).isZero();
+ underTest.execute();
+ underTest.execute();
+ assertThat(db.countSql("select count(*) from sca_releases")).isZero();
+ }
+
+ private void insertRelease(String suffix, String componentUuid, String packageUrl, long createdAt) {
+ db.executeInsert("sca_releases",
+ "uuid", "scaReleaseUuid" + suffix,
+ "component_uuid", componentUuid,
+ "package_url", packageUrl,
+ "package_manager", "MAVEN",
+ "package_name", "packageName",
+ "version", "1.0.0",
+ "license_expression", "MIT",
+ "declared_license_expression", "MIT",
+ "is_new", false,
+ "known", true,
+ "known_package", true,
+ "updated_at", 1L,
+ "created_at", createdAt);
+ }
+
+ private void insertDependency(String suffix, String releaseUuid) {
+ db.executeInsert("sca_dependencies",
+ "uuid", "scaDependencyUuid" + suffix,
+ "sca_release_uuid", releaseUuid,
+ "direct", true,
+ "scope", "compile",
+ "is_new", false,
+ "updated_at", 1L,
+ "created_at", 2L);
+ }
+
+ private void insertIssueRelease(String suffix, String releaseUuid) {
+ db.executeInsert("sca_issues_releases",
+ "uuid", "scaIssueReleaseUuid" + suffix,
+ "sca_release_uuid", releaseUuid,
+ "sca_issue_uuid", "scaIssueUuid" + suffix,
+ "severity", "LOW",
+ "severity_sort_key", 10,
+ "status", "OPEN",
+ "updated_at", 1L,
+ "created_at", 2L);
+ }
+
+ private void insertIssueReleaseChange(String suffix) {
+ db.executeInsert("sca_issue_rels_changes",
+ "uuid", "scaIssueReleaseChangeUuid" + suffix,
+ "sca_issues_releases_uuid", "scaIssueReleaseUuid" + suffix,
+ "updated_at", 1L,
+ "created_at", 2L);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/MigrateRemoveNonCanonicalScaEncounteredLicensesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/MigrateRemoveNonCanonicalScaEncounteredLicensesIT.java
new file mode 100644
index 00000000000..d18b45af262
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/MigrateRemoveNonCanonicalScaEncounteredLicensesIT.java
@@ -0,0 +1,69 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.MigrationStep;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class MigrateRemoveNonCanonicalScaEncounteredLicensesIT {
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(MigrateRemoveNonCanonicalScaEncounteredLicenses.class);
+ private final MigrationStep underTest = new MigrateRemoveNonCanonicalScaEncounteredLicenses(db.database());
+
+ @Test
+ void test_removesNonCanonical() throws SQLException {
+ // we should keep these
+ insertEncounteredLicense("0", "GPL-2.0-only");
+ insertEncounteredLicense("1", "LicenseRef-something");
+ insertEncounteredLicense("2", "LicenseRef-something-with-something-else");
+
+ // we should delete these
+ insertEncounteredLicense("3", "GPL-2.0-with-classpath-exception");
+ insertEncounteredLicense("4", "GPL-2.0-with-autoconf-exception");
+
+ assertThat(db.countSql("select count(*) from sca_encountered_licenses")).isEqualTo(5);
+ underTest.execute();
+
+ assertThat(db.select("select uuid from sca_encountered_licenses")).map(row -> row.get("uuid"))
+ .containsExactlyInAnyOrder("scaEncounteredLicenseUuid0", "scaEncounteredLicenseUuid1", "scaEncounteredLicenseUuid2");
+ }
+
+ @Test
+ void test_canRunMultipleTimesOnEmptyTable() throws SQLException {
+ assertThat(db.countSql("select count(*) from sca_encountered_licenses")).isZero();
+ underTest.execute();
+ underTest.execute();
+ assertThat(db.countSql("select count(*) from sca_encountered_licenses")).isZero();
+ }
+
+ private void insertEncounteredLicense(String suffix, String licensePolicyId) {
+ db.executeInsert("sca_encountered_licenses",
+ "uuid", "scaEncounteredLicenseUuid" + suffix,
+ "license_policy_id", licensePolicyId,
+ "updated_at", 1L,
+ "created_at", 1L);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/MigrateToIsNewOnScaDependenciesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/MigrateToIsNewOnScaDependenciesIT.java
new file mode 100644
index 00000000000..e50f8fe6d4d
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/MigrateToIsNewOnScaDependenciesIT.java
@@ -0,0 +1,82 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import java.util.stream.Stream;
+import javax.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DataChange;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class MigrateToIsNewOnScaDependenciesIT {
+ private static final String COLUMN_NAME = "is_new";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(MigrateToIsNewOnScaDependencies.class);
+ private final DataChange underTest = new MigrateToIsNewOnScaDependencies(db.database());
+
+ private static Stream<Arguments> provideParamsForExecute() {
+ return Stream.of(
+ Arguments.of(false, false, false),
+ Arguments.of(false, true, false),
+ Arguments.of(true, false, true),
+ Arguments.of(true, true, true));
+ }
+
+ @Test
+ void execute_doesNotCreateRecords() throws SQLException {
+ underTest.execute();
+ assertThat(db.countSql("select count(*) from sca_dependencies")).isZero();
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideParamsForExecute")
+ void execute_updatesCorrectly(boolean newInPullRequest, boolean isNew, boolean expectedIsNew) throws SQLException {
+ insertDependency("1", newInPullRequest, isNew);
+
+ assertThat(db.selectFirst("select * from sca_dependencies where uuid = '%s'".formatted("scaReleaseUuid1"))).containsEntry(COLUMN_NAME, isNew);
+ underTest.execute();
+ assertThat(db.selectFirst("select * from sca_dependencies where uuid = '%s'".formatted("scaReleaseUuid1"))).containsEntry(COLUMN_NAME, expectedIsNew);
+ }
+
+ private void insertDependency(String suffix, boolean newInPullRequest, @Nullable Boolean isNew) {
+ db.executeInsert("sca_dependencies",
+ "uuid", "scaReleaseUuid" + suffix,
+ "sca_release_uuid", "componentUuid" + suffix,
+ "direct", true,
+ "scope", "development",
+ "production_scope", true,
+ "user_dependency_file_path", "Gemfile",
+ "lockfile_dependency_file_path", "Gemfile.lock",
+ "chains", "[]",
+ "is_new", isNew,
+ "new_in_pull_request", newInPullRequest,
+ "updated_at", 1L,
+ "created_at", 2L);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/MigrateToIsNewOnScaReleasesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/MigrateToIsNewOnScaReleasesIT.java
new file mode 100644
index 00000000000..50f2c66a8b4
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/MigrateToIsNewOnScaReleasesIT.java
@@ -0,0 +1,84 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import java.util.stream.Stream;
+import javax.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DataChange;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class MigrateToIsNewOnScaReleasesIT {
+ private static final String COLUMN_NAME = "is_new";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(MigrateToIsNewOnScaReleases.class);
+ private final DataChange underTest = new MigrateToIsNewOnScaReleases(db.database());
+
+ private static Stream<Arguments> provideParamsForExecute() {
+ return Stream.of(
+ Arguments.of(false, false, false),
+ Arguments.of(false, true, false),
+ Arguments.of(true, false, true),
+ Arguments.of(true, true, true));
+ }
+
+ @Test
+ void execute_doesNotCreateRecords() throws SQLException {
+ underTest.execute();
+ assertThat(db.countSql("select count(*) from sca_releases")).isZero();
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideParamsForExecute")
+ void execute_updatesCorrectly(boolean newInPullRequest, boolean isNew, boolean expectedIsNew) throws SQLException {
+ insertRelease("1", newInPullRequest, isNew);
+
+ assertThat(db.selectFirst("select * from sca_releases where uuid = '%s'".formatted("scaReleaseUuid1"))).containsEntry(COLUMN_NAME, isNew);
+ underTest.execute();
+ assertThat(db.selectFirst("select * from sca_releases where uuid = '%s'".formatted("scaReleaseUuid1"))).containsEntry(COLUMN_NAME, expectedIsNew);
+ }
+
+ private void insertRelease(String suffix, boolean newInPullRequest, @Nullable Boolean isNew) {
+ db.executeInsert("sca_releases",
+ "uuid", "scaReleaseUuid" + suffix,
+ "component_uuid", "componentUuid" + suffix,
+ "package_url", "packageUrl",
+ "package_manager", "MAVEN",
+ "package_name", "packageName" + suffix,
+ "version", "1.0.0",
+ "license_expression", "MIT",
+ "declared_license_expression", "MIT",
+ "is_new", isNew,
+ "new_in_pull_request", newInPullRequest,
+ "known", true,
+ "known_package", true,
+ "updated_at", 1L,
+ "created_at", 2L);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTableIT.java
new file mode 100644
index 00000000000..483d8a1afb8
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTableIT.java
@@ -0,0 +1,72 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTableIT {
+ @RegisterExtension
+ public final MigrationDbTester db = MigrationDbTester.createForMigrationStep(PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTable.class);
+ private final PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTable underTest = new PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTable(db.database());
+
+ @Test
+ void execute_shouldPopulatePolicyUpdatedAtWithUpdatedAt() throws SQLException {
+ insertScaLicenseProfile(1);
+ insertScaLicenseProfile(2);
+
+ underTest.execute();
+
+ assertThatPolicyUpdatedAtIsPopulated();
+ }
+
+ @Test
+ void execute_whenAlreadyExecuted_shouldBeIdempotent() throws SQLException {
+ insertScaLicenseProfile(1);
+
+ underTest.execute();
+ underTest.execute();
+
+ assertThatPolicyUpdatedAtIsPopulated();
+ }
+
+ private void insertScaLicenseProfile(Integer index) {
+ db.executeInsert("sca_license_profiles",
+ "uuid", "uuid-" + index,
+ "is_default_profile", false,
+ "name", "licenseProfile-" + index,
+ "created_at", 1L,
+ "updated_at", 2L,
+ "policy_updated_at", null);
+ }
+
+ private void assertThatPolicyUpdatedAtIsPopulated() {
+ List<Map<String, Object>> rows = db.select("select policy_updated_at, updated_at from sca_license_profiles");
+ assertThat(rows).isNotEmpty()
+ .allSatisfy(row -> assertEquals(row.get("policy_updated_at"), row.get("updated_at")));
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/PopulateStatusColumnForScaIssuesReleasesTableTestIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/PopulateStatusColumnForScaIssuesReleasesTableTestIT.java
new file mode 100644
index 00000000000..80a32af2464
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/PopulateStatusColumnForScaIssuesReleasesTableTestIT.java
@@ -0,0 +1,75 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class PopulateStatusColumnForScaIssuesReleasesTableTestIT {
+ @RegisterExtension
+ public final MigrationDbTester db = MigrationDbTester.createForMigrationStep(PopulateStatusColumnForScaIssuesReleasesTable.class);
+
+ private final PopulateStatusColumnForScaIssuesReleasesTable underTest = new PopulateStatusColumnForScaIssuesReleasesTable(db.database());
+
+ @Test
+ void execute_shouldPopulateStatusWithToReview() throws SQLException {
+ insertScaIssuesReleases(1);
+ insertScaIssuesReleases(2);
+
+ underTest.execute();
+
+ assertThatStatusIsPopulated();
+ }
+
+ @Test
+ void execute_whenAlreadyExecuted_shouldBeIdempotent() throws SQLException {
+ insertScaIssuesReleases(1);
+
+ underTest.execute();
+ underTest.execute();
+
+ assertThatStatusIsPopulated();
+ }
+
+ private void insertScaIssuesReleases(Integer index) {
+ db.executeInsert("sca_issues_releases",
+ "uuid", "uuid-" + index,
+ "sca_issue_uuid", "issue_id" + index,
+ "sca_release_uuid", "release_id",
+ "severity", "severity",
+ "severity_sort_key", 1,
+ "created_at", new Date().getTime(),
+ "updated_at", new Date().getTime()
+ );
+ }
+
+ private void assertThatStatusIsPopulated() {
+ List<Map<String, Object>> rows = db.select("select status from sca_issues_releases");
+ assertThat(rows).isNotEmpty()
+ .allSatisfy(row -> assertThat(row).containsEntry("status", "TO_REVIEW"));
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/UpdateArchitectureGraphsSourceColumnRenameIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/UpdateArchitectureGraphsSourceColumnRenameIT.java
new file mode 100644
index 00000000000..81c377724e9
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/UpdateArchitectureGraphsSourceColumnRenameIT.java
@@ -0,0 +1,63 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class UpdateArchitectureGraphsSourceColumnRenameIT {
+
+ private static final String TABLE_NAME = "architecture_graphs";
+ private static final String OLD_COLUMN = "source";
+ private static final String NEW_COLUMN = "ecosystem";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(UpdateArchitectureGraphsSourceColumnRename.class);
+ private final DdlChange underTest = new UpdateArchitectureGraphsSourceColumnRename(db.database());
+
+ @Test
+ void execute_shouldUpdateColumn() throws SQLException {
+ db.assertColumnDefinition(TABLE_NAME, OLD_COLUMN, VARCHAR, null, null);
+ db.assertColumnDoesNotExist(TABLE_NAME, NEW_COLUMN);
+
+ underTest.execute();
+
+ db.assertColumnDoesNotExist(TABLE_NAME, OLD_COLUMN);
+ db.assertColumnDefinition(TABLE_NAME, NEW_COLUMN, VARCHAR, null, null);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDefinition(TABLE_NAME, OLD_COLUMN, VARCHAR, null, null);
+ db.assertColumnDoesNotExist(TABLE_NAME, NEW_COLUMN);
+
+ underTest.execute();
+ underTest.execute();
+
+ db.assertColumnDoesNotExist(TABLE_NAME, OLD_COLUMN);
+ db.assertColumnDefinition(TABLE_NAME, NEW_COLUMN, VARCHAR, null, null);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaIssuesReleasesOpenStatusIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaIssuesReleasesOpenStatusIT.java
new file mode 100644
index 00000000000..43069c4911a
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaIssuesReleasesOpenStatusIT.java
@@ -0,0 +1,81 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DataChange;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class UpdateScaIssuesReleasesOpenStatusIT {
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(UpdateScaIssuesReleasesOpenStatus.class);
+ private final DataChange underTest = new UpdateScaIssuesReleasesOpenStatus(db.database());
+
+ @Test
+ void execute_doesNotCreateRecords() throws SQLException {
+ underTest.execute();
+ assertThat(db.countSql("select count(*) from sca_issues_releases")).isZero();
+ }
+
+ @Test
+ void execute_doesNotOverwriteOtherStatus() throws SQLException {
+ insertScaIssuesReleases("FIXED");
+
+ underTest.execute();
+
+ assertAllStatusesEqual("FIXED");
+ }
+
+ @Test
+ void execute_whenAlreadyExecuted_shouldBeIdempotent() throws SQLException {
+ insertScaIssuesReleases("TO_REVIEW");
+ underTest.execute();
+ assertAllStatusesEqual("OPEN");
+ underTest.execute();
+ assertAllStatusesEqual("OPEN");
+ }
+
+ private void insertScaIssuesReleases(String status) {
+ db.executeInsert("sca_issues_releases",
+ "uuid", "uuid",
+ "sca_issue_uuid", "issue_id",
+ "sca_release_uuid", "release_id",
+ "severity", "severity",
+ "severity_sort_key", 1,
+ "status", status,
+ "created_at", new Date().getTime(),
+ "updated_at", new Date().getTime()
+ );
+ }
+
+ private void assertAllStatusesEqual(String status) {
+ List<Map<String, Object>> rows = db.select("select status from sca_issues_releases");
+ assertThat(rows).isNotEmpty()
+ .allSatisfy(row -> assertThat(row).containsEntry("status", status));
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaIssuesReleasesStatusColumnNotNullableTestIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaIssuesReleasesStatusColumnNotNullableTestIT.java
new file mode 100644
index 00000000000..eb1abcab7ec
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaIssuesReleasesStatusColumnNotNullableTestIT.java
@@ -0,0 +1,68 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.sql.DropColumnsBuilder;
+
+import static java.sql.Types.VARCHAR;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+class UpdateScaIssuesReleasesStatusColumnNotNullableTestIT {
+ static final String TABLE_NAME = "sca_issues_releases";
+ static final String COLUMN_NAME = "status";
+ static final int COLUMN_SIZE = 40;
+
+ @RegisterExtension
+ public final MigrationDbTester db = MigrationDbTester.createForMigrationStep(UpdateScaIssuesReleasesStatusColumnNotNullable.class);
+
+ private final UpdateScaIssuesReleasesStatusColumnNotNullable underTest = new UpdateScaIssuesReleasesStatusColumnNotNullable(db.database());
+
+ @Test
+ void execute_whenColumnExists_shouldMakeColumnNotNull() throws SQLException {
+ // Verify column is nullable before update
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, COLUMN_SIZE, true);
+
+ underTest.execute();
+
+ // Verify column is not nullable after update
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, COLUMN_SIZE, false);
+ }
+
+ @Test
+ void execute_whenColumnDoesNotExist_shouldNotFail() throws SQLException {
+ // Ensure the column does not exist before executing the migration
+ DropColumnsBuilder dropColumnsBuilder = new DropColumnsBuilder(db.database().getDialect(), TABLE_NAME, COLUMN_NAME);
+ dropColumnsBuilder.build().forEach(db::executeDdl);
+
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ assertThatCode(underTest::execute).doesNotThrowAnyException();
+ }
+
+ @Test
+ void execute_whenExecutedTwice_shouldBeIdempotent() throws SQLException {
+ underTest.execute();
+ assertThatCode(underTest::execute).doesNotThrowAnyException();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, COLUMN_SIZE, false);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullableIT.java
new file mode 100644
index 00000000000..4d2d61106be
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullableIT.java
@@ -0,0 +1,66 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.sql.DropColumnsBuilder;
+
+import static java.sql.Types.BIGINT;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+class UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullableIT {
+ static final String TABLE_NAME = "sca_license_profiles";
+ static final String COLUMN_NAME = "policy_updated_at";
+
+ @RegisterExtension
+ public final MigrationDbTester db = MigrationDbTester.createForMigrationStep(UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullable.class);
+ private final UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullable underTest = new UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullable(db.database());
+
+ @Test
+ void execute_whenColumnExists_shouldMakeColumnNotNull() throws SQLException {
+ // Verify column is nullable before update
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BIGINT, null, true);
+
+ underTest.execute();
+
+ // Verify column is not nullable after update
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BIGINT, null, false);
+ }
+
+ @Test
+ void execute_whenColumnDoesNotExist_shouldNotFail() throws SQLException {
+ // Ensure the column does not exist before executing the migration
+ DropColumnsBuilder dropColumnsBuilder = new DropColumnsBuilder(db.database().getDialect(), TABLE_NAME, COLUMN_NAME);
+ dropColumnsBuilder.build().forEach(db::executeDdl);
+
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ assertThatCode(underTest::execute).doesNotThrowAnyException();
+ }
+
+ @Test
+ void execute_whenExecutedTwice_shouldBeIdempotent() throws SQLException {
+ underTest.execute();
+ assertThatCode(underTest::execute).doesNotThrowAnyException();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BIGINT, null, false);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddAnalysisParametersToScaAnalysesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddAnalysisParametersToScaAnalysesIT.java
new file mode 100644
index 00000000000..aba4b15194f
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddAnalysisParametersToScaAnalysesIT.java
@@ -0,0 +1,54 @@
+/*
+ * 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.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.CLOB;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class AddAnalysisParametersToScaAnalysesIT {
+ private static final String TABLE_NAME = "sca_analyses";
+ private static final String COLUMN_NAME = "analysis_parameters";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddAnalysisParametersToScaAnalyses.class);
+ private final DdlChange underTest = new AddAnalysisParametersToScaAnalyses(db.database());
+
+ @Test
+ void execute_shouldAddColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, CLOB, null, true);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, CLOB, null, true);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddIndexOnAlmRepoInProjectAlmSettingsIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddIndexOnAlmRepoInProjectAlmSettingsIT.java
new file mode 100644
index 00000000000..eb1afa4db76
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddIndexOnAlmRepoInProjectAlmSettingsIT.java
@@ -0,0 +1,56 @@
+/*
+ * 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.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+import static org.sonar.server.platform.db.migration.version.v202504.AddIndexOnAlmRepoInProjectAlmSettings.COLUMN_NAME;
+import static org.sonar.server.platform.db.migration.version.v202504.AddIndexOnAlmRepoInProjectAlmSettings.TABLE_NAME;
+
+class AddIndexOnAlmRepoInProjectAlmSettingsIT {
+
+ private static final String INDEX_NAME = "project_alm_settings_alm_repo";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddIndexOnAlmRepoInProjectAlmSettings.class);
+
+ private final DdlChange underTest = new AddIndexOnAlmRepoInProjectAlmSettings(db.database());
+
+ @Test
+ void execute_shouldCreateIndex() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ db.assertIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME);
+ }
+
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddManualSeverityWarningToScaIssuesTest.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddManualSeverityWarningToScaIssuesTest.java
new file mode 100644
index 00000000000..55558aae959
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddManualSeverityWarningToScaIssuesTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.BOOLEAN;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class AddManualSeverityWarningToScaIssuesTest {
+ private static final String TABLE_NAME = "sca_issues_releases";
+ private static final String COLUMN_NAME = "show_increased_severity_warning";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddManualSeverityWarningToScaIssues.class);
+ private final DdlChange underTest = new AddManualSeverityWarningToScaIssues(db.database());
+
+ @Test
+ void execute_shouldAddColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BOOLEAN, 1, false);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BOOLEAN, 1, false);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddOrganizationUuidToScaLicenseProfilesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddOrganizationUuidToScaLicenseProfilesIT.java
new file mode 100644
index 00000000000..08f6d1c40de
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddOrganizationUuidToScaLicenseProfilesIT.java
@@ -0,0 +1,54 @@
+/*
+ * 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.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class AddOrganizationUuidToScaLicenseProfilesIT {
+ private static final String TABLE_NAME = "sca_license_profiles";
+ private static final String COLUMN_NAME = "organization_uuid";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddOrganizationUuidToScaLicenseProfiles.class);
+ private final DdlChange underTest = new AddOrganizationUuidToScaLicenseProfiles(db.database());
+
+ @Test
+ void execute_shouldAddColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 40, false);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 40, false);
+ }
+
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssuesTest.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssuesTest.java
new file mode 100644
index 00000000000..0fdbda384a9
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssuesTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+import static org.sonar.server.platform.db.migration.version.v202504.AddOriginalAndManualSeverityToScaIssues.MANUALLY_SET_COLUMN_NAME;
+import static org.sonar.server.platform.db.migration.version.v202504.AddOriginalAndManualSeverityToScaIssues.ORIGINAL_VALUE_COLUMN_NAME;
+import static org.sonar.server.platform.db.migration.version.v202504.AddOriginalAndManualSeverityToScaIssues.TABLE_NAME;
+
+class AddOriginalAndManualSeverityToScaIssuesTest {
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddOriginalAndManualSeverityToScaIssues.class);
+ private final DdlChange underTest = new AddOriginalAndManualSeverityToScaIssues(db.database());
+
+ @Test
+ void execute_shouldAddCalculatedValueColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, ORIGINAL_VALUE_COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, ORIGINAL_VALUE_COLUMN_NAME, VARCHAR, 15, true);
+ }
+
+ @Test
+ void execute_shouldAddManuallySetColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, MANUALLY_SET_COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, MANUALLY_SET_COLUMN_NAME, VARCHAR, 15, true);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, ORIGINAL_VALUE_COLUMN_NAME);
+ db.assertColumnDoesNotExist(TABLE_NAME, MANUALLY_SET_COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, ORIGINAL_VALUE_COLUMN_NAME, VARCHAR, 15, true);
+ db.assertColumnDefinition(TABLE_NAME, MANUALLY_SET_COLUMN_NAME, VARCHAR, 15, true);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddWithdrawnToScaVulnerabilityIssuesTest.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddWithdrawnToScaVulnerabilityIssuesTest.java
new file mode 100644
index 00000000000..e463be0ad37
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddWithdrawnToScaVulnerabilityIssuesTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.BOOLEAN;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class AddWithdrawnToScaVulnerabilityIssuesTest {
+ private static final String TABLE_NAME = "sca_vulnerability_issues";
+ private static final String COLUMN_NAME = "withdrawn";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddWithdrawnToScaVulnerabilityIssues.class);
+ private final DdlChange underTest = new AddWithdrawnToScaVulnerabilityIssues(db.database());
+
+ @Test
+ void execute_shouldAddColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BOOLEAN, 1, false);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BOOLEAN, 1, false);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/CreateUniqueIndexOnScaLicenseProfilesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/CreateUniqueIndexOnScaLicenseProfilesIT.java
new file mode 100644
index 00000000000..c944e1ea1da
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/CreateUniqueIndexOnScaLicenseProfilesIT.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+import static org.sonar.server.platform.db.migration.version.v202504.CreateUniqueIndexOnScaLicenseProfiles.COLUMN_NAME_NAME;
+import static org.sonar.server.platform.db.migration.version.v202504.CreateUniqueIndexOnScaLicenseProfiles.COLUMN_NAME_ORG;
+import static org.sonar.server.platform.db.migration.version.v202504.CreateUniqueIndexOnScaLicenseProfiles.INDEX_NAME;
+import static org.sonar.server.platform.db.migration.version.v202504.CreateUniqueIndexOnScaLicenseProfiles.TABLE_NAME;
+
+class CreateUniqueIndexOnScaLicenseProfilesIT {
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(org.sonar.server.platform.db.migration.version.v202504.CreateUniqueIndexOnScaLicenseProfiles.class);
+ private final DdlChange underTest = new CreateUniqueIndexOnScaLicenseProfiles(db.database());
+
+ @Test
+ void execute_shouldCreateIndex() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_ORG, COLUMN_NAME_NAME);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_ORG, COLUMN_NAME_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/DropUniqueIndexOnScaLicenseProfilesTest.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/DropUniqueIndexOnScaLicenseProfilesTest.java
new file mode 100644
index 00000000000..799adb2d35d
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/DropUniqueIndexOnScaLicenseProfilesTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class DropUniqueIndexOnScaLicenseProfilesTest {
+
+ private static final String TABLE_NAME = "sca_license_profiles";
+ private static final String INDEX_NAME = "sca_license_profiles_uniq";
+ private static final String COLUMN_NAME_NAME = "name";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(DropUniqueIndexOnScaLicenseProfiles.class);
+ private final DropUniqueIndexOnScaLicenseProfiles underTest = new DropUniqueIndexOnScaLicenseProfiles(db.database());
+
+ @Test
+ void index_is_dropped() throws SQLException {
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_NAME);
+
+ underTest.execute();
+
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ }
+
+ @Test
+ void migration_is_reentrant() throws SQLException {
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_NAME);
+
+ underTest.execute();
+ underTest.execute();
+
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTableIT.java
new file mode 100644
index 00000000000..31091c17fc1
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTableIT.java
@@ -0,0 +1,80 @@
+/*
+ * 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.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class PopulateOriginalSeverityForScaIssuesReleasesTableIT {
+ @RegisterExtension
+ public final MigrationDbTester db = MigrationDbTester.createForMigrationStep(PopulateOriginalSeverityForScaIssuesReleasesTable.class);
+ private final PopulateOriginalSeverityForScaIssuesReleasesTable underTest = new PopulateOriginalSeverityForScaIssuesReleasesTable(db.database());
+
+ @Test
+ void execute_shouldPopulateOriginaldSeverity() throws SQLException {
+ insertScaIssuesReleases(1, "HIGH", null);
+ insertScaIssuesReleases(2, "INFO", null);
+
+ underTest.execute();
+
+ assertThatOriginalSeverityIs(1, "HIGH");
+ assertThatOriginalSeverityIs(2, "INFO");
+ }
+
+ @Test
+ void execute_whenAlreadyExecuted_shouldBeIdempotent() throws SQLException {
+ insertScaIssuesReleases(1, "HIGH", "INFO");
+
+ underTest.execute();
+ underTest.execute();
+
+ assertThatOriginalSeverityIs(1, "INFO");
+ }
+
+ private void insertScaIssuesReleases(Integer index, String severity, @Nullable String originalSeverity) {
+ db.executeInsert("sca_issues_releases",
+ "uuid", "uuid-" + index,
+ "sca_issue_uuid", "issue_id" + index,
+ "sca_release_uuid", "release_id",
+ "status", "TO_REVIEW",
+ "severity", severity,
+ "original_severity", originalSeverity,
+ "manual_severity", null,
+ "severity_sort_key", 1,
+ "created_at", new Date().getTime(),
+ "updated_at", new Date().getTime()
+ );
+ }
+
+ private void assertThatOriginalSeverityIs(Integer index, String expectedSeverity) {
+ String uuid = "uuid-" + index;
+ List<Map<String, Object>> rows = db.select("select original_severity from sca_issues_releases where uuid = '%s'".formatted(uuid));
+ assertThat(rows).isNotEmpty()
+ .allSatisfy(row -> assertThat(row).containsEntry("original_severity", expectedSeverity));
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/UpdateRulesNameColumnSizeTest.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/UpdateRulesNameColumnSizeTest.java
new file mode 100644
index 00000000000..c087c930ef2
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/UpdateRulesNameColumnSizeTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class UpdateRulesNameColumnSizeTest {
+
+ private static final String TABLE_NAME = "rules";
+ private static final String COLUMN_NAME = "name";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(DropUniqueIndexOnScaLicenseProfiles.class);
+ private final UpdateRulesNameColumnSize updateRulesNameColumnSize = new UpdateRulesNameColumnSize(db.database());
+
+ @Test
+ void migration_updates_rules_name_column_size() throws SQLException {
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 200, true);
+
+ updateRulesNameColumnSize.execute();
+
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 255, true);
+ }
+
+ @Test
+ void migration_is_reentrant() throws SQLException {
+ updateRulesNameColumnSize.execute();
+ updateRulesNameColumnSize.execute();
+
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 255, true);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullableTestIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullableTestIT.java
new file mode 100644
index 00000000000..2e194f52ec6
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullableTestIT.java
@@ -0,0 +1,68 @@
+/*
+ * 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.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.sql.DropColumnsBuilder;
+
+import static java.sql.Types.VARCHAR;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+class UpdateScaIssuesReleasesOriginalSeverityColumnNotNullableTestIT {
+ static final String TABLE_NAME = "sca_issues_releases";
+ static final String COLUMN_NAME = "original_severity";
+ static final int COLUMN_SIZE = 15;
+
+ @RegisterExtension
+ public final MigrationDbTester db = MigrationDbTester.createForMigrationStep(UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable.class);
+
+ private final UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable underTest = new UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable(db.database());
+
+ @Test
+ void execute_whenColumnExists_shouldMakeColumnNotNull() throws SQLException {
+ // Verify column is nullable before update
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, COLUMN_SIZE, true);
+
+ underTest.execute();
+
+ // Verify column is not nullable after update
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, COLUMN_SIZE, false);
+ }
+
+ @Test
+ void execute_whenColumnDoesNotExist_shouldNotFail() throws SQLException {
+ // Ensure the column does not exist before executing the migration
+ DropColumnsBuilder dropColumnsBuilder = new DropColumnsBuilder(db.database().getDialect(), TABLE_NAME, COLUMN_NAME);
+ dropColumnsBuilder.build().forEach(db::executeDdl);
+
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ assertThatCode(underTest::execute).doesNotThrowAnyException();
+ }
+
+ @Test
+ void execute_whenExecutedTwice_shouldBeIdempotent() throws SQLException {
+ underTest.execute();
+ assertThatCode(underTest::execute).doesNotThrowAnyException();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, COLUMN_SIZE, false);
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MigrationConfigurationModule.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MigrationConfigurationModule.java
index fdfcd97907a..23c17a18540 100644
--- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MigrationConfigurationModule.java
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MigrationConfigurationModule.java
@@ -31,6 +31,7 @@ import org.sonar.server.platform.db.migration.version.v00.DbVersion00;
import org.sonar.server.platform.db.migration.version.v202501.DbVersion202501;
import org.sonar.server.platform.db.migration.version.v202502.DbVersion202502;
import org.sonar.server.platform.db.migration.version.v202503.DbVersion202503;
+import org.sonar.server.platform.db.migration.version.v202504.DbVersion202504;
public class MigrationConfigurationModule extends Module {
@Override
@@ -42,6 +43,7 @@ public class MigrationConfigurationModule extends Module {
DbVersion202501.class,
DbVersion202502.class,
DbVersion202503.class,
+ DbVersion202504.class,
// migration steps
MigrationStepRegistryImpl.class,
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/def/BooleanColumnDef.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/def/BooleanColumnDef.java
index 99c1d76d1b6..c1e93b5d3d2 100644
--- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/def/BooleanColumnDef.java
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/def/BooleanColumnDef.java
@@ -30,7 +30,7 @@ import org.sonar.db.dialect.PostgreSql;
import static org.sonar.server.platform.db.migration.def.Validations.validateColumnName;
/**
- * Used to define VARCHAR column
+ * Used to define BOOLEAN column
*/
@Immutable
public class BooleanColumnDef extends AbstractColumnDef {
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/sql/DropColumnsBuilder.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/sql/DropColumnsBuilder.java
index 5da5ab75c03..9be457e7574 100644
--- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/sql/DropColumnsBuilder.java
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/sql/DropColumnsBuilder.java
@@ -30,7 +30,11 @@ import org.sonar.db.dialect.Oracle;
import org.sonar.db.dialect.PostgreSql;
/**
- * Generate a SQL query to drop multiple columns from a table
+ * Generate a SQL query to drop multiple columns from a table. You probably want to
+ * use DropColumnChange instead, as it will handle dropping autogenerated constraints
+ * on the column for you.
+ *
+ * @see org.sonar.server.platform.db.migration.step.DropColumnChange
*/
public class DropColumnsBuilder {
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/DropColumnChange.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/DropColumnChange.java
index b32fd2a5923..6836025742a 100644
--- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/DropColumnChange.java
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/DropColumnChange.java
@@ -26,6 +26,18 @@ import org.sonar.db.dialect.MsSql;
import org.sonar.server.platform.db.migration.sql.DropColumnsBuilder;
import org.sonar.server.platform.db.migration.sql.DropMsSQLDefaultConstraintsBuilder;
+/**
+ * Drop a column from a table. This also takes care of removing any autogenerated
+ * constraints on the column for you.
+ *
+ * <p>
+ * Usage:
+ * <ol>
+ * <li>Extend your migration class from this class instead of `DdlChange`.</li>
+ * <li>Call `super` in the constructor with your table and column names.</li>
+ * </ol>
+ * </p>
+ */
public abstract class DropColumnChange extends DdlChange {
private final String tableName;
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/CreateUniqueIndexOnArchitectureGraphs.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/CreateUniqueIndexOnArchitectureGraphs.java
index 802396d18b1..2664ff7c0e9 100644
--- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/CreateUniqueIndexOnArchitectureGraphs.java
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/CreateUniqueIndexOnArchitectureGraphs.java
@@ -46,15 +46,26 @@ public class CreateUniqueIndexOnArchitectureGraphs extends DdlChange {
}
private void createIndex(Context context, Connection connection) {
- if (!DatabaseUtils.indexExistsIgnoreCase(TABLE_NAME, INDEX_NAME, connection)) {
- context.execute(new CreateIndexBuilder(getDialect())
- .setTable(TABLE_NAME)
- .setName(INDEX_NAME)
- .setUnique(true)
- .addColumn(COLUMN_NAME_BRANCH_UUID, false)
- .addColumn(COLUMN_NAME_TYPE, false)
- .addColumn(COLUMN_NAME_SOURCE, false)
- .build());
+ if(!DatabaseUtils.tableColumnExists(connection, TABLE_NAME, COLUMN_NAME_BRANCH_UUID)) {
+ return;
}
+ if(!DatabaseUtils.tableColumnExists(connection, TABLE_NAME, COLUMN_NAME_TYPE)) {
+ return;
+ }
+ if(!DatabaseUtils.tableColumnExists(connection, TABLE_NAME, COLUMN_NAME_SOURCE)) {
+ return;
+ }
+ if (DatabaseUtils.indexExistsIgnoreCase(TABLE_NAME, INDEX_NAME, connection)) {
+ return;
+ }
+
+ context.execute(new CreateIndexBuilder(getDialect())
+ .setTable(TABLE_NAME)
+ .setName(INDEX_NAME)
+ .setUnique(true)
+ .addColumn(COLUMN_NAME_BRANCH_UUID, false)
+ .addColumn(COLUMN_NAME_TYPE, false)
+ .addColumn(COLUMN_NAME_SOURCE, false)
+ .build());
}
}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddAnalysisUuidOnArchitectureGraphs.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddAnalysisUuidOnArchitectureGraphs.java
new file mode 100644
index 00000000000..1110d46c570
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddAnalysisUuidOnArchitectureGraphs.java
@@ -0,0 +1,54 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddAnalysisUuidOnArchitectureGraphs extends DdlChange {
+ static final String TABLE_NAME = "architecture_graphs";
+ static final String COLUMN_NAME = "analysis_uuid";
+
+ public AddAnalysisUuidOnArchitectureGraphs(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(DdlChange.Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setLimit(VarcharColumnDef.UUID_SIZE)
+ .setIsNullable(true)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddAssigneeNameToScaIssuesReleases.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddAssigneeNameToScaIssuesReleases.java
new file mode 100644
index 00000000000..a74272f4b2c
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddAssigneeNameToScaIssuesReleases.java
@@ -0,0 +1,54 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddAssigneeNameToScaIssuesReleases extends DdlChange {
+ static final String TABLE_NAME = "sca_issues_releases";
+ static final String COLUMN_NAME = "assignee_name";
+
+ public AddAssigneeNameToScaIssuesReleases(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setLimit(200)
+ .setIsNullable(true)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddAssigneeToScaIssuesReleases.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddAssigneeToScaIssuesReleases.java
new file mode 100644
index 00000000000..2b07bb52a0a
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddAssigneeToScaIssuesReleases.java
@@ -0,0 +1,54 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddAssigneeToScaIssuesReleases extends DdlChange {
+ static final String TABLE_NAME = "sca_issues_releases";
+ static final String COLUMN_NAME = "assignee_uuid";
+
+ public AddAssigneeToScaIssuesReleases(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setLimit(40)
+ .setIsNullable(true)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddCommentToScaIssuesReleasesChangesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddCommentToScaIssuesReleasesChangesTable.java
new file mode 100644
index 00000000000..80231e458ab
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddCommentToScaIssuesReleasesChangesTable.java
@@ -0,0 +1,55 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.MAX_SIZE;
+
+public class AddCommentToScaIssuesReleasesChangesTable extends DdlChange {
+ static final String TABLE_NAME = "sca_issue_rels_changes";
+ static final String COLUMN_NAME = "change_comment";
+
+ public AddCommentToScaIssuesReleasesChangesTable(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setLimit(MAX_SIZE)
+ .setIsNullable(true)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddGraphVersionOnArchitectureGraphsTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddGraphVersionOnArchitectureGraphsTable.java
new file mode 100644
index 00000000000..55ca4c73a4f
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddGraphVersionOnArchitectureGraphsTable.java
@@ -0,0 +1,55 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddGraphVersionOnArchitectureGraphsTable extends DdlChange {
+ static final String TABLE_NAME = "architecture_graphs";
+ static final String COLUMN_NAME = "graph_version";
+
+ public AddGraphVersionOnArchitectureGraphsTable(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(DdlChange.Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setLimit(255)
+ .setIsNullable(false)
+ .setDefaultValue("1.0.0")
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddIsNewToScaDependenciesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddIsNewToScaDependenciesTable.java
new file mode 100644
index 00000000000..a96ad27592e
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddIsNewToScaDependenciesTable.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.BooleanColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddIsNewToScaDependenciesTable extends DdlChange {
+ static final String TABLE_NAME = "sca_dependencies";
+ static final String COLUMN_NAME = "is_new";
+
+ public AddIsNewToScaDependenciesTable(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = BooleanColumnDef.newBooleanColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setIsNullable(false)
+ .setDefaultValue(false)
+ .build();
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddIsNewToScaReleasesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddIsNewToScaReleasesTable.java
new file mode 100644
index 00000000000..f6eca6ddc67
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddIsNewToScaReleasesTable.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.BooleanColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddIsNewToScaReleasesTable extends DdlChange {
+ static final String TABLE_NAME = "sca_releases";
+ static final String COLUMN_NAME = "is_new";
+
+ public AddIsNewToScaReleasesTable(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = BooleanColumnDef.newBooleanColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setIsNullable(false)
+ .setDefaultValue(false)
+ .build();
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddPerspectiveKeyOnArchitectureGraphs.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddPerspectiveKeyOnArchitectureGraphs.java
new file mode 100644
index 00000000000..994e05650a0
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddPerspectiveKeyOnArchitectureGraphs.java
@@ -0,0 +1,54 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddPerspectiveKeyOnArchitectureGraphs extends DdlChange {
+ static final String TABLE_NAME = "architecture_graphs";
+ static final String COLUMN_NAME = "perspective_key";
+
+ public AddPerspectiveKeyOnArchitectureGraphs(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setLimit(255)
+ .setIsNullable(true)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddPolicyUpdatedAtToScaLicenseProfilesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddPolicyUpdatedAtToScaLicenseProfilesTable.java
new file mode 100644
index 00000000000..cd6b6e76cc5
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddPolicyUpdatedAtToScaLicenseProfilesTable.java
@@ -0,0 +1,52 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.BigIntegerColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddPolicyUpdatedAtToScaLicenseProfilesTable extends DdlChange {
+ static final String TABLE_NAME = "sca_license_profiles";
+ static final String COLUMN_NAME = "policy_updated_at";
+
+ public AddPolicyUpdatedAtToScaLicenseProfilesTable(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = BigIntegerColumnDef.newBigIntegerColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddPreviousManualStatusToScaIssuesReleases.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddPreviousManualStatusToScaIssuesReleases.java
new file mode 100644
index 00000000000..fb5514c42e9
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddPreviousManualStatusToScaIssuesReleases.java
@@ -0,0 +1,54 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddPreviousManualStatusToScaIssuesReleases extends DdlChange {
+ static final String TABLE_NAME = "sca_issues_releases";
+ static final String COLUMN_NAME = "previous_manual_status";
+
+ public AddPreviousManualStatusToScaIssuesReleases(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setLimit(40)
+ .setIsNullable(true)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddStatusToScaIssuesReleasesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddStatusToScaIssuesReleasesTable.java
new file mode 100644
index 00000000000..2767ab9a6ce
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddStatusToScaIssuesReleasesTable.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddStatusToScaIssuesReleasesTable extends DdlChange {
+ static final String TABLE_NAME = "sca_issues_releases";
+ static final String COLUMN_NAME = "status";
+
+ public AddStatusToScaIssuesReleasesTable(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setLimit(40)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/BackfillRemoveAssigneeNameFromIssueReleaseChanges.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/BackfillRemoveAssigneeNameFromIssueReleaseChanges.java
new file mode 100644
index 00000000000..762c9e90834
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/BackfillRemoveAssigneeNameFromIssueReleaseChanges.java
@@ -0,0 +1,69 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonParser;
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.step.MassUpdate;
+
+public class BackfillRemoveAssigneeNameFromIssueReleaseChanges extends DataChange {
+ // Select records that might contain assigneeName in their change_data
+ private static final String SELECT_QUERY = "select uuid, change_data from sca_issue_rels_changes where change_data like '%assigneeName%'";
+ private static final String UPDATE_QUERY = "update sca_issue_rels_changes set change_data = ? where uuid = ?";
+ private static final Gson GSON = new Gson();
+
+ public BackfillRemoveAssigneeNameFromIssueReleaseChanges(Database db) {
+ super(db);
+ }
+
+ @Override
+ protected void execute(Context context) throws SQLException {
+ MassUpdate massUpdate = context.prepareMassUpdate();
+ massUpdate.select(SELECT_QUERY);
+ massUpdate.update(UPDATE_QUERY);
+
+ massUpdate.execute((row, update, index) -> {
+ String uuid = row.getString(1);
+ String changeData = row.getString(2);
+
+ if (changeData == null) {
+ return false;
+ }
+
+ String updatedChangeData = removeAssigneeNameFromChangeData(changeData);
+
+ if (updatedChangeData.equals(changeData)) {
+ return false;
+ }
+ update.setString(1, updatedChangeData);
+ update.setString(2, uuid);
+ return true;
+ });
+ }
+
+ private static String removeAssigneeNameFromChangeData(String changeData) {
+ var obj = JsonParser.parseString(changeData).getAsJsonObject();
+ obj.remove("assigneeName");
+ return GSON.toJson(obj);
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateIndexOnScaIssuesReleaseChangesReleaseId.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateIndexOnScaIssuesReleaseChangesReleaseId.java
new file mode 100644
index 00000000000..7054dd07fe4
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateIndexOnScaIssuesReleaseChangesReleaseId.java
@@ -0,0 +1,54 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.db.DatabaseUtils;
+import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+public class CreateIndexOnScaIssuesReleaseChangesReleaseId extends DdlChange {
+ static final String TABLE_NAME = "sca_issue_rels_changes";
+ static final String INDEX_NAME = "sca_iss_rels_changes_ir_uuid";
+ static final String COLUMN_NAME_SCA_RELEASE_UUID = "sca_issues_releases_uuid";
+
+ public CreateIndexOnScaIssuesReleaseChangesReleaseId(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (Connection connection = getDatabase().getDataSource().getConnection()) {
+ createIndex(context, connection);
+ }
+ }
+
+ private void createIndex(Context context, Connection connection) {
+ if (!DatabaseUtils.indexExistsIgnoreCase(TABLE_NAME, INDEX_NAME, connection)) {
+ context.execute(new CreateIndexBuilder(getDialect())
+ .setTable(TABLE_NAME)
+ .setName(INDEX_NAME)
+ .addColumn(COLUMN_NAME_SCA_RELEASE_UUID, false)
+ .build());
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaAnalysesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaAnalysesTable.java
new file mode 100644
index 00000000000..1356126259a
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaAnalysesTable.java
@@ -0,0 +1,57 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import java.util.List;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.sql.CreateTableBuilder;
+import org.sonar.server.platform.db.migration.step.CreateTableChange;
+
+import static org.sonar.server.platform.db.migration.def.BigIntegerColumnDef.newBigIntegerColumnDefBuilder;
+import static org.sonar.server.platform.db.migration.def.ClobColumnDef.newClobColumnDefBuilder;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.newVarcharColumnDefBuilder;
+
+public class CreateScaAnalysesTable extends CreateTableChange {
+ public static final String TABLE_NAME = "sca_analyses";
+ public static final int STATUS_COLUMN_SIZE = 40;
+ public static final int FAILED_REASON_COLUMN_SIZE = 255;
+
+ protected CreateScaAnalysesTable(Database db) {
+ super(db, TABLE_NAME);
+ }
+
+ @Override
+ public void execute(Context context, String tableName) throws SQLException {
+ List<String> createQuery = new CreateTableBuilder(getDialect(), tableName)
+ .addPkColumn(newVarcharColumnDefBuilder().setColumnName("uuid").setIsNullable(false).setLimit(UUID_SIZE).build())
+ .addColumn(newVarcharColumnDefBuilder().setColumnName("component_uuid").setIsNullable(false).setLimit(UUID_SIZE).build())
+ .addColumn(newVarcharColumnDefBuilder().setColumnName("status").setIsNullable(false).setLimit(STATUS_COLUMN_SIZE).build())
+ .addColumn(newVarcharColumnDefBuilder().setColumnName("failed_reason").setIsNullable(true).setLimit(FAILED_REASON_COLUMN_SIZE).build())
+ .addColumn(newClobColumnDefBuilder().setColumnName("errors").setIsNullable(false).build())
+ .addColumn(newClobColumnDefBuilder().setColumnName("parsed_files").setIsNullable(false).build())
+ .addColumn(newBigIntegerColumnDefBuilder().setColumnName("created_at").setIsNullable(false).build())
+ .addColumn(newBigIntegerColumnDefBuilder().setColumnName("updated_at").setIsNullable(false).build())
+ .build();
+
+ context.execute(createQuery);
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaEncounteredLicensesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaEncounteredLicensesTable.java
new file mode 100644
index 00000000000..8af4fd4e088
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaEncounteredLicensesTable.java
@@ -0,0 +1,55 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.sql.CreateTableBuilder;
+import org.sonar.server.platform.db.migration.step.CreateTableChange;
+
+import static org.sonar.server.platform.db.migration.def.BigIntegerColumnDef.newBigIntegerColumnDefBuilder;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.newVarcharColumnDefBuilder;
+
+public class CreateScaEncounteredLicensesTable extends CreateTableChange {
+
+ private static final String TABLE_NAME = "sca_encountered_licenses";
+ private static final String COLUMN_UUID_NAME = "uuid";
+ private static final String COLUMN_LICENSE_POLICY_ID_NAME = "license_policy_id";
+ private static final int COLUMN_LICENSE_POLICY_ID_SIZE = 127;
+ private static final String COLUMN_CREATED_AT_NAME = "created_at";
+ private static final String COLUMN_UPDATED_AT_NAME = "updated_at";
+
+ protected CreateScaEncounteredLicensesTable(Database db) {
+ super(db, TABLE_NAME);
+ }
+
+ @Override
+ public void execute(Context context, String tableName) throws SQLException {
+ context.execute(new CreateTableBuilder(getDialect(), tableName)
+ .addPkColumn(newVarcharColumnDefBuilder().setColumnName(COLUMN_UUID_NAME)
+ .setIsNullable(false).setLimit(UUID_SIZE).build())
+ .addColumn(newVarcharColumnDefBuilder().setColumnName(COLUMN_LICENSE_POLICY_ID_NAME)
+ .setIsNullable(false).setLimit(COLUMN_LICENSE_POLICY_ID_SIZE).build())
+ .addColumn(newBigIntegerColumnDefBuilder().setColumnName(COLUMN_CREATED_AT_NAME).setIsNullable(false).build())
+ .addColumn(newBigIntegerColumnDefBuilder().setColumnName(COLUMN_UPDATED_AT_NAME).setIsNullable(false).build())
+ .build());
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaIssuesReleasesChangesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaIssuesReleasesChangesTable.java
new file mode 100644
index 00000000000..77425f7a9b0
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaIssuesReleasesChangesTable.java
@@ -0,0 +1,55 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import java.util.List;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.sql.CreateTableBuilder;
+import org.sonar.server.platform.db.migration.step.CreateTableChange;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.server.platform.db.migration.def.BigIntegerColumnDef.newBigIntegerColumnDefBuilder;
+import static org.sonar.server.platform.db.migration.def.ClobColumnDef.newClobColumnDefBuilder;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.newVarcharColumnDefBuilder;
+
+public class CreateScaIssuesReleasesChangesTable extends CreateTableChange {
+ public static final String TABLE_NAME = "sca_issue_rels_changes";
+
+ protected CreateScaIssuesReleasesChangesTable(Database db) {
+ super(db, TABLE_NAME);
+ }
+
+ @Override
+ public void execute(DdlChange.Context context, String tableName) throws SQLException {
+ List<String> createQuery = new CreateTableBuilder(getDialect(), tableName)
+ .addPkColumn(newVarcharColumnDefBuilder().setColumnName("uuid").setIsNullable(false).setLimit(UUID_SIZE).build())
+ .addColumn(newVarcharColumnDefBuilder().setColumnName("sca_issues_releases_uuid").setIsNullable(false).setLimit(UUID_SIZE).build())
+ .addColumn(newVarcharColumnDefBuilder().setColumnName("user_uuid").setIsNullable(true).setLimit(UUID_SIZE).build())
+ .addColumn(newVarcharColumnDefBuilder().setColumnName("change_type").setIsNullable(false).setLimit(40).build())
+ .addColumn(newClobColumnDefBuilder().setColumnName("change_data").setIsNullable(true).build())
+ .addColumn(newBigIntegerColumnDefBuilder().setColumnName("created_at").setIsNullable(false).build())
+ .addColumn(newBigIntegerColumnDefBuilder().setColumnName("updated_at").setIsNullable(false).build())
+ .build();
+
+ context.execute(createQuery);
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfileCategoriesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfileCategoriesTable.java
new file mode 100644
index 00000000000..12e81bee508
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfileCategoriesTable.java
@@ -0,0 +1,59 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.sql.CreateTableBuilder;
+import org.sonar.server.platform.db.migration.step.CreateTableChange;
+
+import static org.sonar.server.platform.db.migration.def.BigIntegerColumnDef.newBigIntegerColumnDefBuilder;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.newVarcharColumnDefBuilder;
+
+public class CreateScaLicenseProfileCategoriesTable extends CreateTableChange {
+
+ // abbreviated due to limits on old oracle
+ private static final String TABLE_NAME = "sca_lic_prof_categories";
+ private static final String COLUMN_UUID_NAME = "uuid";
+ private static final String COLUMN_LICENSE_PROFILE_UUID_NAME = "sca_license_profile_uuid";
+ private static final String COLUMN_LICENSE_PROFILE_CATEGORY_NAME = "category";
+ private static final int COLUMN_LICENSE_PROFILE_CATEGORY_SIZE = 40;
+ private static final String COLUMN_CREATED_AT_NAME = "created_at";
+ private static final String COLUMN_UPDATED_AT_NAME = "updated_at";
+
+ protected CreateScaLicenseProfileCategoriesTable(Database db) {
+ super(db, TABLE_NAME);
+ }
+
+ @Override
+ public void execute(Context context, String tableName) throws SQLException {
+ context.execute(new CreateTableBuilder(getDialect(), tableName)
+ .addPkColumn(newVarcharColumnDefBuilder().setColumnName(COLUMN_UUID_NAME)
+ .setIsNullable(false).setLimit(UUID_SIZE).build())
+ .addColumn(newVarcharColumnDefBuilder().setColumnName(COLUMN_LICENSE_PROFILE_UUID_NAME)
+ .setIsNullable(false).setLimit(UUID_SIZE).build())
+ .addColumn(newVarcharColumnDefBuilder().setColumnName(COLUMN_LICENSE_PROFILE_CATEGORY_NAME)
+ .setIsNullable(false).setLimit(COLUMN_LICENSE_PROFILE_CATEGORY_SIZE).build())
+ .addColumn(newBigIntegerColumnDefBuilder().setColumnName(COLUMN_CREATED_AT_NAME).setIsNullable(false).build())
+ .addColumn(newBigIntegerColumnDefBuilder().setColumnName(COLUMN_UPDATED_AT_NAME).setIsNullable(false).build())
+ .build());
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfileCustomizationsTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfileCustomizationsTable.java
new file mode 100644
index 00000000000..a0c999809b9
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfileCustomizationsTable.java
@@ -0,0 +1,63 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.sql.CreateTableBuilder;
+import org.sonar.server.platform.db.migration.step.CreateTableChange;
+
+import static org.sonar.server.platform.db.migration.def.BigIntegerColumnDef.newBigIntegerColumnDefBuilder;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.newVarcharColumnDefBuilder;
+
+public class CreateScaLicenseProfileCustomizationsTable extends CreateTableChange {
+
+ // abbreviated due to limits on old oracle
+ private static final String TABLE_NAME = "sca_lic_prof_customs";
+ private static final String COLUMN_UUID_NAME = "uuid";
+ private static final String COLUMN_LICENSE_PROFILE_UUID_NAME = "sca_license_profile_uuid";
+ private static final String COLUMN_LICENSE_POLICY_ID_NAME = "license_policy_id";
+ private static final int COLUMN_LICENSE_POLICY_ID_SIZE = 127;
+ private static final String COLUMN_STATUS_NAME = "status";
+ private static final int COLUMN_STATUS_SIZE = 40;
+ private static final String COLUMN_CREATED_AT_NAME = "created_at";
+ private static final String COLUMN_UPDATED_AT_NAME = "updated_at";
+
+ protected CreateScaLicenseProfileCustomizationsTable(Database db) {
+ super(db, TABLE_NAME);
+ }
+
+ @Override
+ public void execute(Context context, String tableName) throws SQLException {
+ context.execute(new CreateTableBuilder(getDialect(), tableName)
+ .addPkColumn(newVarcharColumnDefBuilder().setColumnName(COLUMN_UUID_NAME)
+ .setIsNullable(false).setLimit(UUID_SIZE).build())
+ .addColumn(newVarcharColumnDefBuilder().setColumnName(COLUMN_LICENSE_PROFILE_UUID_NAME)
+ .setIsNullable(false).setLimit(UUID_SIZE).build())
+ .addColumn(newVarcharColumnDefBuilder().setColumnName(COLUMN_LICENSE_POLICY_ID_NAME)
+ .setIsNullable(false).setLimit(COLUMN_LICENSE_POLICY_ID_SIZE).build())
+ .addColumn(newVarcharColumnDefBuilder().setColumnName(COLUMN_STATUS_NAME)
+ .setIsNullable(false).setLimit(COLUMN_STATUS_SIZE).build())
+ .addColumn(newBigIntegerColumnDefBuilder().setColumnName(COLUMN_CREATED_AT_NAME).setIsNullable(false).build())
+ .addColumn(newBigIntegerColumnDefBuilder().setColumnName(COLUMN_UPDATED_AT_NAME).setIsNullable(false).build())
+ .build());
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfileProjectsTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfileProjectsTable.java
new file mode 100644
index 00000000000..fdbfe3baca4
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfileProjectsTable.java
@@ -0,0 +1,54 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.sql.CreateTableBuilder;
+import org.sonar.server.platform.db.migration.step.CreateTableChange;
+
+import static org.sonar.server.platform.db.migration.def.BigIntegerColumnDef.newBigIntegerColumnDefBuilder;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.newVarcharColumnDefBuilder;
+
+public class CreateScaLicenseProfileProjectsTable extends CreateTableChange {
+
+ private static final String TABLE_NAME = "sca_lic_prof_projects";
+ private static final String COLUMN_UUID_NAME = "uuid";
+ private static final String COLUMN_PROJECT_UUID_NAME = "project_uuid";
+ private static final String COLUMN_SCA_LICENSE_PROFILE_UUID_NAME = "sca_license_profile_uuid";
+ private static final String COLUMN_CREATED_AT_NAME = "created_at";
+ private static final String COLUMN_UPDATED_AT_NAME = "updated_at";
+
+ protected CreateScaLicenseProfileProjectsTable(Database db) {
+ super(db, TABLE_NAME);
+ }
+
+ @Override
+ public void execute(Context context, String tableName) throws SQLException {
+ context.execute(new CreateTableBuilder(getDialect(), tableName)
+ .addPkColumn(newVarcharColumnDefBuilder().setColumnName(COLUMN_UUID_NAME).setIsNullable(false).setLimit(UUID_SIZE).build())
+ .addColumn(newVarcharColumnDefBuilder().setColumnName(COLUMN_PROJECT_UUID_NAME).setIsNullable(false).setLimit(UUID_SIZE).build())
+ .addColumn(newVarcharColumnDefBuilder().setColumnName(COLUMN_SCA_LICENSE_PROFILE_UUID_NAME).setIsNullable(false).setLimit(UUID_SIZE).build())
+ .addColumn(newBigIntegerColumnDefBuilder().setColumnName(COLUMN_CREATED_AT_NAME).setIsNullable(false).build())
+ .addColumn(newBigIntegerColumnDefBuilder().setColumnName(COLUMN_UPDATED_AT_NAME).setIsNullable(false).build())
+ .build());
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfilesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfilesTable.java
new file mode 100644
index 00000000000..7c836314eab
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaLicenseProfilesTable.java
@@ -0,0 +1,56 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.sql.CreateTableBuilder;
+import org.sonar.server.platform.db.migration.step.CreateTableChange;
+
+import static org.sonar.server.platform.db.migration.def.BigIntegerColumnDef.newBigIntegerColumnDefBuilder;
+import static org.sonar.server.platform.db.migration.def.BooleanColumnDef.newBooleanColumnDefBuilder;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.newVarcharColumnDefBuilder;
+
+public class CreateScaLicenseProfilesTable extends CreateTableChange {
+
+ private static final String TABLE_NAME = "sca_license_profiles";
+ private static final String COLUMN_UUID_NAME = "uuid";
+ private static final String COLUMN_IS_DEFAULT_PROFILE_NAME = "is_default_profile";
+ private static final String COLUMN_NAME_NAME = "name";
+ private static final int COLUMN_NAME_SIZE = 400;
+ private static final String COLUMN_CREATED_AT_NAME = "created_at";
+ private static final String COLUMN_UPDATED_AT_NAME = "updated_at";
+
+ protected CreateScaLicenseProfilesTable(Database db) {
+ super(db, TABLE_NAME);
+ }
+
+ @Override
+ public void execute(Context context, String tableName) throws SQLException {
+ context.execute(new CreateTableBuilder(getDialect(), tableName)
+ .addPkColumn(newVarcharColumnDefBuilder().setColumnName(COLUMN_UUID_NAME).setIsNullable(false).setLimit(UUID_SIZE).build())
+ .addColumn(newBooleanColumnDefBuilder().setColumnName(COLUMN_IS_DEFAULT_PROFILE_NAME).setIsNullable(false).build())
+ .addColumn(newVarcharColumnDefBuilder().setColumnName(COLUMN_NAME_NAME).setIsNullable(false).setLimit(COLUMN_NAME_SIZE).build())
+ .addColumn(newBigIntegerColumnDefBuilder().setColumnName(COLUMN_CREATED_AT_NAME).setIsNullable(false).build())
+ .addColumn(newBigIntegerColumnDefBuilder().setColumnName(COLUMN_UPDATED_AT_NAME).setIsNullable(false).build())
+ .build());
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnArchitectureGraphs.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnArchitectureGraphs.java
new file mode 100644
index 00000000000..1be744195c9
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnArchitectureGraphs.java
@@ -0,0 +1,61 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.db.DatabaseUtils;
+import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+public class CreateUniqueIndexOnArchitectureGraphs extends DdlChange {
+ private static final String TABLE_NAME = "architecture_graphs";
+ private static final String INDEX_NAME = "uq_idx_ag_brch_tp_src_pspctv";
+ private static final String COLUMN_NAME_BRANCH_UUID = "branch_uuid";
+ private static final String COLUMN_NAME_TYPE = "type";
+ private static final String COLUMN_NAME_ECOSYSTEM = "ecosystem";
+ private static final String COLUMN_NAME_PERSPECTIVE_KEY = "perspective_key";
+
+ public CreateUniqueIndexOnArchitectureGraphs(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (Connection connection = getDatabase().getDataSource().getConnection()) {
+ createIndex(context, connection);
+ }
+ }
+
+ private void createIndex(Context context, Connection connection) {
+ if (!DatabaseUtils.indexExistsIgnoreCase(TABLE_NAME, INDEX_NAME, connection)) {
+ context.execute(new CreateIndexBuilder(getDialect())
+ .setTable(TABLE_NAME)
+ .setName(INDEX_NAME)
+ .setUnique(true)
+ .addColumn(COLUMN_NAME_BRANCH_UUID, false)
+ .addColumn(COLUMN_NAME_TYPE, false)
+ .addColumn(COLUMN_NAME_ECOSYSTEM, false)
+ .addColumn(COLUMN_NAME_PERSPECTIVE_KEY, true) // nullable
+ .build());
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaAnalyses.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaAnalyses.java
new file mode 100644
index 00000000000..0a82ad81e4b
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaAnalyses.java
@@ -0,0 +1,55 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.db.DatabaseUtils;
+import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+public class CreateUniqueIndexOnScaAnalyses extends DdlChange {
+ static final String TABLE_NAME = "sca_analyses";
+ static final String INDEX_NAME = "sca_analyses_component_uniq";
+ static final String COLUMN_NAME_COMPONENT_UUID = "component_uuid";
+
+ public CreateUniqueIndexOnScaAnalyses(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (Connection connection = getDatabase().getDataSource().getConnection()) {
+ createIndex(context, connection);
+ }
+ }
+
+ private void createIndex(Context context, Connection connection) {
+ if (!DatabaseUtils.indexExistsIgnoreCase(TABLE_NAME, INDEX_NAME, connection)) {
+ context.execute(new CreateIndexBuilder(getDialect())
+ .setTable(TABLE_NAME)
+ .setName(INDEX_NAME)
+ .setUnique(true)
+ .addColumn(COLUMN_NAME_COMPONENT_UUID, false)
+ .build());
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaEncounteredLicenses.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaEncounteredLicenses.java
new file mode 100644
index 00000000000..a71b1a09d4d
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaEncounteredLicenses.java
@@ -0,0 +1,56 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.db.DatabaseUtils;
+import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+public class CreateUniqueIndexOnScaEncounteredLicenses extends DdlChange {
+
+ static final String TABLE_NAME = "sca_encountered_licenses";
+ static final String INDEX_NAME = "sca_encountered_lic_uniq";
+ static final String COLUMN_LICENSE_POLICY_ID_NAME = "license_policy_id";
+
+ public CreateUniqueIndexOnScaEncounteredLicenses(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (Connection connection = getDatabase().getDataSource().getConnection()) {
+ createIndex(context, connection);
+ }
+ }
+
+ private void createIndex(Context context, Connection connection) {
+ if (!DatabaseUtils.indexExistsIgnoreCase(TABLE_NAME, INDEX_NAME, connection)) {
+ context.execute(new CreateIndexBuilder(getDialect())
+ .setTable(TABLE_NAME)
+ .setName(INDEX_NAME)
+ .setUnique(true)
+ .addColumn(COLUMN_LICENSE_POLICY_ID_NAME, false)
+ .build());
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfileCategories.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfileCategories.java
new file mode 100644
index 00000000000..b9c6961cb70
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfileCategories.java
@@ -0,0 +1,58 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.db.DatabaseUtils;
+import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+public class CreateUniqueIndexOnScaLicenseProfileCategories extends DdlChange {
+
+ static final String TABLE_NAME = "sca_lic_prof_categories";
+ static final String INDEX_NAME = "sca_lic_prof_categories_uniq";
+ static final String COLUMN_LICENSE_PROFILE_UUID_NAME = "sca_license_profile_uuid";
+ static final String COLUMN_CATEGORY_NAME = "category";
+
+ public CreateUniqueIndexOnScaLicenseProfileCategories(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (Connection connection = getDatabase().getDataSource().getConnection()) {
+ createIndex(context, connection);
+ }
+ }
+
+ private void createIndex(Context context, Connection connection) {
+ if (!DatabaseUtils.indexExistsIgnoreCase(TABLE_NAME, INDEX_NAME, connection)) {
+ context.execute(new CreateIndexBuilder(getDialect())
+ .setTable(TABLE_NAME)
+ .setName(INDEX_NAME)
+ .setUnique(true)
+ .addColumn(COLUMN_LICENSE_PROFILE_UUID_NAME, false)
+ .addColumn(COLUMN_CATEGORY_NAME, false)
+ .build());
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfileCustomizations.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfileCustomizations.java
new file mode 100644
index 00000000000..baf73cb8f7c
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfileCustomizations.java
@@ -0,0 +1,58 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.db.DatabaseUtils;
+import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+public class CreateUniqueIndexOnScaLicenseProfileCustomizations extends DdlChange {
+
+ static final String TABLE_NAME = "sca_lic_prof_customs";
+ static final String INDEX_NAME = "sca_lic_prof_customs_uniq";
+ static final String COLUMN_LICENSE_PROFILE_UUID_NAME = "sca_license_profile_uuid";
+ static final String COLUMN_LICENSE_POLICY_ID_NAME = "license_policy_id";
+
+ public CreateUniqueIndexOnScaLicenseProfileCustomizations(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (Connection connection = getDatabase().getDataSource().getConnection()) {
+ createIndex(context, connection);
+ }
+ }
+
+ private void createIndex(Context context, Connection connection) {
+ if (!DatabaseUtils.indexExistsIgnoreCase(TABLE_NAME, INDEX_NAME, connection)) {
+ context.execute(new CreateIndexBuilder(getDialect())
+ .setTable(TABLE_NAME)
+ .setName(INDEX_NAME)
+ .setUnique(true)
+ .addColumn(COLUMN_LICENSE_PROFILE_UUID_NAME, false)
+ .addColumn(COLUMN_LICENSE_POLICY_ID_NAME, false)
+ .build());
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfileProjects.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfileProjects.java
new file mode 100644
index 00000000000..6dc3a31036e
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfileProjects.java
@@ -0,0 +1,56 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.db.DatabaseUtils;
+import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+public class CreateUniqueIndexOnScaLicenseProfileProjects extends DdlChange {
+
+ static final String TABLE_NAME = "sca_lic_prof_projects";
+ static final String INDEX_NAME = "sca_lic_prof_projects_uniq";
+ static final String COLUMN_PROJECT_UUID_NAME = "project_uuid";
+
+ public CreateUniqueIndexOnScaLicenseProfileProjects(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (Connection connection = getDatabase().getDataSource().getConnection()) {
+ createIndex(context, connection);
+ }
+ }
+
+ private void createIndex(Context context, Connection connection) {
+ if (!DatabaseUtils.indexExistsIgnoreCase(TABLE_NAME, INDEX_NAME, connection)) {
+ context.execute(new CreateIndexBuilder(getDialect())
+ .setTable(TABLE_NAME)
+ .setName(INDEX_NAME)
+ .setUnique(true)
+ .addColumn(COLUMN_PROJECT_UUID_NAME, false)
+ .build());
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfiles.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfiles.java
new file mode 100644
index 00000000000..4d45b9d1925
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaLicenseProfiles.java
@@ -0,0 +1,56 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.db.DatabaseUtils;
+import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+public class CreateUniqueIndexOnScaLicenseProfiles extends DdlChange {
+
+ static final String TABLE_NAME = "sca_license_profiles";
+ static final String INDEX_NAME = "sca_license_profiles_uniq";
+ static final String COLUMN_NAME_NAME = "name";
+
+ public CreateUniqueIndexOnScaLicenseProfiles(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (Connection connection = getDatabase().getDataSource().getConnection()) {
+ createIndex(context, connection);
+ }
+ }
+
+ private void createIndex(Context context, Connection connection) {
+ if (!DatabaseUtils.indexExistsIgnoreCase(TABLE_NAME, INDEX_NAME, connection)) {
+ context.execute(new CreateIndexBuilder(getDialect())
+ .setTable(TABLE_NAME)
+ .setName(INDEX_NAME)
+ .setUnique(true)
+ .addColumn(COLUMN_NAME_NAME, false)
+ .build());
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaReleases.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaReleases.java
new file mode 100644
index 00000000000..08afc724ab8
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaReleases.java
@@ -0,0 +1,57 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.db.DatabaseUtils;
+import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+public class CreateUniqueIndexOnScaReleases extends DdlChange {
+ static final String TABLE_NAME = "sca_releases";
+ static final String INDEX_NAME = "sca_releases_package_url_uniq";
+ static final String COLUMN_NAME_PACKAGE_URL = "package_url";
+ static final String COLUMN_NAME_COMPONENT_UUID = "component_uuid";
+
+ public CreateUniqueIndexOnScaReleases(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (Connection connection = getDatabase().getDataSource().getConnection()) {
+ createIndex(context, connection);
+ }
+ }
+
+ private void createIndex(Context context, Connection connection) {
+ if (!DatabaseUtils.indexExistsIgnoreCase(TABLE_NAME, INDEX_NAME, connection)) {
+ context.execute(new CreateIndexBuilder(getDialect())
+ .setTable(TABLE_NAME)
+ .setName(INDEX_NAME)
+ .setUnique(true)
+ .addColumn(COLUMN_NAME_PACKAGE_URL, false)
+ .addColumn(COLUMN_NAME_COMPONENT_UUID, false)
+ .build());
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DbVersion202503.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DbVersion202503.java
index a238186ef25..ba743de7f8f 100644
--- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DbVersion202503.java
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DbVersion202503.java
@@ -38,6 +38,51 @@ public class DbVersion202503 implements DbVersion {
registry
.add(2025_03_000, "Add known_package column to SCA releases", AddKnownPackageToScaReleasesTable.class)
.add(2025_03_001, "Populate known_package column to SCA releases", PopulateKnownPackageColumnForScaReleasesTable.class)
- .add(2025_03_002, "Update known_package on SCA release to be not nullable", UpdateKnownPackageColumnNotNullable.class);
+ .add(2025_03_002, "Update known_package on SCA release to be not nullable", UpdateKnownPackageColumnNotNullable.class)
+ .add(2025_03_003, "Add status column to SCA issues releases join table", AddStatusToScaIssuesReleasesTable.class)
+ .add(2025_03_004, "Populate status column to SCA issues releases join table", PopulateStatusColumnForScaIssuesReleasesTable.class)
+ .add(2025_03_005, "Update status column on SCA issues releases join table to be not nullable", UpdateScaIssuesReleasesStatusColumnNotNullable.class)
+ .add(2025_03_006, "Add is_new column to SCA releases", AddIsNewToScaReleasesTable.class)
+ .add(2025_03_007, "Add is_new column to SCA dependencies", AddIsNewToScaDependenciesTable.class)
+ .add(2025_03_008, "Migrate to is_new on SCA releases", MigrateToIsNewOnScaReleases.class)
+ .add(2025_03_009, "Migrate to is_new on SCA dependencies", MigrateToIsNewOnScaDependencies.class)
+ .add(2025_03_010, "Drop new_in_pull_request column from SCA releases", DropNewInPullRequestFromScaReleasesTable.class)
+ .add(2025_03_011, "Drop new_in_pull_request column from SCA dependencies", DropNewInPullRequestFromScaDependenciesTable.class)
+ .add(2025_03_012, "Add assignee to SCA issues releases", AddAssigneeToScaIssuesReleases.class)
+ .add(2025_03_013, "Create ScaIssuesReleasesHistory table", CreateScaIssuesReleasesChangesTable.class)
+ .add(2025_03_014, "Create index for sca_issues_releases UUID on changes table", CreateIndexOnScaIssuesReleaseChangesReleaseId.class)
+ .add(2025_03_015, "Update default SCA dependency issue status to OPEN from TO_REVIEW", UpdateScaIssuesReleasesOpenStatus.class)
+ .add(2025_03_016, "Create SCA license profiles table", CreateScaLicenseProfilesTable.class)
+ .add(2025_03_017, "Create SCA license profiles projects join table", CreateScaLicenseProfileProjectsTable.class)
+ .add(2025_03_018, "Create unique index on SCA license profiles projects join table", CreateUniqueIndexOnScaLicenseProfileProjects.class)
+ .add(2025_03_019, "Create SCA license profile customizations table", CreateScaLicenseProfileCustomizationsTable.class)
+ .add(2025_03_020, "Create SCA license profile categories table", CreateScaLicenseProfileCategoriesTable.class)
+ .add(2025_03_021, "Create unique index on SCA license profile categories table", CreateUniqueIndexOnScaLicenseProfileCategories.class)
+ .add(2025_03_022, "Create unique index on SCA license profile customizations table", CreateUniqueIndexOnScaLicenseProfileCustomizations.class)
+ .add(2025_03_023, "Create unique index on SCA license profiles table", CreateUniqueIndexOnScaLicenseProfiles.class)
+ .add(2025_03_024, "Create SCA encountered licenses table", CreateScaEncounteredLicensesTable.class)
+ .add(2025_03_025, "Create SCA encountered licenses unique index", CreateUniqueIndexOnScaEncounteredLicenses.class)
+ .add(2025_03_026, "Add change_comment to SCA issues releases changes", AddCommentToScaIssuesReleasesChangesTable.class)
+ .add(2025_03_027, "Drop change_type from SCA issues releases changes", DropChangeTypeFromScaIssuesReleasesChangesTable.class)
+ .add(2025_03_028, "Remove duplicates from SCA releases table", MigrateRemoveDuplicateScaReleases.class)
+ .add(2025_03_029, "Create unique index on SCA releases table", CreateUniqueIndexOnScaReleases.class)
+ .add(2025_03_030, "Create SCA analyses table", CreateScaAnalysesTable.class)
+ .add(2025_03_031, "Create unique index on SCA analyses table", CreateUniqueIndexOnScaAnalyses.class)
+ .add(2025_03_032, "Add 'analysis_uuid' column to 'architecture_graphs' table", AddAnalysisUuidOnArchitectureGraphs.class)
+ .add(2025_03_033, "Add 'perspective_key' column to 'architecture_graphs' table", AddPerspectiveKeyOnArchitectureGraphs.class)
+ .add(2025_03_034, "Drop unique index on 'architecture_graphs' table", DropIndexOnArchitectureGraphs.class)
+ .add(2025_03_035, "Rename column 'source' to 'ecosystem' on 'architecture_graphs' table", UpdateArchitectureGraphsSourceColumnRename.class)
+ .add(2025_03_036, "Create unique index on 'architecture_graphs' table", CreateUniqueIndexOnArchitectureGraphs.class)
+ .add(2025_03_037, "Add previous_manual_status to SCA issues releases", AddPreviousManualStatusToScaIssuesReleases.class)
+ .add(2025_03_038, "Add 'graph_version' column to 'architecture_graphs' table", AddGraphVersionOnArchitectureGraphsTable.class)
+ .add(2025_03_039, "Add assignee name to SCA issues releases", AddAssigneeNameToScaIssuesReleases.class)
+ .add(2025_03_040, "Drop assignee name from SCA issues releases", DropAssigneeNameFromScaIssuesReleases.class)
+ .add(2025_03_041, "Remove AssigneeName from Sca issue release changes", BackfillRemoveAssigneeNameFromIssueReleaseChanges.class)
+ .add(2025_03_042, "Remove non-canonical from Sca encountered licenses", MigrateRemoveNonCanonicalScaEncounteredLicenses.class)
+ .add(2025_03_043, "Add 'policy_updated_at' to Sca license profiles", AddPolicyUpdatedAtToScaLicenseProfilesTable.class)
+ .add(2025_03_044, "Populate 'policy_updated_at' column for Sca license profiles", PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTable.class)
+ .add(2025_03_045, "Update 'policy_updated_at' column for Sca license profiles to be not nullable", UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullable.class)
+
+ ;
}
}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/NetworkInterfaceProvider.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropAssigneeNameFromScaIssuesReleases.java
index 160565dee81..cfe9ba322b8 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/NetworkInterfaceProvider.java
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropAssigneeNameFromScaIssuesReleases.java
@@ -17,20 +17,16 @@
* 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.server.webhook.ws;
+package org.sonar.server.platform.db.migration.version.v202503;
-import java.net.InetAddress;
-import java.net.NetworkInterface;
-import java.net.SocketException;
-import java.util.Collections;
-import java.util.List;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DropColumnChange;
-public class NetworkInterfaceProvider {
+public class DropAssigneeNameFromScaIssuesReleases extends DropColumnChange {
+ static final String TABLE_NAME = "sca_issues_releases";
+ static final String COLUMN_NAME = "assignee_name";
- public List<InetAddress> getNetworkInterfaceAddresses() throws SocketException {
- return Collections.list(NetworkInterface.getNetworkInterfaces())
- .stream()
- .flatMap(ni -> Collections.list(ni.getInetAddresses()).stream())
- .toList();
+ public DropAssigneeNameFromScaIssuesReleases(Database db) {
+ super(db, TABLE_NAME, COLUMN_NAME);
}
}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropChangeTypeFromScaIssuesReleasesChangesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropChangeTypeFromScaIssuesReleasesChangesTable.java
new file mode 100644
index 00000000000..602d957ecb1
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropChangeTypeFromScaIssuesReleasesChangesTable.java
@@ -0,0 +1,32 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DropColumnChange;
+
+public class DropChangeTypeFromScaIssuesReleasesChangesTable extends DropColumnChange {
+ static final String TABLE_NAME = "sca_issue_rels_changes";
+ static final String COLUMN_NAME = "change_type";
+
+ public DropChangeTypeFromScaIssuesReleasesChangesTable(Database db) {
+ super(db, TABLE_NAME, COLUMN_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropIndexOnArchitectureGraphs.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropIndexOnArchitectureGraphs.java
new file mode 100644
index 00000000000..d48236d6d76
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropIndexOnArchitectureGraphs.java
@@ -0,0 +1,33 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DropIndexChange;
+
+public class DropIndexOnArchitectureGraphs extends DropIndexChange {
+ private static final String TABLE_NAME = "architecture_graphs";
+ private static final String INDEX_NAME = "uq_idx_ag_branch_type_source";
+
+ public DropIndexOnArchitectureGraphs(Database db) {
+ super(db, INDEX_NAME, TABLE_NAME);
+ }
+
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaDependenciesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaDependenciesTable.java
new file mode 100644
index 00000000000..eee944c8d9e
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaDependenciesTable.java
@@ -0,0 +1,32 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DropColumnChange;
+
+public class DropNewInPullRequestFromScaDependenciesTable extends DropColumnChange {
+ static final String TABLE_NAME = "sca_dependencies";
+ static final String COLUMN_NAME = "new_in_pull_request";
+
+ public DropNewInPullRequestFromScaDependenciesTable(Database db) {
+ super(db, TABLE_NAME, COLUMN_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaReleasesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaReleasesTable.java
new file mode 100644
index 00000000000..31f08ea6166
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaReleasesTable.java
@@ -0,0 +1,32 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DropColumnChange;
+
+public class DropNewInPullRequestFromScaReleasesTable extends DropColumnChange {
+ static final String TABLE_NAME = "sca_releases";
+ static final String COLUMN_NAME = "new_in_pull_request";
+
+ public DropNewInPullRequestFromScaReleasesTable(Database db) {
+ super(db, TABLE_NAME, COLUMN_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/MigrateRemoveDuplicateScaReleases.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/MigrateRemoveDuplicateScaReleases.java
new file mode 100644
index 00000000000..22b733e27b8
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/MigrateRemoveDuplicateScaReleases.java
@@ -0,0 +1,112 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.MigrationStep;
+
+public class MigrateRemoveDuplicateScaReleases implements MigrationStep {
+ static final String SELECT_BATCH_QUERY = """
+ WITH duplicate_releases AS (
+ SELECT
+ uuid,
+ ROW_NUMBER() OVER (
+ PARTITION BY component_uuid, package_url
+ ORDER BY created_at ASC
+ ) AS row_num
+ FROM sca_releases
+ )
+ SELECT
+ uuid
+ FROM duplicate_releases
+ WHERE row_num > 1
+ """;
+
+ static final String DELETE_BATCH_DEPENDENCIES_QUERY = """
+ DELETE FROM sca_dependencies WHERE sca_release_uuid IN (?)
+ """;
+
+ static final String DELETE_BATCH_ISSUES_RELEASES_CHANGES_QUERY = """
+ DELETE FROM sca_issue_rels_changes WHERE sca_issues_releases_uuid IN (SELECT uuid FROM sca_issues_releases WHERE sca_release_uuid IN (?))
+ """;
+
+ static final String DELETE_BATCH_ISSUES_RELEASES_QUERY = """
+ DELETE FROM sca_issues_releases WHERE sca_release_uuid IN (?)
+ """;
+
+ static final String DELETE_BATCH_RELEASES_QUERY = """
+ DELETE FROM sca_releases WHERE uuid IN (?)
+ """;
+
+ private final Database db;
+
+ public MigrateRemoveDuplicateScaReleases(Database db) {
+ this.db = db;
+ }
+
+ private static List<String> findBatchOfDuplicates(Connection connection) throws SQLException {
+ List<String> results = new ArrayList<>();
+
+ try (PreparedStatement preparedStatement = connection.prepareStatement(SELECT_BATCH_QUERY)) {
+ preparedStatement.setMaxRows(999);
+ try (ResultSet resultSet = preparedStatement.executeQuery()) {
+ while (resultSet.next()) {
+ results.add(resultSet.getString(1));
+ }
+ }
+ }
+
+ return results;
+ }
+
+ private static void deleteBatch(Connection connection, String batchSql, List<String> duplicateReleaseUuids) throws SQLException {
+ try (PreparedStatement preparedStatement = connection.prepareStatement(batchSql)) {
+ for (String uuid : duplicateReleaseUuids) {
+ preparedStatement.setString(1, uuid);
+ preparedStatement.addBatch();
+ }
+ preparedStatement.executeBatch();
+ }
+ }
+
+ private static void deleteBatchOfDuplicates(Connection connection, List<String> duplicateRowUuids) throws SQLException {
+ deleteBatch(connection, DELETE_BATCH_DEPENDENCIES_QUERY, duplicateRowUuids);
+ deleteBatch(connection, DELETE_BATCH_ISSUES_RELEASES_CHANGES_QUERY, duplicateRowUuids);
+ deleteBatch(connection, DELETE_BATCH_ISSUES_RELEASES_QUERY, duplicateRowUuids);
+ deleteBatch(connection, DELETE_BATCH_RELEASES_QUERY, duplicateRowUuids);
+ }
+
+ @Override
+ public void execute() throws SQLException {
+ try (var connection = db.getDataSource().getConnection()) {
+ List<String> duplicateRowUuids = findBatchOfDuplicates(connection);
+ while (!duplicateRowUuids.isEmpty()) {
+ deleteBatchOfDuplicates(connection, duplicateRowUuids);
+ duplicateRowUuids = findBatchOfDuplicates(connection);
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/MigrateRemoveNonCanonicalScaEncounteredLicenses.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/MigrateRemoveNonCanonicalScaEncounteredLicenses.java
new file mode 100644
index 00000000000..f9fc92aab5a
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/MigrateRemoveNonCanonicalScaEncounteredLicenses.java
@@ -0,0 +1,90 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.MigrationStep;
+
+public class MigrateRemoveNonCanonicalScaEncounteredLicenses implements MigrationStep {
+ static final String SELECT_BATCH_QUERY = """
+ SELECT
+ uuid
+ FROM sca_encountered_licenses
+ WHERE
+ license_policy_id not like 'LicenseRef%'
+ and license_policy_id like '%-with-%'
+ """;
+
+ static final String DELETE_BATCH_ENCOUNTERED_LICENSES = """
+ DELETE FROM sca_encountered_licenses WHERE uuid IN (?)
+ """;
+
+ private final Database db;
+
+ public MigrateRemoveNonCanonicalScaEncounteredLicenses(Database db) {
+ this.db = db;
+ }
+
+ private static List<String> findBatchOfNonCanonical(Connection connection) throws SQLException {
+ List<String> results = new ArrayList<>();
+
+ try (PreparedStatement preparedStatement = connection.prepareStatement(SELECT_BATCH_QUERY)) {
+ preparedStatement.setMaxRows(999);
+ try (ResultSet resultSet = preparedStatement.executeQuery()) {
+ while (resultSet.next()) {
+ results.add(resultSet.getString(1));
+ }
+ }
+ }
+
+ return results;
+ }
+
+ private static void deleteBatch(Connection connection, List<String> nonCanonicalRowUuids) throws SQLException {
+ try (PreparedStatement preparedStatement = connection.prepareStatement(DELETE_BATCH_ENCOUNTERED_LICENSES)) {
+ for (String uuid : nonCanonicalRowUuids) {
+ preparedStatement.setString(1, uuid);
+ preparedStatement.addBatch();
+ }
+ preparedStatement.executeBatch();
+ }
+ }
+
+ private static void deleteBatchOfNonCanonical(Connection connection, List<String> nonCanonicalRowUuids) throws SQLException {
+ deleteBatch(connection, nonCanonicalRowUuids);
+ }
+
+ @Override
+ public void execute() throws SQLException {
+ try (var connection = db.getDataSource().getConnection()) {
+ List<String> nonCanonicalRowIds = findBatchOfNonCanonical(connection);
+ while (!nonCanonicalRowIds.isEmpty()) {
+ deleteBatchOfNonCanonical(connection, nonCanonicalRowIds);
+ nonCanonicalRowIds = findBatchOfNonCanonical(connection);
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/MigrateToIsNewOnScaDependencies.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/MigrateToIsNewOnScaDependencies.java
new file mode 100644
index 00000000000..f014c602565
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/MigrateToIsNewOnScaDependencies.java
@@ -0,0 +1,31 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import org.sonar.db.Database;
+
+public class MigrateToIsNewOnScaDependencies extends MigrateToIsNewOnScaTable {
+ private static final String SELECT_QUERY = "select new_in_pull_request, uuid from sca_dependencies where new_in_pull_request <> is_new";
+ private static final String UPDATE_QUERY = "update sca_dependencies set is_new = ? where uuid = ?";
+
+ public MigrateToIsNewOnScaDependencies(Database db) {
+ super(db, SELECT_QUERY, UPDATE_QUERY);
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/MigrateToIsNewOnScaReleases.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/MigrateToIsNewOnScaReleases.java
new file mode 100644
index 00000000000..546ed7345ce
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/MigrateToIsNewOnScaReleases.java
@@ -0,0 +1,31 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import org.sonar.db.Database;
+
+public class MigrateToIsNewOnScaReleases extends MigrateToIsNewOnScaTable {
+ private static final String SELECT_QUERY = "select new_in_pull_request, uuid from sca_releases where new_in_pull_request <> is_new";
+ private static final String UPDATE_QUERY = "update sca_releases set is_new = ? where uuid = ?";
+
+ public MigrateToIsNewOnScaReleases(Database db) {
+ super(db, SELECT_QUERY, UPDATE_QUERY);
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/MigrateToIsNewOnScaTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/MigrateToIsNewOnScaTable.java
new file mode 100644
index 00000000000..3f1944fc4d2
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/MigrateToIsNewOnScaTable.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.step.MassUpdate;
+
+public class MigrateToIsNewOnScaTable extends DataChange {
+ private String selectQuery;
+ private String updateQuery;
+
+ public MigrateToIsNewOnScaTable(
+ Database db,
+ String selectQuery,
+ String updateQuery) {
+ super(db);
+ this.selectQuery = selectQuery;
+ this.updateQuery = updateQuery;
+ }
+
+ @Override
+ protected void execute(Context context) throws SQLException {
+ MassUpdate massUpdate = context.prepareMassUpdate();
+ massUpdate.select(selectQuery);
+ massUpdate.update(updateQuery);
+
+ massUpdate.execute((row, update, index) -> {
+ update
+ .setBoolean(1, row.getBoolean(1))
+ .setString(2, row.getString(2));
+ return true;
+ });
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTable.java
new file mode 100644
index 00000000000..8cacf1212e4
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTable.java
@@ -0,0 +1,50 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.step.MassUpdate;
+
+public class PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTable extends DataChange {
+ private static final String SELECT_QUERY = "select updated_at, uuid from sca_license_profiles where policy_updated_at is null";
+ private static final String UPDATE_QUERY = "update sca_license_profiles set policy_updated_at = ? where uuid = ?";
+
+ public PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTable(Database db) {
+ super(db);
+ }
+
+ @Override
+ protected void execute(DataChange.Context context) throws SQLException {
+ MassUpdate massUpdate = context.prepareMassUpdate();
+ massUpdate.select(SELECT_QUERY);
+ massUpdate.update(UPDATE_QUERY);
+
+ massUpdate.execute((row, update, index) -> {
+ update
+ // Set policy_updated_at from updated_at
+ .setLong(1, row.getLong(1))
+ // Set uuid from uuid
+ .setString(2, row.getString(2));
+ return true;
+ });
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/PopulateStatusColumnForScaIssuesReleasesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/PopulateStatusColumnForScaIssuesReleasesTable.java
new file mode 100644
index 00000000000..1243133c65b
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/PopulateStatusColumnForScaIssuesReleasesTable.java
@@ -0,0 +1,51 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.step.MassUpdate;
+
+public class PopulateStatusColumnForScaIssuesReleasesTable extends DataChange {
+
+ private static final String SELECT_QUERY = "select status, uuid from sca_issues_releases where status is null";
+ private static final String UPDATE_QUERY = "update sca_issues_releases set status = ? where uuid = ?";
+
+ public PopulateStatusColumnForScaIssuesReleasesTable(Database db) {
+ super(db);
+ }
+
+ @Override
+ protected void execute(Context context) throws SQLException {
+ MassUpdate massUpdate = context.prepareMassUpdate();
+ massUpdate.select(SELECT_QUERY);
+ massUpdate.update(UPDATE_QUERY);
+
+ massUpdate.execute((row, update, index) -> {
+ update
+ // Set default status value
+ .setString(1, "TO_REVIEW")
+ // Set uuid from uuid
+ .setString(2, row.getString(2));
+ return true;
+ });
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/UpdateArchitectureGraphsSourceColumnRename.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/UpdateArchitectureGraphsSourceColumnRename.java
new file mode 100644
index 00000000000..0a19b0046f9
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/UpdateArchitectureGraphsSourceColumnRename.java
@@ -0,0 +1,33 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.RenameVarcharColumnChange;
+
+public class UpdateArchitectureGraphsSourceColumnRename extends RenameVarcharColumnChange {
+ private static final String TABLE_NAME = "architecture_graphs";
+ private static final String OLD_COLUMN_NAME = "source";
+ private static final String NEW_COLUMN_NAME = "ecosystem";
+
+ public UpdateArchitectureGraphsSourceColumnRename(Database db) {
+ super(db, TABLE_NAME, OLD_COLUMN_NAME, NEW_COLUMN_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaIssuesReleasesOpenStatus.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaIssuesReleasesOpenStatus.java
new file mode 100644
index 00000000000..2b755559f44
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaIssuesReleasesOpenStatus.java
@@ -0,0 +1,47 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.step.MassUpdate;
+
+public class UpdateScaIssuesReleasesOpenStatus extends DataChange {
+ static final String SELECT_QUERY = "select uuid from sca_issues_releases where status = 'TO_REVIEW'";
+ static final String UPDATE_QUERY = "update sca_issues_releases set status = 'OPEN' where uuid = ?";
+
+ public UpdateScaIssuesReleasesOpenStatus(Database db) {
+ super(db);
+ }
+
+ @Override
+ protected void execute(DataChange.Context context) throws SQLException {
+ MassUpdate massUpdate = context.prepareMassUpdate();
+ massUpdate.select(SELECT_QUERY);
+ massUpdate.update(UPDATE_QUERY);
+
+ massUpdate.execute((row, update, index) -> {
+ update
+ .setString(1, row.getString(1));
+ return true;
+ });
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaIssuesReleasesStatusColumnNotNullable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaIssuesReleasesStatusColumnNotNullable.java
new file mode 100644
index 00000000000..d4804c141d8
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaIssuesReleasesStatusColumnNotNullable.java
@@ -0,0 +1,54 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AlterColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class UpdateScaIssuesReleasesStatusColumnNotNullable extends DdlChange {
+ static final String TABLE_NAME = "sca_issues_releases";
+ static final String COLUMN_NAME = "status";
+
+ public UpdateScaIssuesReleasesStatusColumnNotNullable(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setIsNullable(false)
+ .setLimit(40)
+ .build();
+
+ context.execute(new AlterColumnsBuilder(getDialect(), TABLE_NAME)
+ .updateColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullable.java
new file mode 100644
index 00000000000..44761a449fc
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullable.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.BigIntegerColumnDef;
+import org.sonar.server.platform.db.migration.sql.AlterColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullable extends DdlChange {
+ static final String TABLE_NAME = "sca_license_profiles";
+ static final String COLUMN_NAME = "policy_updated_at";
+
+ public UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullable(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = BigIntegerColumnDef.newBigIntegerColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setIsNullable(false)
+ .build();
+
+ context.execute(new AlterColumnsBuilder(getDialect(), TABLE_NAME)
+ .updateColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddAnalysisParametersToScaAnalyses.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddAnalysisParametersToScaAnalyses.java
new file mode 100644
index 00000000000..447f5081e60
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddAnalysisParametersToScaAnalyses.java
@@ -0,0 +1,54 @@
+/*
+ * 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.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.ClobColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddAnalysisParametersToScaAnalyses extends DdlChange {
+ static final String TABLE_NAME = "sca_analyses";
+ static final String COLUMN_NAME = "analysis_parameters";
+
+ public AddAnalysisParametersToScaAnalyses(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = ClobColumnDef.newClobColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setIsNullable(true)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddIndexOnAlmRepoInProjectAlmSettings.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddIndexOnAlmRepoInProjectAlmSettings.java
new file mode 100644
index 00000000000..62cde95315b
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddIndexOnAlmRepoInProjectAlmSettings.java
@@ -0,0 +1,35 @@
+/*
+ * 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.server.platform.db.migration.version.v202504;
+
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.CreateIndexOnColumn;
+
+public class AddIndexOnAlmRepoInProjectAlmSettings extends CreateIndexOnColumn {
+
+ protected static final String TABLE_NAME = "project_alm_settings";
+ protected static final String COLUMN_NAME = "alm_repo";
+ protected static final boolean UNIQUE = false;
+
+ public AddIndexOnAlmRepoInProjectAlmSettings(Database db) {
+ super(db, TABLE_NAME, COLUMN_NAME, UNIQUE);
+ }
+
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddManualSeverityWarningToScaIssues.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddManualSeverityWarningToScaIssues.java
new file mode 100644
index 00000000000..9700e9fe565
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddManualSeverityWarningToScaIssues.java
@@ -0,0 +1,54 @@
+/*
+ * 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.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.BooleanColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddManualSeverityWarningToScaIssues extends DdlChange {
+ static final String TABLE_NAME = "sca_issues_releases";
+ static final String COLUMN_NAME = "show_increased_severity_warning";
+
+ public AddManualSeverityWarningToScaIssues(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = BooleanColumnDef.newBooleanColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setDefaultValue(false)
+ .setIsNullable(false)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddOrganizationUuidToScaLicenseProfiles.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddOrganizationUuidToScaLicenseProfiles.java
new file mode 100644
index 00000000000..90c1cb6e472
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddOrganizationUuidToScaLicenseProfiles.java
@@ -0,0 +1,57 @@
+/*
+ * 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.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddOrganizationUuidToScaLicenseProfiles extends DdlChange {
+ static final String TABLE_NAME = "sca_license_profiles";
+ static final String COLUMN_NAME = "organization_uuid";
+ static final String SQS_ORGANIZATION_UUID = "00000000-0000-4000-0000-000000000000";
+
+ public AddOrganizationUuidToScaLicenseProfiles(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setDefaultValue(SQS_ORGANIZATION_UUID)
+ .setColumnName(COLUMN_NAME)
+ .setLimit(40)
+ .setIsNullable(false)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssues.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssues.java
new file mode 100644
index 00000000000..66c4c27254e
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssues.java
@@ -0,0 +1,67 @@
+/*
+ * 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.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddOriginalAndManualSeverityToScaIssues extends DdlChange {
+ static final String TABLE_NAME = "sca_issues_releases";
+ static final String ORIGINAL_VALUE_COLUMN_NAME = "original_severity";
+ static final String MANUALLY_SET_COLUMN_NAME = "manual_severity";
+
+ public AddOriginalAndManualSeverityToScaIssues(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, ORIGINAL_VALUE_COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(ORIGINAL_VALUE_COLUMN_NAME)
+ .setLimit(15)
+ .setIsNullable(true)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+
+ if (!tableColumnExists(connection, TABLE_NAME, MANUALLY_SET_COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(MANUALLY_SET_COLUMN_NAME)
+ .setLimit(15)
+ .setIsNullable(true)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddWithdrawnToScaVulnerabilityIssues.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddWithdrawnToScaVulnerabilityIssues.java
new file mode 100644
index 00000000000..786259b8f97
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddWithdrawnToScaVulnerabilityIssues.java
@@ -0,0 +1,54 @@
+/*
+ * 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.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.BooleanColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddWithdrawnToScaVulnerabilityIssues extends DdlChange {
+ static final String TABLE_NAME = "sca_vulnerability_issues";
+ static final String COLUMN_NAME = "withdrawn";
+
+ public AddWithdrawnToScaVulnerabilityIssues(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = BooleanColumnDef.newBooleanColumnDefBuilder()
+ .setDefaultValue(false)
+ .setColumnName(COLUMN_NAME)
+ .setIsNullable(false)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/CreateUniqueIndexOnScaLicenseProfiles.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/CreateUniqueIndexOnScaLicenseProfiles.java
new file mode 100644
index 00000000000..5b58a91fe31
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/CreateUniqueIndexOnScaLicenseProfiles.java
@@ -0,0 +1,58 @@
+/*
+ * 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.server.platform.db.migration.version.v202504;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.db.DatabaseUtils;
+import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+public class CreateUniqueIndexOnScaLicenseProfiles extends DdlChange {
+
+ static final String TABLE_NAME = "sca_license_profiles";
+ static final String INDEX_NAME = "sca_license_profiles_uniq";
+ static final String COLUMN_NAME_ORG = "organization_uuid";
+ static final String COLUMN_NAME_NAME = "name";
+
+ public CreateUniqueIndexOnScaLicenseProfiles(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (Connection connection = getDatabase().getDataSource().getConnection()) {
+ createIndex(context, connection);
+ }
+ }
+
+ private void createIndex(Context context, Connection connection) {
+ if (!DatabaseUtils.indexExistsIgnoreCase(TABLE_NAME, INDEX_NAME, connection)) {
+ context.execute(new CreateIndexBuilder(getDialect())
+ .setTable(TABLE_NAME)
+ .setName(INDEX_NAME)
+ .setUnique(true)
+ .addColumn(COLUMN_NAME_ORG, true)
+ .addColumn(COLUMN_NAME_NAME, false)
+ .build());
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DbVersion202504.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DbVersion202504.java
new file mode 100644
index 00000000000..be71ba4bf93
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DbVersion202504.java
@@ -0,0 +1,44 @@
+/*
+ * 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.server.platform.db.migration.version.v202504;
+
+import org.sonar.server.platform.db.migration.step.MigrationStepRegistry;
+import org.sonar.server.platform.db.migration.version.DbVersion;
+
+public class DbVersion202504 implements DbVersion {
+ // ignoring bad number formatting, as it's intended that we align the migration numbers to SQ versions
+ @SuppressWarnings("java:S3937")
+
+ @Override
+ public void addSteps(MigrationStepRegistry registry) {
+ registry
+ .add(2025_04_000, "Add 'withdrawn' column to 'sca_vulnerability_issues' table", AddWithdrawnToScaVulnerabilityIssues.class)
+ .add(2025_04_001, "Add 'organization_uuid' column to 'sca_license_profiles' table", AddOrganizationUuidToScaLicenseProfiles.class)
+ .add(2025_04_002, "Drop unique index from 'sca_license_profiles'", DropUniqueIndexOnScaLicenseProfiles.class)
+ .add(2025_04_003, "Create unique index from 'sca_license_profiles'", CreateUniqueIndexOnScaLicenseProfiles.class)
+ .add(2025_04_004, "Add index on 'alm_repo' column in 'project_alm_settings' table", AddIndexOnAlmRepoInProjectAlmSettings.class)
+ .add(2025_04_005, "Add 'original_severity' and 'manual_severity' columns to 'sca_issues_releases' table", AddOriginalAndManualSeverityToScaIssues.class)
+ .add(2025_04_006, "Populate 'original_severity' column for 'sca_issues_releases' table", PopulateOriginalSeverityForScaIssuesReleasesTable.class)
+ .add(2025_04_007, "Update 'original_severity' column to be not nullable in 'sca_issues_releases' table", UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable.class)
+ .add(2025_04_008, "Update size of 'name' column in 'rules' table", UpdateRulesNameColumnSize.class)
+ .add(2025_04_009, "Add 'show_increased_severity_warning' column to 'sca_issues_releases' table", AddManualSeverityWarningToScaIssues.class)
+ .add(2025_04_010, "Add 'analysis_parameters' column to 'sca_analyses' table", AddAnalysisParametersToScaAnalyses.class);
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DropUniqueIndexOnScaLicenseProfiles.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DropUniqueIndexOnScaLicenseProfiles.java
new file mode 100644
index 00000000000..9294bd95c12
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DropUniqueIndexOnScaLicenseProfiles.java
@@ -0,0 +1,32 @@
+/*
+ * 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.server.platform.db.migration.version.v202504;
+
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DropIndexChange;
+
+public class DropUniqueIndexOnScaLicenseProfiles extends DropIndexChange {
+ private static final String TABLE_NAME = "sca_license_profiles";
+ private static final String INDEX_NAME = "sca_license_profiles_uniq";
+
+ public DropUniqueIndexOnScaLicenseProfiles(Database db) {
+ super(db, INDEX_NAME, TABLE_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTable.java
new file mode 100644
index 00000000000..bcee5bb0903
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTable.java
@@ -0,0 +1,50 @@
+/*
+ * 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.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.step.MassUpdate;
+
+public class PopulateOriginalSeverityForScaIssuesReleasesTable extends DataChange {
+ private static final String SELECT_QUERY = "select severity, uuid from sca_issues_releases where original_severity is null";
+ private static final String UPDATE_QUERY = "update sca_issues_releases set original_severity = ? where uuid = ?";
+
+ public PopulateOriginalSeverityForScaIssuesReleasesTable(Database db) {
+ super(db);
+ }
+
+ @Override
+ protected void execute(Context context) throws SQLException {
+ MassUpdate massUpdate = context.prepareMassUpdate();
+ massUpdate.select(SELECT_QUERY);
+ massUpdate.update(UPDATE_QUERY);
+
+ massUpdate.execute((row, update, index) -> {
+ update
+ // Set original_severity from severity
+ .setString(1, row.getString(1))
+ // Set uuid from uuid
+ .setString(2, row.getString(2));
+ return true;
+ });
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/UpdateRulesNameColumnSize.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/UpdateRulesNameColumnSize.java
new file mode 100644
index 00000000000..c93096e5293
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/UpdateRulesNameColumnSize.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AlterColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class UpdateRulesNameColumnSize extends DdlChange {
+ private static final String TABLE_NAME = "rules";
+ private static final String COLUMN_NAME = "name";
+ private static final int NEW_COLUMN_SIZE = 255;
+
+ public UpdateRulesNameColumnSize(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setLimit(NEW_COLUMN_SIZE)
+ .build();
+ context.execute(new AlterColumnsBuilder(getDialect(), TABLE_NAME)
+ .updateColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable.java
new file mode 100644
index 00000000000..4c842e81e1f
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable.java
@@ -0,0 +1,54 @@
+/*
+ * 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.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AlterColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable extends DdlChange {
+ static final String TABLE_NAME = "sca_issues_releases";
+ static final String COLUMN_NAME = "original_severity";
+
+ public UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setIsNullable(false)
+ .setLimit(15)
+ .build();
+
+ context.execute(new AlterColumnsBuilder(getDialect(), TABLE_NAME)
+ .updateColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/roots/package-info.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/package-info.java
index c255d1eea80..ebad6a23ac3 100644
--- a/sonar-ws/src/main/java/org/sonarqube/ws/client/roots/package-info.java
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/package-info.java
@@ -18,9 +18,6 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
@ParametersAreNonnullByDefault
-@Generated("sonar-ws-generator")
-package org.sonarqube.ws.client.roots;
+package org.sonar.server.platform.db.migration.version.v202504;
import javax.annotation.ParametersAreNonnullByDefault;
-import jakarta.annotation.Generated;
-
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/roots/SetRootRequest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v202504/DbVersion202504Test.java
index d7b7b2a74c7..3b27807db59 100644
--- a/sonar-ws/src/main/java/org/sonarqube/ws/client/roots/SetRootRequest.java
+++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v202504/DbVersion202504Test.java
@@ -17,31 +17,24 @@
* 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.sonarqube.ws.client.roots;
+package org.sonar.server.platform.db.migration.version.v202504;
-import jakarta.annotation.Generated;
+import org.junit.jupiter.api.Test;
-/**
- * This is part of the internal API.
- * This is a POST request.
- * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/roots/set_root">Further information about this action online (including a response example)</a>
- * @since 6.2
- */
-@Generated("sonar-ws-generator")
-public class SetRootRequest {
+import static org.sonar.server.platform.db.migration.version.DbVersionTestUtils.verifyMigrationNotEmpty;
+import static org.sonar.server.platform.db.migration.version.DbVersionTestUtils.verifyMinimumMigrationNumber;
+
+class DbVersion202503Test {
- private String login;
+ private final DbVersion202504 underTest = new DbVersion202504();
- /**
- * This is a mandatory parameter.
- * Example value: "admin"
- */
- public SetRootRequest setLogin(String login) {
- this.login = login;
- return this;
+ @Test
+ void migrationNumber_starts_at_2025_04_000() {
+ verifyMinimumMigrationNumber(underTest, 2025_04_000);
}
- public String getLogin() {
- return login;
+ @Test
+ void verify_migration_is_not_empty() {
+ verifyMigrationNotEmpty(underTest);
}
}
diff --git a/server/sonar-main/src/main/java/org/sonar/application/cluster/ClusterAppStateImpl.java b/server/sonar-main/src/main/java/org/sonar/application/cluster/ClusterAppStateImpl.java
index 5287f4bd9b5..b9813eb1a08 100644
--- a/server/sonar-main/src/main/java/org/sonar/application/cluster/ClusterAppStateImpl.java
+++ b/server/sonar-main/src/main/java/org/sonar/application/cluster/ClusterAppStateImpl.java
@@ -25,7 +25,6 @@ import com.hazelcast.cluster.MembershipEvent;
import com.hazelcast.core.EntryAdapter;
import com.hazelcast.core.EntryEvent;
import com.hazelcast.core.HazelcastInstanceNotActiveException;
-import com.hazelcast.cp.IAtomicReference;
import com.hazelcast.replicatedmap.ReplicatedMap;
import java.util.ArrayList;
import java.util.EnumMap;
@@ -34,6 +33,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
+import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.cluster.health.ClusterHealthStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -47,6 +47,7 @@ import org.sonar.application.es.EsConnector;
import org.sonar.process.MessageException;
import org.sonar.process.NetworkUtilsImpl;
import org.sonar.process.ProcessId;
+import org.sonar.process.cluster.hz.DistributedReference;
import org.sonar.process.cluster.hz.HazelcastMember;
import static java.lang.String.format;
@@ -126,7 +127,7 @@ public class ClusterAppStateImpl implements ClusterAppState {
@Override
public boolean tryToLockWebLeader() {
- IAtomicReference<UUID> leader = hzMember.getAtomicReference(LEADER);
+ DistributedReference<UUID> leader = hzMember.getAtomicReference(LEADER);
return leader.compareAndSet(null, hzMember.getUuid());
}
@@ -138,10 +139,11 @@ public class ClusterAppStateImpl implements ClusterAppState {
/**
* Tries to release the lock of the cluster leader. It is safe to call this method even if one is not sure about the UUID of the leader.
* If all nodes call this method then we can be confident that the lock is released.
+ *
* @param uuidOfLeader - the UUID of the leader to release the lock. In case the UUID is not the leader's uuid this method has no effect.
*/
private void tryToReleaseWebLeaderLock(UUID uuidOfLeader) {
- IAtomicReference<UUID> leader = hzMember.getAtomicReference(LEADER);
+ DistributedReference<UUID> leader = hzMember.getAtomicReference(LEADER);
leader.compareAndSet(uuidOfLeader, null);
}
@@ -152,7 +154,7 @@ public class ClusterAppStateImpl implements ClusterAppState {
@Override
public void registerSonarQubeVersion(String sonarqubeVersion) {
- IAtomicReference<String> sqVersion = hzMember.getAtomicReference(SONARQUBE_VERSION);
+ DistributedReference<String> sqVersion = hzMember.getAtomicReference(SONARQUBE_VERSION);
boolean wasSet = sqVersion.compareAndSet(null, sonarqubeVersion);
if (!wasSet) {
@@ -166,7 +168,7 @@ public class ClusterAppStateImpl implements ClusterAppState {
@Override
public void registerClusterName(String clusterName) {
- IAtomicReference<String> property = hzMember.getAtomicReference(CLUSTER_NAME);
+ DistributedReference<String> property = hzMember.getAtomicReference(CLUSTER_NAME);
boolean wasSet = property.compareAndSet(null, clusterName);
if (!wasSet) {
@@ -221,9 +223,14 @@ public class ClusterAppStateImpl implements ClusterAppState {
}
private boolean isElasticSearchOperational() {
- return esConnector.getClusterHealthStatus()
- .filter(t -> ClusterHealthStatus.GREEN.equals(t) || ClusterHealthStatus.YELLOW.equals(t))
- .isPresent();
+ try {
+ return esConnector.getClusterHealthStatus()
+ .filter(t -> ClusterHealthStatus.GREEN.equals(t) || ClusterHealthStatus.YELLOW.equals(t))
+ .isPresent();
+ } catch (ElasticsearchException e) {
+ LOGGER.warn("Cannot check at current time whether Elasticsearch is operational", e);
+ return false;
+ }
}
private void asyncWaitForEsToBecomeOperational() {
diff --git a/server/sonar-main/src/main/java/org/sonar/application/es/EsSettings.java b/server/sonar-main/src/main/java/org/sonar/application/es/EsSettings.java
index 46f57e2c65c..dcc39aa99a5 100644
--- a/server/sonar-main/src/main/java/org/sonar/application/es/EsSettings.java
+++ b/server/sonar-main/src/main/java/org/sonar/application/es/EsSettings.java
@@ -252,5 +252,9 @@ public class EsSettings {
if (props.value(JAVA_ADDITIONAL_OPS_PROPERTY, "").contains("-D" + ALLOW_MMAP + "=" + Boolean.FALSE)) {
builder.put(ALLOW_MMAP, Boolean.FALSE.toString());
}
+
+ if (props.value(JAVA_ADDITIONAL_OPS_PROPERTY, "").contains("-Dcluster.routing.allocation.disk.threshold_enabled=" + Boolean.FALSE)) {
+ builder.put("cluster.routing.allocation.disk.threshold_enabled", Boolean.FALSE.toString());
+ }
}
}
diff --git a/server/sonar-main/src/test/java/org/sonar/application/cluster/AppNodesClusterHostsConsistencyTest.java b/server/sonar-main/src/test/java/org/sonar/application/cluster/AppNodesClusterHostsConsistencyTest.java
index 2d81dfb08b0..372bd5f57ad 100644
--- a/server/sonar-main/src/test/java/org/sonar/application/cluster/AppNodesClusterHostsConsistencyTest.java
+++ b/server/sonar-main/src/test/java/org/sonar/application/cluster/AppNodesClusterHostsConsistencyTest.java
@@ -24,7 +24,6 @@ import com.hazelcast.cluster.Address;
import com.hazelcast.cluster.Cluster;
import com.hazelcast.cluster.Member;
import com.hazelcast.cluster.MemberSelector;
-import com.hazelcast.cp.IAtomicReference;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
@@ -43,6 +42,7 @@ import org.sonar.application.config.TestAppSettings;
import org.sonar.process.cluster.hz.DistributedAnswer;
import org.sonar.process.cluster.hz.DistributedCall;
import org.sonar.process.cluster.hz.DistributedCallback;
+import org.sonar.process.cluster.hz.DistributedReference;
import org.sonar.process.cluster.hz.HazelcastMember;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -150,7 +150,7 @@ public class AppNodesClusterHostsConsistencyTest {
}
@Override
- public <E> IAtomicReference<E> getAtomicReference(String name) {
+ public <E> DistributedReference<E> getAtomicReference(String name) {
throw new IllegalStateException("not expected to be called");
}
diff --git a/server/sonar-main/src/test/java/org/sonar/application/cluster/ClusterAppStateImplTest.java b/server/sonar-main/src/test/java/org/sonar/application/cluster/ClusterAppStateImplTest.java
index 849c1523e34..c61a1d3621d 100644
--- a/server/sonar-main/src/test/java/org/sonar/application/cluster/ClusterAppStateImplTest.java
+++ b/server/sonar-main/src/test/java/org/sonar/application/cluster/ClusterAppStateImplTest.java
@@ -21,6 +21,7 @@ package org.sonar.application.cluster;
import java.net.InetAddress;
import java.util.Optional;
+import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.cluster.health.ClusterHealthStatus;
import org.junit.Rule;
import org.junit.Test;
@@ -39,6 +40,7 @@ import org.sonar.process.cluster.hz.JoinConfigurationType;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
@@ -106,6 +108,18 @@ public class ClusterAppStateImplTest {
}
@Test
+ public void isOperational_whenElasticsearchException_tryAgain() {
+ EsConnector esConnectorMock = mock();
+ doThrow(new ElasticsearchException("failed to connect")).when(esConnectorMock).getClusterHealthStatus();
+
+ try (ClusterAppStateImpl underTest = createClusterAppState(esConnectorMock)) {
+ boolean operational = underTest.isOperational(ProcessId.ELASTICSEARCH, false);
+
+ assertThat(operational).isFalse();
+ }
+ }
+
+ @Test
public void constructor_checks_appNodesClusterHostsConsistency() {
AppNodesClusterHostsConsistency clusterHostsConsistency = mock(AppNodesClusterHostsConsistency.class);
try (ClusterAppStateImpl underTest = new ClusterAppStateImpl(new TestAppSettings(), newHzMember(),
@@ -120,7 +134,7 @@ public class ClusterAppStateImplTest {
try (ClusterAppStateImpl underTest = createClusterAppState()) {
underTest.registerSonarQubeVersion("6.4.1.5");
- assertThat(underTest.getHazelcastMember().getAtomicReference(SONARQUBE_VERSION).get())
+ assertThat((String) underTest.getHazelcastMember().getAtomicReference(SONARQUBE_VERSION).get())
.isEqualTo("6.4.1.5");
}
}
@@ -130,7 +144,7 @@ public class ClusterAppStateImplTest {
try (ClusterAppStateImpl underTest = createClusterAppState()) {
underTest.registerClusterName("foo");
- assertThat(underTest.getHazelcastMember().getAtomicReference(CLUSTER_NAME).get())
+ assertThat((String) underTest.getHazelcastMember().getAtomicReference(CLUSTER_NAME).get())
.isEqualTo("foo");
}
}
diff --git a/server/sonar-main/src/test/java/org/sonar/application/es/EsSettingsTest.java b/server/sonar-main/src/test/java/org/sonar/application/es/EsSettingsTest.java
index 012445ff83c..974ffd79dcb 100644
--- a/server/sonar-main/src/test/java/org/sonar/application/es/EsSettingsTest.java
+++ b/server/sonar-main/src/test/java/org/sonar/application/es/EsSettingsTest.java
@@ -367,6 +367,25 @@ public class EsSettingsTest {
}
@Test
+ @UseDataProvider("clusterEnabledOrNot")
+ public void disable_disk_threshold_if_configured_in_search_additional_props(boolean clusterEnabled) throws Exception {
+ Props props = minProps(clusterEnabled);
+ props.set("sonar.search.javaAdditionalOpts", "-Dcluster.routing.allocation.disk.threshold_enabled=false");
+ Map<String, String> settings = new EsSettings(props, new EsInstallation(props), system).build();
+
+ assertThat(settings).containsEntry("cluster.routing.allocation.disk.threshold_enabled", "false");
+ }
+
+ @Test
+ @UseDataProvider("clusterEnabledOrNot")
+ public void disk_threshold_not_set_by_default(boolean clusterEnabled) throws Exception {
+ Props props = minProps(clusterEnabled);
+ Map<String, String> settings = new EsSettings(props, new EsInstallation(props), system).build();
+
+ assertThat(settings.get("cluster.routing.allocation.disk.threshold_enabled")).isNull();
+ }
+
+ @Test
public void configureSecurity_givenClusterSearchPasswordNotProvided_dontAddXpackParameters() throws Exception {
Props props = minProps(true);
diff --git a/server/sonar-process/src/main/java/org/sonar/process/ProcessProperties.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessProperties.java
index 3cf08d4085f..fd13abb2bfb 100644
--- a/server/sonar-process/src/main/java/org/sonar/process/ProcessProperties.java
+++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessProperties.java
@@ -101,7 +101,10 @@ public class ProcessProperties {
WEB_HTTP_MAX_THREADS("sonar.web.http.maxThreads"),
WEB_HTTP_ACCEPT_COUNT("sonar.web.http.acceptCount"),
WEB_HTTP_KEEP_ALIVE_TIMEOUT("sonar.web.http.keepAliveTimeout"),
- WEB_SESSION_TIMEOUT_IN_MIN("sonar.web.sessionTimeoutInMinutes"),
+ // The time a user can remain idle (no activity) before the session ends.
+ WEB_INACTIVE_SESSION_TIMEOUT_IN_MIN("sonar.web.sessionTimeoutInMinutes"),
+ // The time a user can remain logged in, regardless of activity
+ WEB_ACTIVE_SESSION_TIMEOUT_IN_MIN("sonar.web.activeSessionTimeoutInMinutes"),
WEB_SYSTEM_PASS_CODE("sonar.web.systemPasscode"),
WEB_ACCESSLOGS_ENABLE("sonar.web.accessLogs.enable"),
WEB_ACCESSLOGS_PATTERN("sonar.web.accessLogs.pattern"),
diff --git a/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/DistributedLock.java b/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/DistributedLock.java
new file mode 100644
index 00000000000..e0cb7c6dd65
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/DistributedLock.java
@@ -0,0 +1,85 @@
+/*
+ * 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.process.cluster.hz;
+
+import com.hazelcast.map.IMap;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+
+public class DistributedLock implements Lock {
+ private final IMap<String, UUID> lockMap;
+ private final String lockName;
+ private final UUID ownerId;
+
+ public DistributedLock(IMap<String, UUID> lockMap, String lockName) {
+ this.lockMap = lockMap;
+ this.lockName = lockName;
+ // Unique ID for this lock owner
+ this.ownerId = UUID.randomUUID();
+ }
+
+ @Override
+ public void lock() {
+ while (!tryLock()) {
+ try {
+ // Retry after a short delay
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("Thread interrupted while acquiring lock");
+ }
+ }
+ }
+
+ @Override
+ public void unlock() {
+ lockMap.computeIfPresent(lockName, (key, value) -> value.equals(ownerId) ? null : value);
+ }
+
+ @Override
+ public boolean tryLock() {
+ return lockMap.putIfAbsent(lockName, ownerId) == null;
+ }
+
+ @Override
+ public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
+ long deadline = System.currentTimeMillis() + unit.toMillis(time);
+ while (System.currentTimeMillis() < deadline) {
+ if (tryLock()) {
+ return true;
+ }
+ // Retry after a short delay
+ Thread.sleep(10);
+ }
+ return false;
+ }
+
+ @Override
+ public Condition newCondition() {
+ throw new UnsupportedOperationException("Conditions are not supported in DistributedLock");
+ }
+
+ @Override
+ public void lockInterruptibly() throws InterruptedException {
+ throw new UnsupportedOperationException("Interruptible locking is not supported in DistributedLock");
+ }
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/DistributedReference.java b/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/DistributedReference.java
new file mode 100644
index 00000000000..e21d7846eff
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/DistributedReference.java
@@ -0,0 +1,69 @@
+/*
+ * 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.process.cluster.hz;
+
+import com.hazelcast.map.IMap;
+import javax.annotation.Nullable;
+
+public class DistributedReference<E> {
+ private static final String ATOMIC_KEY = "atomicKey";
+
+ private final IMap<String, E> lockMap;
+
+ public DistributedReference(IMap<String, E> lockMap) {
+ this.lockMap = lockMap;
+ }
+
+ public E get() {
+ return lockMap.get(ATOMIC_KEY);
+ }
+
+ public void set(@Nullable E value) {
+ lockMap.lock(ATOMIC_KEY);
+ try {
+ putOrRemove(value);
+ } finally {
+ lockMap.unlock(ATOMIC_KEY);
+ }
+ }
+
+ public boolean compareAndSet(@Nullable E expected, @Nullable E newValue) {
+ lockMap.lock(ATOMIC_KEY);
+ try {
+ if ((expected == null && lockMap.get(ATOMIC_KEY) == null) ||
+ (expected != null && expected.equals(lockMap.get(ATOMIC_KEY)))) {
+ putOrRemove(newValue);
+ return true;
+ }
+ return false;
+ } finally {
+ lockMap.unlock(ATOMIC_KEY);
+ }
+ }
+
+ private void putOrRemove(@org.jetbrains.annotations.Nullable E value) {
+ if (value != null) {
+ lockMap.put(ATOMIC_KEY, value);
+ } else {
+ lockMap.remove(ATOMIC_KEY);
+ }
+ }
+
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMember.java b/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMember.java
index f46bda5f8c1..03a0442d5a7 100644
--- a/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMember.java
+++ b/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMember.java
@@ -21,7 +21,6 @@ package org.sonar.process.cluster.hz;
import com.hazelcast.cluster.Cluster;
import com.hazelcast.cluster.MemberSelector;
-import com.hazelcast.cp.IAtomicReference;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@@ -51,7 +50,7 @@ public interface HazelcastMember extends AutoCloseable {
}
}
- <E> IAtomicReference<E> getAtomicReference(String name);
+ <E> DistributedReference<E> getAtomicReference(String name);
/**
* Gets the replicated map shared by the cluster and identified by name.
diff --git a/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberImpl.java b/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberImpl.java
index 7dcda62e7fb..6fce2b1eadd 100644
--- a/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberImpl.java
+++ b/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberImpl.java
@@ -26,7 +26,7 @@ import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.HazelcastInstanceNotActiveException;
import com.hazelcast.core.IExecutorService;
import com.hazelcast.core.MultiExecutionCallback;
-import com.hazelcast.cp.IAtomicReference;
+import com.hazelcast.map.IMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@@ -47,8 +47,8 @@ class HazelcastMemberImpl implements HazelcastMember {
}
@Override
- public <E> IAtomicReference<E> getAtomicReference(String name) {
- return hzInstance.getCPSubsystem().getAtomicReference(name);
+ public <E> DistributedReference<E> getAtomicReference(String name) {
+ return new DistributedReference<>(hzInstance.getMap(name));
}
@Override
@@ -68,7 +68,8 @@ class HazelcastMemberImpl implements HazelcastMember {
@Override
public Lock getLock(String s) {
- return hzInstance.getCPSubsystem().getLock(s);
+ IMap<String, UUID> lockMap = hzInstance.getMap("distributedLocks");
+ return new DistributedLock(lockMap, s);
}
@Override
diff --git a/server/sonar-process/src/test/java/org/sonar/process/cluster/hz/DistributedLockTest.java b/server/sonar-process/src/test/java/org/sonar/process/cluster/hz/DistributedLockTest.java
new file mode 100644
index 00000000000..d5546e01918
--- /dev/null
+++ b/server/sonar-process/src/test/java/org/sonar/process/cluster/hz/DistributedLockTest.java
@@ -0,0 +1,176 @@
+/*
+ * 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.process.cluster.hz;
+
+import com.hazelcast.map.IMap;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class DistributedLockTest {
+
+ private static final String LOCK_NAME = "lockName";
+
+ private final IMap<String, UUID> map = new MockIMap<>();
+ private final DistributedLock underTest = new DistributedLock(map, LOCK_NAME);
+
+
+ @Test
+ void lock() {
+ underTest.lock();
+
+ assertThat(map.get(LOCK_NAME)).isNotNull();
+
+ underTest.unlock();
+ }
+
+ @Test
+ void lock_whenAlreadyLocked_WaitsUntilUnlocked() throws InterruptedException {
+
+ Thread thread1 = new Thread(() -> {
+ underTest.lock();
+ try {
+ // Simulate some work
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ underTest.unlock();
+ }
+ });
+
+ Thread thread2 = new Thread(() -> {
+ underTest.lock();
+ try {
+ // Simulate some work
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ underTest.unlock();
+ }
+ });
+
+ thread1.start();
+ thread2.start();
+
+ thread1.join();
+ thread2.join();
+ assertThat(map.get(LOCK_NAME)).isNull();
+ }
+
+ @Test
+ void lock_InterruptedThread_throwsException() throws InterruptedException {
+
+ Thread thread1 = new Thread(() -> {
+ underTest.lock();
+ assertThat(map.tryLock(LOCK_NAME)).isFalse();
+ try {
+ // Simulate some work
+ Thread.sleep(300);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ underTest.unlock();
+ }
+ });
+
+ Thread thread2 = new Thread(() -> {
+ try {
+ underTest.lock();
+ assertThat(map.tryLock(LOCK_NAME)).isFalse();
+ // Simulate some work
+ } catch (Exception e) {
+ assertThat(e).isInstanceOf(IllegalStateException.class).hasMessage("Thread interrupted while acquiring lock");
+ } finally {
+ underTest.unlock();
+ assertThat(map.get(LOCK_NAME)).isNull();
+ }
+ });
+
+ thread1.start();
+ thread2.start();
+ thread2.interrupt();
+
+ thread1.join();
+ thread2.join();
+ }
+
+ @Test
+ void unlock() {
+ underTest.lock();
+
+ assertThat(map.get(LOCK_NAME)).isNotNull();
+
+ underTest.unlock();
+
+ assertThat(map.get(LOCK_NAME)).isNull();
+ }
+
+ @Test
+ void tryLock() {
+
+ underTest.lock();
+
+ assertThat(underTest.tryLock()).isFalse();
+
+ underTest.unlock();
+
+ assertThat(underTest.tryLock()).isTrue();
+ assertThat(map.get(LOCK_NAME)).isNotNull();
+
+ underTest.unlock();
+ }
+
+ @Test
+ void tryLock_WithTimeout() throws InterruptedException {
+ underTest.lock();
+
+ assertThat(map.get(LOCK_NAME)).isNotNull();
+ assertThat(underTest.tryLock(1, TimeUnit.SECONDS)).isFalse();
+
+ underTest.unlock();
+
+ assertThat(underTest.tryLock(1, TimeUnit.SECONDS)).isTrue();
+ assertThat(map.get(LOCK_NAME)).isNotNull();
+
+ underTest.unlock();
+ }
+
+ @Test
+ void newCondition() {
+ try {
+ underTest.newCondition();
+ } catch (UnsupportedOperationException e) {
+ assertThat(e).isInstanceOf(UnsupportedOperationException.class);
+ }
+ }
+
+ @Test
+ void lockInterruptibly() {
+ try {
+ underTest.lockInterruptibly();
+ } catch (Exception e) {
+ assertThat(e).isInstanceOf(UnsupportedOperationException.class);
+ }
+ }
+}
diff --git a/server/sonar-process/src/test/java/org/sonar/process/cluster/hz/DistributedReferenceTest.java b/server/sonar-process/src/test/java/org/sonar/process/cluster/hz/DistributedReferenceTest.java
new file mode 100644
index 00000000000..bc6e1437213
--- /dev/null
+++ b/server/sonar-process/src/test/java/org/sonar/process/cluster/hz/DistributedReferenceTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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.process.cluster.hz;
+
+import com.hazelcast.map.IMap;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class DistributedReferenceTest {
+
+ private final IMap<String, String> map = new MockIMap<>();
+
+ @Test
+ void set_whenValueIsNull_KeyValuePairIsRemoved() {
+ DistributedReference<String> stringDistributedReference = new DistributedReference<>(map);
+
+ stringDistributedReference.set("value");
+
+ assertThat(stringDistributedReference.get()).isEqualTo("value");
+
+ stringDistributedReference.set(null);
+
+ assertThat(stringDistributedReference.get()).isNull();
+
+ }
+
+ @Test
+ void get_returnsExpectedValue() {
+ DistributedReference<String> stringDistributedReference = new DistributedReference<>(map);
+
+ stringDistributedReference.set("value");
+
+ assertThat(stringDistributedReference.get()).isEqualTo("value");
+ }
+
+ @Test
+ void compareAndSet_whenExpectedValueMatches_newValueIsSet() {
+ DistributedReference<String> stringDistributedReference = new DistributedReference<>(map);
+
+ stringDistributedReference.set("oldValue");
+
+ boolean result = stringDistributedReference.compareAndSet("oldValue", "newValue");
+
+ assertThat(result).isTrue();
+ assertThat(stringDistributedReference.get()).isEqualTo("newValue");
+ }
+
+ @Test
+ void compareAndSet_whenExpectedValueMatches_newValueIsSetEvenIfNull() {
+ DistributedReference<String> stringDistributedReference = new DistributedReference<>(map);
+
+ stringDistributedReference.set("value");
+
+ boolean result = stringDistributedReference.compareAndSet("value", null);
+
+ assertThat(result).isTrue();
+ assertThat(stringDistributedReference.get()).isNull();
+ }
+
+ @Test
+ void compareAndSet_whenExpectedValueMatchesNull_newValueIsSet() {
+ DistributedReference<String> stringDistributedReference = new DistributedReference<>(map);
+
+ boolean result = stringDistributedReference.compareAndSet(null, "newValue");
+
+ assertThat(result).isTrue();
+ assertThat(stringDistributedReference.get()).isEqualTo("newValue");
+ }
+
+ @Test
+ void compareAndSet_whenExpectedValueDoesNotMatchNull_newValueIsNotSet() {
+ DistributedReference<String> stringDistributedReference = new DistributedReference<>(map);
+
+ boolean result = stringDistributedReference.compareAndSet("oldValue", "newValue");
+
+ assertThat(result).isFalse();
+ assertThat(stringDistributedReference.get()).isNull();
+ }
+
+ @Test
+ void compareAndSet_whenExpectedValueDoesNotMatch_previousValueIsKept() {
+ DistributedReference<String> stringDistributedReference = new DistributedReference<>(map);
+
+ stringDistributedReference.set("oldValue");
+
+ boolean result = stringDistributedReference.compareAndSet("value", "newValue");
+
+ assertThat(result).isFalse();
+ assertThat(stringDistributedReference.get()).isEqualTo("oldValue");
+ }
+
+
+}
+
diff --git a/server/sonar-process/src/test/java/org/sonar/process/cluster/hz/MockIMap.java b/server/sonar-process/src/test/java/org/sonar/process/cluster/hz/MockIMap.java
new file mode 100644
index 00000000000..0cc3ca33390
--- /dev/null
+++ b/server/sonar-process/src/test/java/org/sonar/process/cluster/hz/MockIMap.java
@@ -0,0 +1,577 @@
+/*
+ * 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.process.cluster.hz;
+
+import com.hazelcast.aggregation.Aggregator;
+import com.hazelcast.config.IndexConfig;
+import com.hazelcast.core.EntryView;
+import com.hazelcast.map.EntryProcessor;
+import com.hazelcast.map.IMap;
+import com.hazelcast.map.LocalMapStats;
+import com.hazelcast.map.MapInterceptor;
+import com.hazelcast.map.QueryCache;
+import com.hazelcast.map.listener.MapListener;
+import com.hazelcast.map.listener.MapPartitionLostListener;
+import com.hazelcast.projection.Projection;
+import com.hazelcast.query.Predicate;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class MockIMap<K, V> implements IMap<K, V> {
+ private final Map<K, V> map = new HashMap<>();
+ private final ReentrantLock lock = new ReentrantLock();
+
+ @Override
+ public V get(Object key) {
+ return map.get(key);
+ }
+
+ @Override
+ public V put(K key, V value) {
+ return map.put(key, value);
+ }
+
+ @Override
+ public V remove(Object key) {
+ return map.remove(key);
+ }
+
+ @Override
+ public boolean remove(@NotNull Object o, @NotNull Object o1) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public void removeAll(@NotNull Predicate<K, V> predicate) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public boolean tryLock(K key) {
+ return lock.tryLock();
+ }
+
+ @Override
+ public boolean tryLock(@NotNull K k, long l, @Nullable TimeUnit timeUnit) throws InterruptedException {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public boolean tryLock(@NotNull K k, long l, @Nullable TimeUnit timeUnit, long l1, @Nullable TimeUnit timeUnit1) throws InterruptedException {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public void unlock(K key) {
+ lock.unlock();
+ }
+
+ @Override
+ public void forceUnlock(@NotNull K k) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public UUID addLocalEntryListener(@NotNull MapListener mapListener) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public UUID addLocalEntryListener(@NotNull MapListener mapListener, @NotNull Predicate<K, V> predicate, boolean b) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public UUID addLocalEntryListener(@NotNull MapListener mapListener, @NotNull Predicate<K, V> predicate, @Nullable K k, boolean b) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public String addInterceptor(@NotNull MapInterceptor mapInterceptor) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public boolean removeInterceptor(@NotNull String s) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public UUID addEntryListener(@NotNull MapListener mapListener, boolean b) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public boolean removeEntryListener(@NotNull UUID uuid) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public UUID addPartitionLostListener(@NotNull MapPartitionLostListener mapPartitionLostListener) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public boolean removePartitionLostListener(@NotNull UUID uuid) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public UUID addEntryListener(@NotNull MapListener mapListener, @NotNull K k, boolean b) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public UUID addEntryListener(@NotNull MapListener mapListener, @NotNull Predicate<K, V> predicate, boolean b) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public UUID addEntryListener(@NotNull MapListener mapListener, @NotNull Predicate<K, V> predicate, @Nullable K k, boolean b) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public EntryView<K, V> getEntryView(@NotNull K k) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public boolean evict(@NotNull K k) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public void evictAll() {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @NotNull
+ @Override
+ public Set<K> keySet() {
+ return map.keySet();
+ }
+
+ @NotNull
+ @Override
+ public Collection<V> values() {
+ return map.values();
+ }
+
+ @NotNull
+ @Override
+ public Set<Entry<K, V>> entrySet() {
+ return map.entrySet();
+ }
+
+ @Override
+ public Set<K> keySet(@NotNull Predicate<K, V> predicate) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public Set<Entry<K, V>> entrySet(@NotNull Predicate<K, V> predicate) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public Collection<V> values(@NotNull Predicate<K, V> predicate) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public Collection<V> localValues() {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public Collection<V> localValues(@NotNull Predicate<K, V> predicate) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public Set<K> localKeySet() {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public Set<K> localKeySet(@NotNull Predicate<K, V> predicate) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public void addIndex(IndexConfig indexConfig) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public LocalMapStats getLocalMapStats() {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public <R> R executeOnKey(@NotNull K k, @NotNull EntryProcessor<K, V, R> entryProcessor) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public <R> Map<K, R> executeOnKeys(@NotNull Set<K> set, @NotNull EntryProcessor<K, V, R> entryProcessor) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public <R> CompletionStage<Map<K, R>> submitToKeys(@NotNull Set<K> set, @NotNull EntryProcessor<K, V, R> entryProcessor) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public <R> CompletionStage<R> submitToKey(@NotNull K k, @NotNull EntryProcessor<K, V, R> entryProcessor) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public <R> Map<K, R> executeOnEntries(@NotNull EntryProcessor<K, V, R> entryProcessor) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public <R> Map<K, R> executeOnEntries(@NotNull EntryProcessor<K, V, R> entryProcessor, @NotNull Predicate<K, V> predicate) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public <R> R aggregate(@NotNull Aggregator<? super Entry<K, V>, R> aggregator) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public <R> R aggregate(@NotNull Aggregator<? super Entry<K, V>, R> aggregator, @NotNull Predicate<K, V> predicate) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public <R> Collection<R> project(@NotNull Projection<? super Entry<K, V>, R> projection) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public <R> Collection<R> project(@NotNull Projection<? super Entry<K, V>, R> projection, @NotNull Predicate<K, V> predicate) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public QueryCache<K, V> getQueryCache(@NotNull String s) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public QueryCache<K, V> getQueryCache(@NotNull String s, @NotNull Predicate<K, V> predicate, boolean b) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public QueryCache<K, V> getQueryCache(@NotNull String s, @NotNull MapListener mapListener, @NotNull Predicate<K, V> predicate, boolean b) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public boolean setTtl(@NotNull K k, long l, @NotNull TimeUnit timeUnit) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public V computeIfPresent(@NotNull K k, @NotNull BiFunction<? super K, ? super V, ? extends V> biFunction) {
+ if (map.containsKey(k)) {
+ V value = map.get(k);
+ V newValue = biFunction.apply(k, value);
+ if (null != newValue) {
+ map.put(k, newValue);
+ } else {
+ map.remove(k);
+ }
+ return newValue;
+ }
+ return null;
+ }
+
+ @Override
+ public V computeIfAbsent(@NotNull K k, @NotNull Function<? super K, ? extends V> function) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public V compute(@NotNull K k, @NotNull BiFunction<? super K, ? super V, ? extends V> biFunction) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public V merge(@NotNull K k, @NotNull V v, @NotNull BiFunction<? super V, ? super V, ? extends V> biFunction) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @NotNull
+ @Override
+ public Iterator<Entry<K, V>> iterator() {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @NotNull
+ @Override
+ public Iterator<Entry<K, V>> iterator(int i) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ // Add other methods as needed, throwing UnsupportedOperationException for unimplemented ones
+ @Override
+ public void delete(Object key) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public void flush() {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public Map<K, V> getAll(@Nullable Set<K> set) {
+ return Map.of();
+ }
+
+ @Override
+ public void loadAll(boolean b) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public void loadAll(@NotNull Set<K> set, boolean b) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public void clear() {
+ map.clear();
+ }
+
+ @Override
+ public CompletionStage<V> getAsync(@NotNull K k) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public CompletionStage<V> putAsync(@NotNull K k, @NotNull V v) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public CompletionStage<V> putAsync(@NotNull K k, @NotNull V v, long l, @NotNull TimeUnit timeUnit) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public CompletionStage<V> putAsync(@NotNull K k, @NotNull V v, long l, @NotNull TimeUnit timeUnit, long l1, @NotNull TimeUnit timeUnit1) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public CompletionStage<Void> putAllAsync(@NotNull Map<? extends K, ? extends V> map) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public CompletionStage<Void> setAsync(@NotNull K k, @NotNull V v) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public CompletionStage<Void> setAsync(@NotNull K k, @NotNull V v, long l, @NotNull TimeUnit timeUnit) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public CompletionStage<Void> setAsync(@NotNull K k, @NotNull V v, long l, @NotNull TimeUnit timeUnit, long l1, @NotNull TimeUnit timeUnit1) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public CompletionStage<V> removeAsync(@NotNull K k) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public CompletionStage<Boolean> deleteAsync(@NotNull K k) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public boolean tryRemove(@NotNull K k, long l, @NotNull TimeUnit timeUnit) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public boolean tryPut(@NotNull K k, @NotNull V v, long l, @NotNull TimeUnit timeUnit) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public V put(@NotNull K k, @NotNull V v, long l, @NotNull TimeUnit timeUnit) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public V put(@NotNull K k, @NotNull V v, long l, @NotNull TimeUnit timeUnit, long l1, @NotNull TimeUnit timeUnit1) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public void putTransient(@NotNull K k, @NotNull V v, long l, @NotNull TimeUnit timeUnit) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public void putTransient(@NotNull K k, @NotNull V v, long l, @NotNull TimeUnit timeUnit, long l1, @NotNull TimeUnit timeUnit1) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public V putIfAbsent(@NotNull K k, @NotNull V v) {
+ if (!map.containsKey(k)) {
+ map.put(k, v);
+ return null;
+ }
+ return map.get(k);
+ }
+
+ @Override
+ public V putIfAbsent(@NotNull K k, @NotNull V v, long l, @NotNull TimeUnit timeUnit) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public V putIfAbsent(@NotNull K k, @NotNull V v, long l, @NotNull TimeUnit timeUnit, long l1, @NotNull TimeUnit timeUnit1) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public boolean replace(@NotNull K k, @NotNull V v, @NotNull V v1) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public V replace(@NotNull K k, @NotNull V v) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public void set(@NotNull K k, @NotNull V v) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public void set(@NotNull K k, @NotNull V v, long l, @NotNull TimeUnit timeUnit) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public void set(@NotNull K k, @NotNull V v, long l, @NotNull TimeUnit timeUnit, long l1, @NotNull TimeUnit timeUnit1) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public void setAll(@NotNull Map<? extends K, ? extends V> map) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public CompletionStage<Void> setAllAsync(@NotNull Map<? extends K, ? extends V> map) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public void lock(@NotNull K k) {
+ lock.lock();
+ }
+
+ @Override
+ public void lock(@NotNull K k, long l, @Nullable TimeUnit timeUnit) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public boolean isLocked(@NotNull K k) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public void putAll(@NotNull Map<? extends K, ? extends V> map) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public int size() {
+ return map.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return map.isEmpty();
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ return map.containsKey(key);
+ }
+
+ @Override
+ public boolean containsValue(@NotNull Object o) {
+ return map.containsKey(o);
+ }
+
+ @Override
+ public String getPartitionKey() {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public String getName() {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public String getServiceName() {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public void destroy() {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+}
diff --git a/server/sonar-server-common/build.gradle b/server/sonar-server-common/build.gradle
index 2aefc235c70..5533700fdac 100644
--- a/server/sonar-server-common/build.gradle
+++ b/server/sonar-server-common/build.gradle
@@ -29,7 +29,10 @@ dependencies {
api project(':sonar-markdown')
api project(':sonar-ws')
+ implementation project(':server:sonar-statemachine')
+
compileOnlyApi 'com.github.spotbugs:spotbugs-annotations'
+
testImplementation 'org.elasticsearch.plugin:transport-netty4-client'
testImplementation 'ch.qos.logback:logback-core'
testImplementation 'com.github.spotbugs:spotbugs-annotations'
diff --git a/server/sonar-server-common/src/it/java/org/sonar/server/component/index/EntityDefinitionIndexerIT.java b/server/sonar-server-common/src/it/java/org/sonar/server/component/index/EntityDefinitionIndexerIT.java
index e898a27dbb7..2f624caebef 100644
--- a/server/sonar-server-common/src/it/java/org/sonar/server/component/index/EntityDefinitionIndexerIT.java
+++ b/server/sonar-server-common/src/it/java/org/sonar/server/component/index/EntityDefinitionIndexerIT.java
@@ -21,10 +21,14 @@ package org.sonar.server.component.index;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Optional;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.slf4j.event.Level;
+import org.sonar.api.testfixtures.log.LogTester;
import org.sonar.api.utils.System2;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
@@ -33,15 +37,18 @@ import org.sonar.db.component.BranchDto;
import org.sonar.db.component.ProjectData;
import org.sonar.db.entity.EntityDto;
import org.sonar.db.es.EsQueueDto;
+import org.sonar.db.portfolio.PortfolioDto;
import org.sonar.db.project.ProjectDto;
import org.sonar.server.es.EsClient;
import org.sonar.server.es.EsTester;
import org.sonar.server.es.Indexers;
import org.sonar.server.es.IndexingResult;
+import static java.lang.String.format;
import static java.util.Collections.emptySet;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatException;
import static org.elasticsearch.index.query.QueryBuilders.matchQuery;
import static org.sonar.db.component.ComponentQualifiers.PROJECT;
import static org.sonar.server.component.index.ComponentIndexDefinition.FIELD_NAME;
@@ -60,10 +67,18 @@ public class EntityDefinitionIndexerIT {
public EsTester es = EsTester.create();
@Rule
public DbTester db = DbTester.create(system2);
+ @Rule
+ public LogTester logTester = new LogTester();
private DbClient dbClient = db.getDbClient();
private DbSession dbSession = db.getSession();
- private EntityDefinitionIndexer underTest = new EntityDefinitionIndexer(db.getDbClient(), es.client());
+ private EntityDefinitionIndexer underTest;
+
+ @Before
+ public void setup() {
+ underTest = new EntityDefinitionIndexer(db.getDbClient(), es.client());
+ logTester.setLevel(Level.DEBUG);
+ }
@Test
public void test_getIndexTypes() {
@@ -121,6 +136,62 @@ public class EntityDefinitionIndexerIT {
}
@Test
+ public void indexOnStartup_fixes_corrupted_portfolios_if_possible_and_then_indexes_them() throws Exception {
+ underTest = new EntityDefinitionIndexer(db.getDbClient(), es.client());
+ String uuid = "portfolioUuid1";
+ ProjectDto project = db.components().insertPrivateProject().getProjectDto();
+ PortfolioDto corruptedPortfolio = new PortfolioDto()
+ .setKey("portfolio1")
+ .setName("My Portfolio")
+ .setSelectionMode(PortfolioDto.SelectionMode.NONE)
+ .setUuid(uuid)
+ .setRootUuid(uuid);
+ db.getDbClient().portfolioDao().insert(dbSession, corruptedPortfolio, false);
+
+ // corrupt the portfolio in a fixable way (root portfolio with self-referential parent_uuid)
+ dbSession.getSqlSession().getConnection().prepareStatement(format("UPDATE portfolios SET parent_uuid = '%s' where uuid = '%s'", uuid, uuid))
+ .execute();
+ dbSession.commit();
+ Optional<EntityDto> entity = dbClient.entityDao().selectByUuid(dbSession, uuid);
+
+ assertThat(entity).isPresent();
+ assertThat(entity.get().getAuthUuid()).isNull();
+
+ underTest.indexOnStartup(emptySet());
+
+ assertThat(logTester.logs()).contains("Fixing corrupted portfolio tree for root portfolio " + corruptedPortfolio.getUuid());
+ assertThatIndexContainsOnly(project, corruptedPortfolio);
+ }
+
+ @Test
+ public void indexOnStartup_logs_warning_about_corrupted_portfolios_that_cannot_be_fixed_automatically() throws Exception {
+ underTest = new EntityDefinitionIndexer(db.getDbClient(), es.client());
+ String uuid = "portfolioUuid1";
+ PortfolioDto corruptedPortfolio = new PortfolioDto()
+ .setKey("portfolio1")
+ .setName("My Portfolio")
+ .setSelectionMode(PortfolioDto.SelectionMode.NONE)
+ .setUuid(uuid)
+ .setRootUuid(uuid);
+ db.getDbClient().portfolioDao().insert(dbSession, corruptedPortfolio, false);
+
+ // corrupt the portfolio in an un-fixable way (non-existent parent)
+ dbSession.getSqlSession().getConnection().prepareStatement(format("UPDATE portfolios SET parent_uuid = 'junk_uuid' where uuid = '%s'", uuid))
+ .execute();
+ dbSession.commit();
+ Optional<EntityDto> entity = dbClient.entityDao().selectByUuid(dbSession, uuid);
+
+ assertThat(entity).isPresent();
+ assertThat(entity.get().getAuthUuid()).isNull();
+
+ assertThatException()
+ .isThrownBy(() -> underTest.indexOnStartup(emptySet()));
+
+ assertThat(logTester.logs()).contains("Detected portfolio tree corruption for portfolio " + corruptedPortfolio.getUuid());
+
+ }
+
+ @Test
public void indexOnAnalysis_indexes_project() {
ProjectData project = db.components().insertPrivateProject();
diff --git a/server/sonar-server-common/src/it/java/org/sonar/server/rule/index/RuleIndexIT.java b/server/sonar-server-common/src/it/java/org/sonar/server/rule/index/RuleIndexIT.java
index c218b8759d7..42d89f332e0 100644
--- a/server/sonar-server-common/src/it/java/org/sonar/server/rule/index/RuleIndexIT.java
+++ b/server/sonar-server-common/src/it/java/org/sonar/server/rule/index/RuleIndexIT.java
@@ -516,6 +516,22 @@ class RuleIndexIT {
@ParameterizedTest
@ValueSource(booleans = {true, false})
+ void search_by_security_owaspMobileTop10_2024_return_correct_data_based_on_mode(boolean mqrMode) {
+ doReturn(Optional.of(mqrMode)).when(config).getBoolean(MULTI_QUALITY_MODE_ENABLED);
+ RuleDto rule1 = createRule(setSecurityStandards(of("owaspMobileTop10-2024:m1", "owaspMobileTop10-2024:m10", "cwe:543")),
+ r -> r.setType(VULNERABILITY).replaceAllDefaultImpacts(List.of(new ImpactDto(SECURITY, Severity.HIGH))));
+ RuleDto rule2 = createRule(setSecurityStandards(of("owaspMobileTop10-2024:m10", "cwe:543")), r -> r.setType(SECURITY_HOTSPOT));
+ createRule(setSecurityStandards(of("cwe:543")),
+ r -> r.setType(CODE_SMELL).replaceAllDefaultImpacts(List.of(new ImpactDto(MAINTAINABILITY, Severity.HIGH))));
+ index();
+
+ RuleQuery query = new RuleQuery().setOwaspMobileTop10For2024(of("m5", "m10"));
+ SearchIdResult<String> results = underTest.search(query, new SearchOptions().addFacets("owaspMobileTop10-2024"));
+ assertThat(results.getUuids()).containsOnly(rule1.getUuid(), rule2.getUuid());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
void search_by_security_sansTop25_return_correct_data_based_on_mode(boolean mqrMode) {
doReturn(Optional.of(mqrMode)).when(config).getBoolean(MULTI_QUALITY_MODE_ENABLED);
RuleDto rule1 = createRule(setSecurityStandards(of("owaspTop10:a1", "owaspTop10:a10", "cwe:89")),
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/component/index/EntityDefinitionIndexer.java b/server/sonar-server-common/src/main/java/org/sonar/server/component/index/EntityDefinitionIndexer.java
index 9a9a4cb96f3..a2a2a55ae9c 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/component/index/EntityDefinitionIndexer.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/component/index/EntityDefinitionIndexer.java
@@ -30,6 +30,8 @@ import java.util.stream.Collectors;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.BranchDto;
@@ -56,7 +58,7 @@ import static org.sonar.server.component.index.ComponentIndexDefinition.TYPE_COM
* Indexes the definition of all entities: projects, applications, portfolios and sub-portfolios.
*/
public class EntityDefinitionIndexer implements EventIndexer, AnalysisIndexer, NeedAuthorizationIndexer {
-
+ private static final Logger LOG = LoggerFactory.getLogger(EntityDefinitionIndexer.class);
private static final AuthorizationScope AUTHORIZATION_SCOPE = new AuthorizationScope(TYPE_COMPONENT, entity -> true);
private static final Set<IndexType> INDEX_TYPES = Set.of(TYPE_COMPONENT);
@@ -172,16 +174,43 @@ public class EntityDefinitionIndexer implements EventIndexer, AnalysisIndexer, N
private void doIndexByEntityUuid(Size bulkSize) {
BulkIndexer bulk = new BulkIndexer(esClient, TYPE_COMPONENT, bulkSize);
bulk.start();
+ Set<EntityDto> corruptedEntities = new HashSet<>();
try (DbSession dbSession = dbClient.openSession(false)) {
dbClient.entityDao().scrollForIndexing(dbSession, context -> {
EntityDto dto = context.getResultObject();
- bulk.add(toDocument(dto).toIndexRequest());
+ if (dto.getAuthUuid() == null) {
+ corruptedEntities.add(dto);
+ } else {
+ bulk.add(toDocument(dto).toIndexRequest());
+ }
});
+ if (!corruptedEntities.isEmpty()) {
+ attemptToFixCorruptedEntities(dbSession, corruptedEntities);
+ List<EntityDto> fixedEntities = dbClient.entityDao().selectByUuids(dbSession, corruptedEntities.stream().map(EntityDto::getUuid).toList());
+ fixedEntities.forEach(entity -> bulk.add(toDocument(entity).toIndexRequest()));
+ }
}
bulk.stop();
}
+ private void attemptToFixCorruptedEntities(DbSession dbSession, Set<EntityDto> corruptedEntities) {
+ for (EntityDto entity : corruptedEntities) {
+ dbClient.portfolioDao().selectByUuid(dbSession, entity.getUuid()).ifPresent(portfolio -> {
+ String portfolioUuid = portfolio.getUuid();
+ String rootUuid = portfolio.getRootUuid();
+ String parentUuid = portfolio.getParentUuid();
+ if (portfolioUuid.equals(rootUuid) && portfolioUuid.equals(parentUuid)) {
+ LOG.warn("Fixing corrupted portfolio tree for root portfolio {}", portfolioUuid);
+ portfolio.setParentUuid(null);
+ dbClient.portfolioDao().update(dbSession, portfolio);
+ } else {
+ LOG.warn("Detected portfolio tree corruption for portfolio {}", portfolioUuid);
+ }
+ });
+ }
+ }
+
private static void addProjectDeletionToBulkIndexer(BulkIndexer bulkIndexer, String projectUuid) {
SearchRequest searchRequest = EsClient.prepareSearch(TYPE_COMPONENT.getMainType())
.source(new SearchSourceBuilder().query(QueryBuilders.termQuery(ComponentIndexDefinition.FIELD_UUID, projectUuid)))
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/feature/SonarQubeFeature.java b/server/sonar-server-common/src/main/java/org/sonar/server/feature/SonarQubeFeature.java
index af149a25f55..7a18916216b 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/feature/SonarQubeFeature.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/feature/SonarQubeFeature.java
@@ -24,4 +24,12 @@ public interface SonarQubeFeature {
String getName();
boolean isAvailable();
+
+ default boolean isEnabled() {
+ return isAvailable();
+ }
+
+ default void setEnabled(boolean enabled) {
+ throw new UnsupportedOperationException("This feature does not support enabling/disabling.");
+ }
}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/SearchRequest.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/SearchRequest.java
index 343075669ef..90b6e0bfa01 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/SearchRequest.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/SearchRequest.java
@@ -70,6 +70,7 @@ public class SearchRequest {
private Set<String> types;
private List<String> pciDss32;
private List<String> pciDss40;
+ private List<String> owaspMobileTop10For2024;
private List<String> owaspTop10;
private List<String> owaspAsvs40;
private List<String> owaspTop10For2021;
@@ -419,6 +420,16 @@ public class SearchRequest {
}
@CheckForNull
+ public List<String> getOwaspMobileTop10For2024() {
+ return owaspMobileTop10For2024;
+ }
+
+ public SearchRequest setOwaspMobileTop10For2024(@Nullable List<String> owaspMobileTop10For2024) {
+ this.owaspMobileTop10For2024 = owaspMobileTop10For2024;
+ return this;
+ }
+
+ @CheckForNull
public List<String> getOwaspTop10() {
return owaspTop10;
}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/TaintChecker.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/TaintChecker.java
index 972f06e0366..0ffc6baebed 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/TaintChecker.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/TaintChecker.java
@@ -77,8 +77,8 @@ public class TaintChecker {
}
private List<String> initializeRepositories() {
- List<String> repositories = new ArrayList<>(List.of("roslyn.sonaranalyzer.security.cs",
- "javasecurity", "jssecurity", "tssecurity", "phpsecurity", "pythonsecurity"));
+ List<String> repositories = new ArrayList<>(List.of("gosecurity", "javasecurity", "jssecurity", "kotlinsecurity", "phpsecurity", "pythonsecurity",
+ "roslyn.sonaranalyzer.security.cs", "tssecurity", "vbnetsecurity"));
if (!config.hasKey(EXTRA_TAINT_REPOSITORIES)) {
return repositories;
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java
index 4f044ecc2e0..e199187e443 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java
@@ -336,6 +336,16 @@ public class IssueDoc extends BaseDoc {
}
@CheckForNull
+ public Collection<String> getOwaspMobileTop10For2024() {
+ return getNullableField(IssueIndexDefinition.FIELD_ISSUE_OWASP_MOBILE_TOP_10_2024);
+ }
+
+ public IssueDoc setOwaspMobileTop10For2024(@Nullable Collection<String> o) {
+ setField(IssueIndexDefinition.FIELD_ISSUE_OWASP_MOBILE_TOP_10_2024, o);
+ return this;
+ }
+
+ @CheckForNull
public Collection<String> getOwaspTop10() {
return getNullableField(IssueIndexDefinition.FIELD_ISSUE_OWASP_TOP_10);
}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java
index 685797b4db5..4f54d03cef1 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java
@@ -93,6 +93,7 @@ public class IssueIndexDefinition implements IndexDefinition {
public static final String FIELD_ISSUE_PCI_DSS_40 = "pciDss-4.0";
public static final String FIELD_ISSUE_OWASP_ASVS_40 = "owaspAsvs-4.0";
public static final String FIELD_ISSUE_OWASP_ASVS_40_LEVEL = "owaspAsvs-4.0-level";
+ public static final String FIELD_ISSUE_OWASP_MOBILE_TOP_10_2024 = "owaspMobileTop10-2024";
public static final String FIELD_ISSUE_OWASP_TOP_10 = "owaspTop10";
public static final String FIELD_ISSUE_OWASP_TOP_10_2021 = "owaspTop10-2021";
public static final String FIELD_ISSUE_SANS_TOP_25 = "sansTop25";
@@ -180,6 +181,7 @@ public class IssueIndexDefinition implements IndexDefinition {
mapping.keywordFieldBuilder(FIELD_ISSUE_PCI_DSS_40).disableNorms().build();
mapping.keywordFieldBuilder(FIELD_ISSUE_OWASP_ASVS_40).disableNorms().build();
mapping.keywordFieldBuilder(FIELD_ISSUE_OWASP_ASVS_40_LEVEL).disableNorms().build();
+ mapping.keywordFieldBuilder(FIELD_ISSUE_OWASP_MOBILE_TOP_10_2024).disableNorms().build();
mapping.keywordFieldBuilder(FIELD_ISSUE_OWASP_TOP_10).disableNorms().build();
mapping.keywordFieldBuilder(FIELD_ISSUE_OWASP_TOP_10_2021).disableNorms().build();
mapping.keywordFieldBuilder(FIELD_ISSUE_SANS_TOP_25).disableNorms().build();
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java
index 676a6284b61..7383ef4a757 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java
@@ -32,8 +32,8 @@ import org.apache.ibatis.cursor.Cursor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.rules.CleanCodeAttribute;
-import org.sonar.core.rule.RuleType;
import org.sonar.api.server.rule.RulesDefinition.StigVersion;
+import org.sonar.core.rule.RuleType;
import org.sonar.db.DatabaseUtils;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
@@ -130,6 +130,7 @@ class IssueIteratorForSingleChunk implements IssueIterator {
doc.setImpacts(indexedIssueDto.getEffectiveImpacts());
SecurityStandards securityStandards = fromSecurityStandards(deserializeSecurityStandardsString(indexedIssueDto.getSecurityStandards()));
SecurityStandards.SQCategory sqCategory = securityStandards.getSqCategory();
+ doc.setOwaspMobileTop10For2024(securityStandards.getOwaspMobileTop10For2024());
doc.setOwaspTop10(securityStandards.getOwaspTop10());
doc.setOwaspTop10For2021(securityStandards.getOwaspTop10For2021());
doc.setStigAsdV5R3(securityStandards.getStig(StigVersion.ASD_V5R3));
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/SecurityStandardCategoryStatistics.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/SecurityStandardCategoryStatistics.java
index 1c8dfb0696b..903fe411831 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/SecurityStandardCategoryStatistics.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/SecurityStandardCategoryStatistics.java
@@ -20,6 +20,7 @@
package org.sonar.server.issue.index;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import javax.annotation.Nullable;
@@ -38,9 +39,11 @@ public class SecurityStandardCategoryStatistics {
private boolean hasMoreRules;
private final Optional<String> version;
private Optional<String> level = Optional.empty();
+ private final Map<String, Long> severityDistribution;
public SecurityStandardCategoryStatistics(String category, long vulnerabilities, OptionalInt vulnerabiliyRating, long toReviewSecurityHotspots,
- long reviewedSecurityHotspots, Integer securityReviewRating, @Nullable List<SecurityStandardCategoryStatistics> children, @Nullable String version) {
+ long reviewedSecurityHotspots, Integer securityReviewRating, @Nullable List<SecurityStandardCategoryStatistics> children, @Nullable String version,
+ Map<String, Long> severityDistribution) {
this.category = category;
this.vulnerabilities = vulnerabilities;
this.vulnerabilityRating = vulnerabiliyRating;
@@ -50,6 +53,30 @@ public class SecurityStandardCategoryStatistics {
this.children = children;
this.version = Optional.ofNullable(version);
this.hasMoreRules = false;
+ this.severityDistribution = severityDistribution;
+ }
+
+ public SecurityStandardCategoryStatistics withModifiedVulnerabilities(
+ int additionalVulnerabilities,
+ @Nullable Integer newVulnerabilityRating) {
+ OptionalInt newVulnerabilityRatingValue;
+
+ if (newVulnerabilityRating != null) {
+ newVulnerabilityRatingValue = OptionalInt.of(Math.max(newVulnerabilityRating, this.getVulnerabilityRating().orElse(0)));
+ } else {
+ newVulnerabilityRatingValue = this.getVulnerabilityRating();
+ }
+
+ return new SecurityStandardCategoryStatistics(
+ this.getCategory(),
+ this.getVulnerabilities() + additionalVulnerabilities,
+ newVulnerabilityRatingValue,
+ this.getToReviewSecurityHotspots(),
+ this.getReviewedSecurityHotspots(),
+ this.getSecurityReviewRating(),
+ this.getChildren(),
+ this.getVersion().orElse(null),
+ this.getSeverityDistribution());
}
public String getCategory() {
@@ -118,4 +145,8 @@ public class SecurityStandardCategoryStatistics {
public void setHasMoreRules(boolean hasMoreRules) {
this.hasMoreRules = hasMoreRules;
}
+
+ public Map<String, Long> getSeverityDistribution() {
+ return severityDistribution;
+ }
}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationHandler.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationHandler.java
index 154d5db7640..352e22c2995 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationHandler.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationHandler.java
@@ -44,7 +44,7 @@ import static org.sonar.server.notification.NotificationManager.SubscriberPermis
public class ChangesOnMyIssueNotificationHandler extends EmailNotificationHandler<IssuesChangesNotification> {
- private static final String KEY = "ChangesOnMyIssue";
+ public static final String KEY = "ChangesOnMyIssue";
private static final NotificationDispatcherMetadata METADATA = NotificationDispatcherMetadata.create(KEY)
.setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(true))
.setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true));
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/IssueWorkflow.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/IssueWorkflow.java
index abda9fc29d8..82da993207e 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/IssueWorkflow.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/IssueWorkflow.java
@@ -27,7 +27,6 @@ import org.sonar.core.issue.IssueChangeContext;
import org.sonar.core.rule.RuleType;
import org.sonar.server.issue.workflow.codequalityissue.CodeQualityIssueWorkflow;
import org.sonar.server.issue.workflow.securityhotspot.SecurityHotspotWorkflow;
-import org.sonar.server.issue.workflow.statemachine.Transition;
/**
* Common entry point for both issues and security hotspots, because some features are not making the difference.
@@ -57,11 +56,11 @@ public class IssueWorkflow {
}
}
- public List<Transition> outTransitions(DefaultIssue issue) {
+ public List<String> outTransitionsKeys(DefaultIssue issue) {
if (isSecurityHotspot(issue)) {
- return securityHotspotWorkflow.outTransitions(issue);
+ return securityHotspotWorkflow.outTransitionsKeys(issue);
} else {
- return codeQualityIssueWorkflow.outTransitions(issue);
+ return codeQualityIssueWorkflow.outTransitionsKeys(issue);
}
}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/codequalityissue/CodeQualityIssueWorkflow.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/codequalityissue/CodeQualityIssueWorkflow.java
index 1325e685b97..750fcc81107 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/codequalityissue/CodeQualityIssueWorkflow.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/codequalityissue/CodeQualityIssueWorkflow.java
@@ -29,10 +29,10 @@ import org.sonar.api.issue.IssueStatus;
import org.sonar.api.server.ServerSide;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.IssueChangeContext;
+import org.sonar.issue.workflow.statemachine.State;
+import org.sonar.issue.workflow.statemachine.Transition;
import org.sonar.server.issue.TaintChecker;
import org.sonar.server.issue.workflow.issue.IssueWorkflowEntityAdapter;
-import org.sonar.server.issue.workflow.statemachine.State;
-import org.sonar.server.issue.workflow.statemachine.Transition;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
@@ -70,17 +70,19 @@ public class CodeQualityIssueWorkflow {
}
public Set<CodeQualityIssueWorkflowTransition> outTransitionsEnums(DefaultIssue issue) {
- return outTransitions(issue).stream().map(Transition::key)
+ return outTransitionsKeys(issue).stream()
.map(CodeQualityIssueWorkflowTransition::fromKey)
.flatMap(Optional::stream)
.collect(Collectors.toSet());
}
- public List<Transition> outTransitions(DefaultIssue issue) {
+ public List<String> outTransitionsKeys(DefaultIssue issue) {
String status = issue.status();
State<CodeQualityIssueWorkflowEntity, CodeQualityIssueWorkflowActions> state = workflowDefinition.getMachine().state(status);
checkArgument(state != null, "Unknown status: %s", status);
- return state.outManualTransitions(adapt(issue)).stream().map(t -> (Transition) t).toList();
+ return state.outManualTransitions(adapt(issue)).stream()
+ .map(Transition::key)
+ .toList();
}
public void doAutomaticTransition(DefaultIssue issue, IssueChangeContext issueChangeContext) {
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/codequalityissue/CodeQualityIssueWorkflowDefinition.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/codequalityissue/CodeQualityIssueWorkflowDefinition.java
index a190ca1b144..3da617a1be9 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/codequalityissue/CodeQualityIssueWorkflowDefinition.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/codequalityissue/CodeQualityIssueWorkflowDefinition.java
@@ -23,10 +23,10 @@ import java.util.function.Consumer;
import java.util.function.Predicate;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.server.ServerSide;
-import org.sonar.db.permission.ProjectPermission;
-import org.sonar.server.issue.workflow.statemachine.StateMachine;
-import org.sonar.server.issue.workflow.statemachine.Transition;
+import org.sonar.issue.workflow.statemachine.StateMachine;
+import org.sonar.issue.workflow.statemachine.Transition;
+import static java.util.function.Predicate.not;
import static org.sonar.api.issue.Issue.RESOLUTION_FALSE_POSITIVE;
import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
import static org.sonar.api.issue.Issue.RESOLUTION_REMOVED;
@@ -57,6 +57,9 @@ public class CodeQualityIssueWorkflowDefinition {
private static final Consumer<CodeQualityIssueWorkflowActions> UNSET_RESOLUTION = CodeQualityIssueWorkflowActions::unsetResolution;
private static final Consumer<CodeQualityIssueWorkflowActions> RESTORE_RESOLUTION = CodeQualityIssueWorkflowActions::restoreResolution;
+ private static final Predicate<CodeQualityIssueWorkflowEntity> AUTOMATIC_REOPEN_PREDICATE = not(CodeQualityIssueWorkflowEntity::isBeingClosed)
+ .and(issue -> issue.hasAnyResolution(RESOLUTION_FIXED));
+
private final StateMachine<CodeQualityIssueWorkflowEntity, CodeQualityIssueWorkflowActions> machine;
public CodeQualityIssueWorkflowDefinition() {
@@ -78,34 +81,28 @@ public class CodeQualityIssueWorkflowDefinition {
.transition(Transition.<CodeQualityIssueWorkflowEntity, CodeQualityIssueWorkflowActions>builder(ACCEPT.getKey())
.from(STATUS_OPEN).to(STATUS_RESOLVED)
.actions(a -> a.setResolution(RESOLUTION_WONT_FIX), UNSET_ASSIGNEE)
- .requiredProjectPermission(ProjectPermission.ISSUE_ADMIN)
.build())
.transition(Transition.<CodeQualityIssueWorkflowEntity, CodeQualityIssueWorkflowActions>builder(ACCEPT.getKey())
.from(STATUS_REOPENED).to(STATUS_RESOLVED)
.actions(a -> a.setResolution(RESOLUTION_WONT_FIX), UNSET_ASSIGNEE)
- .requiredProjectPermission(ProjectPermission.ISSUE_ADMIN)
.build())
.transition(Transition.<CodeQualityIssueWorkflowEntity, CodeQualityIssueWorkflowActions>builder(ACCEPT.getKey())
.from(STATUS_CONFIRMED).to(STATUS_RESOLVED)
.actions(a -> a.setResolution(RESOLUTION_WONT_FIX), UNSET_ASSIGNEE)
- .requiredProjectPermission(ProjectPermission.ISSUE_ADMIN)
.build())
// resolve as false-positive
.transition(Transition.<CodeQualityIssueWorkflowEntity, CodeQualityIssueWorkflowActions>builder(FALSE_POSITIVE.getKey())
.from(STATUS_OPEN).to(STATUS_RESOLVED)
.actions(a -> a.setResolution(RESOLUTION_FALSE_POSITIVE), UNSET_ASSIGNEE)
- .requiredProjectPermission(ProjectPermission.ISSUE_ADMIN)
.build())
.transition(Transition.<CodeQualityIssueWorkflowEntity, CodeQualityIssueWorkflowActions>builder(FALSE_POSITIVE.getKey())
.from(STATUS_REOPENED).to(STATUS_RESOLVED)
.actions(a -> a.setResolution(RESOLUTION_FALSE_POSITIVE), UNSET_ASSIGNEE)
- .requiredProjectPermission(ProjectPermission.ISSUE_ADMIN)
.build())
.transition(Transition.<CodeQualityIssueWorkflowEntity, CodeQualityIssueWorkflowActions>builder(FALSE_POSITIVE.getKey())
.from(STATUS_CONFIRMED).to(STATUS_RESOLVED)
.actions(a -> a.setResolution(RESOLUTION_FALSE_POSITIVE), UNSET_ASSIGNEE)
- .requiredProjectPermission(ProjectPermission.ISSUE_ADMIN)
.build())
// reopen
@@ -132,34 +129,28 @@ public class CodeQualityIssueWorkflowDefinition {
.transition(Transition.<CodeQualityIssueWorkflowEntity, CodeQualityIssueWorkflowActions>builder(RESOLVE.getKey())
.from(STATUS_OPEN).to(STATUS_RESOLVED)
.actions(a -> a.setResolution(RESOLUTION_FIXED))
- .requiredProjectPermission(ProjectPermission.ISSUE_ADMIN)
.build())
.transition(Transition.<CodeQualityIssueWorkflowEntity, CodeQualityIssueWorkflowActions>builder(RESOLVE.getKey())
.from(STATUS_REOPENED).to(STATUS_RESOLVED)
.actions(a -> a.setResolution(RESOLUTION_FIXED))
- .requiredProjectPermission(ProjectPermission.ISSUE_ADMIN)
.build())
.transition(Transition.<CodeQualityIssueWorkflowEntity, CodeQualityIssueWorkflowActions>builder(RESOLVE.getKey())
.from(STATUS_CONFIRMED).to(STATUS_RESOLVED)
.actions(a -> a.setResolution(RESOLUTION_FIXED))
- .requiredProjectPermission(ProjectPermission.ISSUE_ADMIN)
.build())
// resolve as won't fix, deprecated
.transition(Transition.<CodeQualityIssueWorkflowEntity, CodeQualityIssueWorkflowActions>builder(WONT_FIX.getKey())
.from(STATUS_OPEN).to(STATUS_RESOLVED)
.actions(a -> a.setResolution(RESOLUTION_WONT_FIX), UNSET_ASSIGNEE)
- .requiredProjectPermission(ProjectPermission.ISSUE_ADMIN)
.build())
.transition(Transition.<CodeQualityIssueWorkflowEntity, CodeQualityIssueWorkflowActions>builder(WONT_FIX.getKey())
.from(STATUS_REOPENED).to(STATUS_RESOLVED)
.actions(a -> a.setResolution(RESOLUTION_WONT_FIX), UNSET_ASSIGNEE)
- .requiredProjectPermission(ProjectPermission.ISSUE_ADMIN)
.build())
.transition(Transition.<CodeQualityIssueWorkflowEntity, CodeQualityIssueWorkflowActions>builder(WONT_FIX.getKey())
.from(STATUS_CONFIRMED).to(STATUS_RESOLVED)
.actions(a -> a.setResolution(RESOLUTION_WONT_FIX), UNSET_ASSIGNEE)
- .requiredProjectPermission(ProjectPermission.ISSUE_ADMIN)
.build());
}
@@ -194,9 +185,7 @@ public class CodeQualityIssueWorkflowDefinition {
// Reopen issues that are marked as resolved but that are still alive.
.transition(Transition.<CodeQualityIssueWorkflowEntity, CodeQualityIssueWorkflowActions>builder("automaticreopen")
.from(STATUS_RESOLVED).to(STATUS_REOPENED)
- .conditions(
- Predicate.not(CodeQualityIssueWorkflowEntity::isBeingClosed),
- i -> i.hasAnyResolution(RESOLUTION_FIXED))
+ .conditions(AUTOMATIC_REOPEN_PREDICATE)
.actions(UNSET_RESOLUTION, UNSET_CLOSE_DATE)
.automatic()
.build())
@@ -240,6 +229,8 @@ public class CodeQualityIssueWorkflowDefinition {
.from(STATUS_RESOLVED)
.to(STATUS_REOPENED)
.conditions(
+ // We check this first condition to avoid overlap with the automaticreopen transition.
+ not(AUTOMATIC_REOPEN_PREDICATE),
CodeQualityIssueWorkflowEntity::isTaintVulnerability,
CodeQualityIssueWorkflowEntity::locationsChanged)
.actions(
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/securityhotspot/SecurityHotspotWorkflow.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/securityhotspot/SecurityHotspotWorkflow.java
index 93bbf8c5906..cffefe28741 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/securityhotspot/SecurityHotspotWorkflow.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/securityhotspot/SecurityHotspotWorkflow.java
@@ -26,9 +26,9 @@ import org.sonar.api.issue.IssueStatus;
import org.sonar.api.server.ServerSide;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.IssueChangeContext;
+import org.sonar.issue.workflow.statemachine.State;
+import org.sonar.issue.workflow.statemachine.Transition;
import org.sonar.server.issue.workflow.issue.IssueWorkflowEntityAdapter;
-import org.sonar.server.issue.workflow.statemachine.State;
-import org.sonar.server.issue.workflow.statemachine.Transition;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
@@ -58,11 +58,14 @@ public class SecurityHotspotWorkflow {
return false;
}
- public List<Transition> outTransitions(DefaultIssue issue) {
+ public List<String> outTransitionsKeys(DefaultIssue issue) {
String status = issue.status();
State<SecurityHotspotWorkflowEntity, SecurityHotspotWorkflowActions> state = workflowDefinition.getMachine().state(status);
checkArgument(state != null, "Unknown status: %s", status);
- return state.outManualTransitions(adapt(issue)).stream().map(t -> (Transition) t).toList();
+ return state.outManualTransitions(adapt(issue))
+ .stream()
+ .map(Transition::key)
+ .toList();
}
public void doAutomaticTransition(DefaultIssue issue, IssueChangeContext issueChangeContext) {
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/securityhotspot/SecurityHotspotWorkflowDefinition.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/securityhotspot/SecurityHotspotWorkflowDefinition.java
index c40a955df85..8c6793221af 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/securityhotspot/SecurityHotspotWorkflowDefinition.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/securityhotspot/SecurityHotspotWorkflowDefinition.java
@@ -22,9 +22,8 @@ package org.sonar.server.issue.workflow.securityhotspot;
import java.util.function.Consumer;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.server.ServerSide;
-import org.sonar.db.permission.ProjectPermission;
-import org.sonar.server.issue.workflow.statemachine.StateMachine;
-import org.sonar.server.issue.workflow.statemachine.Transition;
+import org.sonar.issue.workflow.statemachine.StateMachine;
+import org.sonar.issue.workflow.statemachine.Transition;
import static org.sonar.api.issue.Issue.RESOLUTION_ACKNOWLEDGED;
import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
@@ -71,8 +70,7 @@ public class SecurityHotspotWorkflowDefinition {
Transition.TransitionBuilder<SecurityHotspotWorkflowEntity, SecurityHotspotWorkflowActions> reviewedAsFixedBuilder = Transition
.<SecurityHotspotWorkflowEntity, SecurityHotspotWorkflowActions>builder(RESOLVE_AS_REVIEWED.getKey())
.to(STATUS_REVIEWED)
- .actions(a -> a.setResolution(RESOLUTION_FIXED))
- .requiredProjectPermission(ProjectPermission.SECURITYHOTSPOT_ADMIN);
+ .actions(a -> a.setResolution(RESOLUTION_FIXED));
builder
.transition(reviewedAsFixedBuilder
.from(STATUS_TO_REVIEW)
@@ -86,8 +84,7 @@ public class SecurityHotspotWorkflowDefinition {
Transition.TransitionBuilder<SecurityHotspotWorkflowEntity, SecurityHotspotWorkflowActions> resolveAsSafeTransitionBuilder = Transition
.<SecurityHotspotWorkflowEntity, SecurityHotspotWorkflowActions>builder(RESOLVE_AS_SAFE.getKey())
.to(STATUS_REVIEWED)
- .actions(a -> a.setResolution(RESOLUTION_SAFE))
- .requiredProjectPermission(ProjectPermission.SECURITYHOTSPOT_ADMIN);
+ .actions(a -> a.setResolution(RESOLUTION_SAFE));
builder
.transition(resolveAsSafeTransitionBuilder
.from(STATUS_TO_REVIEW)
@@ -101,8 +98,7 @@ public class SecurityHotspotWorkflowDefinition {
Transition.TransitionBuilder<SecurityHotspotWorkflowEntity, SecurityHotspotWorkflowActions> resolveAsAcknowledgedTransitionBuilder = Transition
.<SecurityHotspotWorkflowEntity, SecurityHotspotWorkflowActions>builder(RESOLVE_AS_ACKNOWLEDGED.getKey())
.to(STATUS_REVIEWED)
- .actions(a -> a.setResolution(RESOLUTION_ACKNOWLEDGED))
- .requiredProjectPermission(ProjectPermission.SECURITYHOTSPOT_ADMIN);
+ .actions(a -> a.setResolution(RESOLUTION_ACKNOWLEDGED));
builder
.transition(resolveAsAcknowledgedTransitionBuilder
.from(STATUS_TO_REVIEW)
@@ -118,7 +114,6 @@ public class SecurityHotspotWorkflowDefinition {
.from(STATUS_REVIEWED).to(STATUS_TO_REVIEW)
.conditions(sh -> sh.hasAnyResolution(RESOLUTION_FIXED, RESOLUTION_SAFE, RESOLUTION_ACKNOWLEDGED))
.actions(UNSET_RESOLUTION)
- .requiredProjectPermission(ProjectPermission.SECURITYHOTSPOT_ADMIN)
.build());
}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/securityhotspot/SecurityHotspotWorkflowTransition.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/securityhotspot/SecurityHotspotWorkflowTransition.java
index af6c9dae152..659774ab983 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/securityhotspot/SecurityHotspotWorkflowTransition.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/securityhotspot/SecurityHotspotWorkflowTransition.java
@@ -26,18 +26,7 @@ import java.util.stream.Stream;
import org.sonar.server.issue.workflow.WorkflowTransition;
public enum SecurityHotspotWorkflowTransition implements WorkflowTransition {
- /**
- * @deprecated since 8.1, transition has no effect
- */
- @Deprecated
- SET_AS_IN_REVIEW("setinreview"),
- /**
- * @since 7.8
- * @deprecated since 8.1, security hotspots can no longer be converted to vulnerabilities
- */
- @Deprecated
- OPEN_AS_VULNERABILITY("openasvulnerability"),
RESOLVE_AS_REVIEWED("resolveasreviewed"),
RESOLVE_AS_SAFE("resolveassafe"),
RESOLVE_AS_ACKNOWLEDGED("resolveasacknowledged"),
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/metric/StandardToMQRMetrics.java b/server/sonar-server-common/src/main/java/org/sonar/server/metric/StandardToMQRMetrics.java
index 53d39f62652..6ebf315f6ca 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/metric/StandardToMQRMetrics.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/metric/StandardToMQRMetrics.java
@@ -22,6 +22,7 @@ package org.sonar.server.metric;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import java.util.Optional;
+import java.util.Set;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.core.metric.SoftwareQualitiesMetrics;
@@ -120,6 +121,14 @@ public class StandardToMQRMetrics {
return MQR_TO_STANDARD_MODE_METRICS.containsKey(metricKey);
}
+ public static Set<String> getMQRMetrics() {
+ return STANDARD_TO_MQR_MODE_METRICS.keySet();
+ }
+
+ public static Set<String> getStandardMetrics() {
+ return MQR_TO_STANDARD_MODE_METRICS.keySet();
+ }
+
/**
* Retrieves equivalent metric in the other mode. Return empty if metric has no equivalence
*/
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/webhook/NetworkInterfaceProvider.java b/server/sonar-server-common/src/main/java/org/sonar/server/network/NetworkInterfaceProvider.java
index 0616e438276..f9da21d10eb 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/webhook/NetworkInterfaceProvider.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/network/NetworkInterfaceProvider.java
@@ -17,7 +17,7 @@
* 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.server.webhook;
+package org.sonar.server.network;
import java.net.InetAddress;
import java.net.NetworkInterface;
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/statemachine/package-info.java b/server/sonar-server-common/src/main/java/org/sonar/server/network/package-info.java
index d288e191fed..d7820748e60 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/statemachine/package-info.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/network/package-info.java
@@ -18,6 +18,6 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
@ParametersAreNonnullByDefault
-package org.sonar.server.issue.workflow.statemachine;
+package org.sonar.server.network;
import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleDoc.java b/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleDoc.java
index ae66aec98a8..bf4ad404ac4 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleDoc.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleDoc.java
@@ -186,6 +186,16 @@ public class RuleDoc extends BaseDoc {
}
@CheckForNull
+ public Collection<String> getOwaspMobileTop10For2024() {
+ return getNullableField(RuleIndexDefinition.FIELD_RULE_OWASP_MOBILE_TOP_10_2024);
+ }
+
+ public RuleDoc setOwaspMobileTop10For2024(@Nullable Collection<String> o) {
+ setField(RuleIndexDefinition.FIELD_RULE_OWASP_MOBILE_TOP_10_2024, o);
+ return this;
+ }
+
+ @CheckForNull
public Collection<String> getSansTop25() {
return getNullableField(RuleIndexDefinition.FIELD_RULE_SANS_TOP_25);
}
@@ -330,6 +340,7 @@ public class RuleDoc extends BaseDoc {
.setCwe(securityStandards.getCwe())
.setOwaspTop10(securityStandards.getOwaspTop10())
.setOwaspTop10For2021(securityStandards.getOwaspTop10For2021())
+ .setOwaspMobileTop10For2024(securityStandards.getOwaspMobileTop10For2024())
.setSansTop25(securityStandards.getSansTop25())
.setSonarSourceSecurityCategory(securityStandards.getSqCategory())
.setName(dto.getName())
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleIndex.java b/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleIndex.java
index 3f443234135..22911ec6577 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleIndex.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleIndex.java
@@ -53,6 +53,7 @@ import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.FieldSortBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
+import org.jetbrains.annotations.NotNull;
import org.sonar.api.config.Configuration;
import org.sonar.api.issue.impact.SoftwareQuality;
import org.sonar.api.rule.RuleStatus;
@@ -115,6 +116,7 @@ import static org.sonar.server.rule.index.RuleIndexDefinition.FIELD_RULE_IS_TEMP
import static org.sonar.server.rule.index.RuleIndexDefinition.FIELD_RULE_KEY;
import static org.sonar.server.rule.index.RuleIndexDefinition.FIELD_RULE_LANGUAGE;
import static org.sonar.server.rule.index.RuleIndexDefinition.FIELD_RULE_NAME;
+import static org.sonar.server.rule.index.RuleIndexDefinition.FIELD_RULE_OWASP_MOBILE_TOP_10_2024;
import static org.sonar.server.rule.index.RuleIndexDefinition.FIELD_RULE_OWASP_TOP_10;
import static org.sonar.server.rule.index.RuleIndexDefinition.FIELD_RULE_OWASP_TOP_10_2021;
import static org.sonar.server.rule.index.RuleIndexDefinition.FIELD_RULE_REPOSITORY;
@@ -155,6 +157,7 @@ public class RuleIndex {
public static final String FACET_SANS_TOP_25 = "sansTop25";
public static final String FACET_OWASP_TOP_10 = "owaspTop10";
public static final String FACET_OWASP_TOP_10_2021 = "owaspTop10-2021";
+ public static final String FACET_OWASP_MOBILE_TOP_10_2024 = "owaspMobileTop10-2024";
public static final String FACET_SONARSOURCE_SECURITY = "sonarsourceSecurity";
public static final String FACET_CLEAN_CODE_ATTRIBUTE_CATEGORY = "cleanCodeAttributeCategories";
public static final String FACET_IMPACT_SOFTWARE_QUALITY = "impactSoftwareQualities";
@@ -313,6 +316,8 @@ public class RuleIndex {
addSecurityStandardFilter(filters, FIELD_RULE_OWASP_TOP_10_2021, query.getOwaspTop10For2021());
+ addSecurityStandardFilter(filters, FIELD_RULE_OWASP_MOBILE_TOP_10_2024, query.getOwaspMobileTop10For2024());
+
addSecurityStandardFilter(filters, FIELD_RULE_SANS_TOP_25, query.getSansTop25());
addSecurityStandardFilter(filters, FIELD_RULE_SONARSOURCE_SECURITY, query.getSonarsourceSecurity());
@@ -538,13 +543,13 @@ public class RuleIndex {
Collection<String> languages = query.getLanguages();
aggregations.put(FACET_LANGUAGES,
stickyFacetBuilder.buildStickyFacet(FIELD_RULE_LANGUAGE, FACET_LANGUAGES, MAX_FACET_SIZE,
- (languages == null) ? (new String[0]) : languages.toArray()));
+ toStringArray(languages)));
}
if (options.getFacets().contains(FACET_TAGS) || options.getFacets().contains(FACET_OLD_DEFAULT)) {
Collection<String> tags = query.getTags();
aggregations.put(FACET_TAGS,
stickyFacetBuilder.buildStickyFacet(FIELD_RULE_TAGS, FACET_TAGS, MAX_FACET_SIZE,
- (tags == null) ? (new String[0]) : tags.toArray()));
+ toStringArray(tags)));
}
if (options.getFacets().contains(FACET_TYPES)) {
Collection<RuleType> types = query.getTypes();
@@ -556,13 +561,13 @@ public class RuleIndex {
Collection<String> repositories = query.getRepositories();
aggregations.put(FACET_REPOSITORIES,
stickyFacetBuilder.buildStickyFacet(FIELD_RULE_REPOSITORY, FACET_REPOSITORIES, MAX_FACET_SIZE,
- (repositories == null) ? (new String[0]) : repositories.toArray()));
+ toStringArray(repositories)));
}
if (options.getFacets().contains(FACET_CLEAN_CODE_ATTRIBUTE_CATEGORY)) {
Collection<String> cleanCodeCategories = query.getCleanCodeAttributesCategories();
aggregations.put(FACET_CLEAN_CODE_ATTRIBUTE_CATEGORY,
stickyFacetBuilder.buildStickyFacet(FIELD_RULE_CLEAN_CODE_ATTRIBUTE_CATEGORY, FACET_CLEAN_CODE_ATTRIBUTE_CATEGORY, MAX_FACET_SIZE,
- (cleanCodeCategories == null) ? (new String[0]) : cleanCodeCategories.toArray()));
+ toStringArray(cleanCodeCategories)));
}
addImpactSoftwareQualityFacetIfNeeded(options, query, aggregations, stickyFacetBuilder);
@@ -701,38 +706,50 @@ public class RuleIndex {
aggregations.put(FACET_CWE,
stickyFacetBuilder.buildStickyFacet(FIELD_RULE_CWE, FACET_CWE,
FACET_DEFAULT_SIZE, filterSecurityCategories(),
- (categories == null) ? (new String[0]) : categories.toArray()));
+ toStringArray(categories)));
}
if (options.getFacets().contains(FACET_OWASP_TOP_10)) {
Collection<String> categories = query.getOwaspTop10();
aggregations.put(FACET_OWASP_TOP_10,
stickyFacetBuilder.buildStickyFacet(FIELD_RULE_OWASP_TOP_10, FACET_OWASP_TOP_10,
FACET_DEFAULT_SIZE, filterSecurityCategories(),
- (categories == null) ? (new String[0]) : categories.toArray()));
+ toStringArray(categories)));
}
if (options.getFacets().contains(FACET_OWASP_TOP_10_2021)) {
Collection<String> categories = query.getOwaspTop10For2021();
aggregations.put(FACET_OWASP_TOP_10_2021,
stickyFacetBuilder.buildStickyFacet(FIELD_RULE_OWASP_TOP_10_2021, FACET_OWASP_TOP_10_2021,
FACET_DEFAULT_SIZE, filterSecurityCategories(),
- (categories == null) ? (new String[0]) : categories.toArray()));
+ toStringArray(categories)));
+ }
+ if (options.getFacets().contains(FACET_OWASP_MOBILE_TOP_10_2024)) {
+ Collection<String> categories = query.getOwaspTop10For2021();
+ aggregations.put(FACET_OWASP_MOBILE_TOP_10_2024,
+ stickyFacetBuilder.buildStickyFacet(FIELD_RULE_OWASP_MOBILE_TOP_10_2024, FACET_OWASP_MOBILE_TOP_10_2024,
+ FACET_DEFAULT_SIZE, filterSecurityCategories(),
+ toStringArray(categories)));
}
if (options.getFacets().contains(FACET_SANS_TOP_25)) {
Collection<String> categories = query.getSansTop25();
aggregations.put(FACET_SANS_TOP_25,
stickyFacetBuilder.buildStickyFacet(FIELD_RULE_SANS_TOP_25, FACET_SANS_TOP_25,
FACET_DEFAULT_SIZE, filterSecurityCategories(),
- (categories == null) ? (new String[0]) : categories.toArray()));
+ toStringArray(categories)));
}
if (options.getFacets().contains(FACET_SONARSOURCE_SECURITY)) {
Collection<String> categories = query.getSonarsourceSecurity();
aggregations.put(FACET_SONARSOURCE_SECURITY,
stickyFacetBuilder.buildStickyFacet(FIELD_RULE_SONARSOURCE_SECURITY, FACET_SONARSOURCE_SECURITY,
SecurityStandards.SQCategory.values().length, filterSecurityCategories(),
- (categories == null) ? (new String[0]) : categories.toArray()));
+ toStringArray(categories)));
}
}
+ @NotNull
+ private static Object[] toStringArray(@Nullable Collection<String> items) {
+ return (items == null) ? (new String[0]) : items.toArray(new String[0]);
+ }
+
private static void addStatusFacetIfNeeded(SearchOptions options, Map<String, AggregationBuilder> aggregations, StickyFacetBuilder stickyFacetBuilder) {
if (options.getFacets().contains(FACET_STATUSES)) {
BoolQueryBuilder facetFilter = stickyFacetBuilder.getStickyFacetFilter(FIELD_RULE_STATUS);
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleIndexDefinition.java b/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleIndexDefinition.java
index 76170885f2b..7bce1048b8d 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleIndexDefinition.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleIndexDefinition.java
@@ -63,6 +63,7 @@ public class RuleIndexDefinition implements IndexDefinition {
public static final String FIELD_RULE_CWE = "cwe";
public static final String FIELD_RULE_OWASP_TOP_10 = "owaspTop10";
public static final String FIELD_RULE_OWASP_TOP_10_2021 = "owaspTop10-2021";
+ public static final String FIELD_RULE_OWASP_MOBILE_TOP_10_2024 = "owaspMobileTop10-2024";
public static final String FIELD_RULE_SANS_TOP_25 = "sansTop25";
public static final String FIELD_RULE_SONARSOURCE_SECURITY = "sonarsourceSecurity";
public static final String FIELD_RULE_TAGS = "tags";
@@ -154,6 +155,7 @@ public class RuleIndexDefinition implements IndexDefinition {
ruleMapping.keywordFieldBuilder(FIELD_RULE_CWE).disableNorms().build();
ruleMapping.keywordFieldBuilder(FIELD_RULE_OWASP_TOP_10).disableNorms().build();
ruleMapping.keywordFieldBuilder(FIELD_RULE_OWASP_TOP_10_2021).disableNorms().build();
+ ruleMapping.keywordFieldBuilder(FIELD_RULE_OWASP_MOBILE_TOP_10_2024).disableNorms().build();
ruleMapping.keywordFieldBuilder(FIELD_RULE_SANS_TOP_25).disableNorms().build();
ruleMapping.keywordFieldBuilder(FIELD_RULE_SONARSOURCE_SECURITY).disableNorms().build();
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleQuery.java b/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleQuery.java
index d0904071684..f309a66eb16 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleQuery.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleQuery.java
@@ -31,39 +31,38 @@ import org.sonar.db.qualityprofile.QProfileDto;
import static java.util.Arrays.asList;
public class RuleQuery {
-
- private String key;
- private String queryText;
- private Collection<String> languages;
- private Collection<String> repositories;
- private Collection<String> severities;
- private Collection<RuleStatus> statuses;
- private Collection<String> tags;
- private Collection<RuleType> types;
- private Boolean activation;
- private QProfileDto profile;
- private QProfileDto compareToQProfile;
- private Collection<String> inheritance;
- private Collection<String> activeSeverities;
- private String templateKey;
- private Boolean isTemplate;
- private Long availableSince;
- private String sortField;
+ private String key = null;
+ private String queryText = null;
+ private Collection<String> languages = null;
+ private Collection<String> repositories = null;
+ private Collection<String> severities = null;
+ private Collection<RuleStatus> statuses = null;
+ private Collection<String> tags = null;
+ private Collection<RuleType> types = null;
+ private Boolean activation = null;
+ private QProfileDto profile = null;
+ private QProfileDto compareToQProfile = null;
+ private Collection<String> inheritance = null;
+ private Collection<String> activeSeverities = null;
+ private String templateKey = null;
+ private Boolean isTemplate = null;
+ private Long availableSince = null;
+ private String sortField = null;
private boolean ascendingSort = true;
- private String internalKey;
- private String ruleKey;
- private boolean includeExternal;
- private Collection<String> owaspTop10;
- private Collection<String> owaspTop10For2021;
- private Collection<String> sansTop25;
- private Collection<String> cwe;
- private Collection<String> sonarsourceSecurity;
- private Collection<String> impactSeverities;
- private Collection<String> impactSoftwareQualities;
- private Collection<String> activeImpactSeverities;
- private Collection<String> cleanCodeAttributesCategories;
- private Boolean prioritizedRule;
-
+ private String internalKey = null;
+ private String ruleKey = null;
+ private boolean includeExternal = false;
+ private Collection<String> owaspTop10 = null;
+ private Collection<String> owaspTop10For2021 = null;
+ private Collection<String> owaspMobileTop10For2024 = null;
+ private Collection<String> sansTop25 = null;
+ private Collection<String> cwe = null;
+ private Collection<String> sonarsourceSecurity = null;
+ private Collection<String> impactSeverities = null;
+ private Collection<String> impactSoftwareQualities = null;
+ private Collection<String> activeImpactSeverities = null;
+ private Collection<String> cleanCodeAttributesCategories = null;
+ private Boolean prioritizedRule = null;
@CheckForNull
public QProfileDto getQProfile() {
@@ -335,6 +334,16 @@ public class RuleQuery {
}
@CheckForNull
+ public Collection<String> getOwaspMobileTop10For2024() {
+ return owaspMobileTop10For2024;
+ }
+
+ public RuleQuery setOwaspMobileTop10For2024(@Nullable Collection<String> owaspMobileTop10For2024) {
+ this.owaspMobileTop10For2024 = owaspMobileTop10For2024;
+ return this;
+ }
+
+ @CheckForNull
public Collection<String> getSansTop25() {
return sansTop25;
}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/security/SecurityStandards.java b/server/sonar-server-common/src/main/java/org/sonar/server/security/SecurityStandards.java
index eaead555007..9e4b5df7878 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/security/SecurityStandards.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/security/SecurityStandards.java
@@ -65,6 +65,7 @@ public final class SecurityStandards {
@Deprecated(since = "10.0", forRemoval = true)
public static final String SANS_TOP_25_POROUS_DEFENSES = "porous-defenses";
+ private static final String OWASP_MOBILE_TOP10_2024_PREFIX = "owaspMobileTop10-2024:";
private static final String OWASP_TOP10_PREFIX = "owaspTop10:";
private static final String OWASP_TOP10_2021_PREFIX = "owaspTop10-2021:";
private static final String PCI_DSS_32_PREFIX = V3_2.prefix() + ":";
@@ -104,14 +105,20 @@ public final class SecurityStandards {
public static final List<String> CWE_TOP25_2023 = List.of("787", "79", "89", "416", "78", "20", "125", "22", "352", "434", "862", "476", "287", "190", "502",
"77", "119", "798", "918", "306", "362", "269", "94", "863", "276");
+ // https://cwe.mitre.org/top25/archive/2024/2024_cwe_top25.html#tableView
+ public static final List<String> CWE_TOP25_2024 = List.of("79", "787", "89", "352", "22", "125", "78", "416", "862", "434", "94", "20", "77", "287", "269",
+ "502", "200", "863", "918", "119", "476", "798", "190", "400", "306");
+
public static final String CWE_YEAR_2021 = "2021";
public static final String CWE_YEAR_2022 = "2022";
public static final String CWE_YEAR_2023 = "2023";
+ public static final String CWE_YEAR_2024 = "2024";
public static final Map<String, List<String>> CWES_BY_CWE_TOP_25 = Map.of(
CWE_YEAR_2021, CWE_TOP25_2021,
CWE_YEAR_2022, CWE_TOP25_2022,
- CWE_YEAR_2023, CWE_TOP25_2023);
+ CWE_YEAR_2023, CWE_TOP25_2023,
+ CWE_YEAR_2024, CWE_TOP25_2024);
private static final List<String> OWASP_ASVS_40_LEVEL_1 = List.of("2.1.1", "2.1.10", "2.1.11", "2.1.12", "2.1.2", "2.1.3", "2.1.4", "2.1.5", "2.1.6", "2.1.7", "2.1.8", "2.1.9",
"2.10.1", "2.10.2", "2.10.3", "2.10.4", "2.2.1", "2.2.2", "2.2.3", "2.3.1", "2.5.1", "2.5.2", "2.5.3", "2.5.4", "2.5.5", "2.5.6", "2.7.1", "2.7.2", "2.7.3", "2.7.4", "2.8.1",
@@ -447,6 +454,10 @@ public final class SecurityStandards {
return getMatchingStandards(standards, OWASP_ASVS_40_PREFIX);
}
+ public Set<String> getOwaspMobileTop10For2024() {
+ return getMatchingStandards(standards, OWASP_MOBILE_TOP10_2024_PREFIX);
+ }
+
public Set<String> getOwaspTop10() {
return getMatchingStandards(standards, OWASP_TOP10_PREFIX);
}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/webhook/WebhookCustomDns.java b/server/sonar-server-common/src/main/java/org/sonar/server/webhook/WebhookCustomDns.java
index 19ba5f16ef7..9066ab3c76d 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/webhook/WebhookCustomDns.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/webhook/WebhookCustomDns.java
@@ -29,6 +29,7 @@ import org.jetbrains.annotations.NotNull;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.config.Configuration;
import org.sonar.api.server.ServerSide;
+import org.sonar.server.network.NetworkInterfaceProvider;
import static org.sonar.api.CoreProperties.SONAR_VALIDATE_WEBHOOKS_DEFAULT_VALUE;
import static org.sonar.api.CoreProperties.SONAR_VALIDATE_WEBHOOKS_PROPERTY;
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/webhook/WebhookModule.java b/server/sonar-server-common/src/main/java/org/sonar/server/webhook/WebhookModule.java
index 07db18db659..204df77936c 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/webhook/WebhookModule.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/webhook/WebhookModule.java
@@ -25,7 +25,6 @@ public class WebhookModule extends Module {
@Override
protected void configureModule() {
add(
- NetworkInterfaceProvider.class,
WebhookCustomDns.class,
WebhookCallerImpl.class,
WebhookDeliveryStorage.class,
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/TaintCheckerTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/TaintCheckerTest.java
index 8e5073080ad..3a2471084ec 100644
--- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/TaintCheckerTest.java
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/TaintCheckerTest.java
@@ -44,13 +44,16 @@ public class TaintCheckerTest {
public void test_getTaintIssuesOnly() {
List<IssueDto> taintIssues = underTest.getTaintIssuesOnly(getIssues());
- assertThat(taintIssues).hasSize(6);
+ assertThat(taintIssues).hasSize(9);
assertThat(taintIssues.get(0).getKey()).isEqualTo("taintIssue1");
assertThat(taintIssues.get(1).getKey()).isEqualTo("taintIssue2");
assertThat(taintIssues.get(2).getKey()).isEqualTo("taintIssue3");
assertThat(taintIssues.get(3).getKey()).isEqualTo("taintIssue4");
assertThat(taintIssues.get(4).getKey()).isEqualTo("taintIssue5");
assertThat(taintIssues.get(5).getKey()).isEqualTo("taintIssue6");
+ assertThat(taintIssues.get(6).getKey()).isEqualTo("taintIssue7");
+ assertThat(taintIssues.get(7).getKey()).isEqualTo("taintIssue8");
+ assertThat(taintIssues.get(8).getKey()).isEqualTo("taintIssue9");
}
@Test
@@ -69,7 +72,7 @@ public class TaintCheckerTest {
Map<Boolean, List<IssueDto>> issuesByTaintStatus = underTest.mapIssuesByTaintStatus(getIssues());
assertThat(issuesByTaintStatus.keySet()).hasSize(2);
- assertThat(issuesByTaintStatus.get(true)).hasSize(6);
+ assertThat(issuesByTaintStatus.get(true)).hasSize(9);
assertThat(issuesByTaintStatus.get(false)).hasSize(3);
assertThat(issuesByTaintStatus.get(true).get(0).getKey()).isEqualTo("taintIssue1");
@@ -78,6 +81,8 @@ public class TaintCheckerTest {
assertThat(issuesByTaintStatus.get(true).get(3).getKey()).isEqualTo("taintIssue4");
assertThat(issuesByTaintStatus.get(true).get(4).getKey()).isEqualTo("taintIssue5");
assertThat(issuesByTaintStatus.get(true).get(5).getKey()).isEqualTo("taintIssue6");
+ assertThat(issuesByTaintStatus.get(true).get(6).getKey()).isEqualTo("taintIssue7");
+ assertThat(issuesByTaintStatus.get(true).get(7).getKey()).isEqualTo("taintIssue8");
assertThat(issuesByTaintStatus.get(false).get(0).getKey()).isEqualTo("standardIssue1");
assertThat(issuesByTaintStatus.get(false).get(1).getKey()).isEqualTo("standardIssue2");
@@ -87,9 +92,9 @@ public class TaintCheckerTest {
@Test
public void test_getTaintRepositories() {
assertThat(underTest.getTaintRepositories())
- .hasSize(6)
- .containsExactlyInAnyOrder("roslyn.sonaranalyzer.security.cs", "javasecurity", "jssecurity",
- "tssecurity", "phpsecurity", "pythonsecurity");
+ .hasSize(9)
+ .containsExactlyInAnyOrder("gosecurity", "javasecurity", "jssecurity", "kotlinsecurity", "phpsecurity", "pythonsecurity",
+ "roslyn.sonaranalyzer.security.cs", "tssecurity", "vbnetsecurity");
}
@Test
@@ -98,9 +103,9 @@ public class TaintCheckerTest {
when(configuration.getStringArray(EXTRA_TAINT_REPOSITORIES)).thenReturn(new String[]{"extra-1", "extra-2"});
TaintChecker underTest = new TaintChecker(configuration);
assertThat(underTest.getTaintRepositories())
- .hasSize(8)
- .containsExactlyInAnyOrder("roslyn.sonaranalyzer.security.cs", "javasecurity", "jssecurity",
- "tssecurity", "phpsecurity", "pythonsecurity", "extra-1", "extra-2");
+ .hasSize(11)
+ .containsExactlyInAnyOrder("gosecurity", "javasecurity", "jssecurity", "kotlinsecurity", "phpsecurity", "pythonsecurity",
+ "roslyn.sonaranalyzer.security.cs", "tssecurity", "vbnetsecurity", "extra-1", "extra-2");
}
@Test
@@ -135,6 +140,9 @@ public class TaintCheckerTest {
issues.add(createIssueWithRepository("taintIssue4", "tssecurity"));
issues.add(createIssueWithRepository("taintIssue5", "phpsecurity"));
issues.add(createIssueWithRepository("taintIssue6", "pythonsecurity"));
+ issues.add(createIssueWithRepository("taintIssue7", "kotlinsecurity"));
+ issues.add(createIssueWithRepository("taintIssue8", "gosecurity"));
+ issues.add(createIssueWithRepository("taintIssue9", "vbnetsecurity"));
issues.add(createIssueWithRepository("standardIssue1", "java"));
issues.add(createIssueWithRepository("standardIssue2", "python"));
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/SecurityStandardCategoryStatisticsTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/SecurityStandardCategoryStatisticsTest.java
index 13c8dd28285..a23c45cd248 100644
--- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/SecurityStandardCategoryStatisticsTest.java
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/SecurityStandardCategoryStatisticsTest.java
@@ -20,6 +20,9 @@
package org.sonar.server.issue.index;
import java.util.ArrayList;
+import java.util.Map;
+import java.util.OptionalInt;
+
import org.junit.Test;
import static java.util.OptionalInt.empty;
@@ -31,7 +34,7 @@ public class SecurityStandardCategoryStatisticsTest {
public void hasMoreRules_default_false() {
SecurityStandardCategoryStatistics standardCategoryStatistics = new SecurityStandardCategoryStatistics(
"cat", 0, empty(), 0,
- 0, 5, null, null
+ 0, 5, null, null, Map.of()
);
assertThat(standardCategoryStatistics.hasMoreRules()).isFalse();
}
@@ -40,7 +43,7 @@ public class SecurityStandardCategoryStatisticsTest {
public void hasMoreRules_is_updatable() {
SecurityStandardCategoryStatistics standardCategoryStatistics = new SecurityStandardCategoryStatistics(
"cat", 0, empty(), 0,
- 0, 5, null, null
+ 0, 5, null, null, Map.of()
);
standardCategoryStatistics.setHasMoreRules(true);
assertThat(standardCategoryStatistics.hasMoreRules()).isTrue();
@@ -50,7 +53,7 @@ public class SecurityStandardCategoryStatisticsTest {
public void test_getters() {
SecurityStandardCategoryStatistics standardCategoryStatistics = new SecurityStandardCategoryStatistics(
"cat", 1, empty(), 0,
- 0, 5, new ArrayList<>(), "version"
+ 0, 5, new ArrayList<>(), "version", Map.of()
).setLevel("1");
standardCategoryStatistics.setActiveRules(3);
@@ -69,6 +72,47 @@ public class SecurityStandardCategoryStatisticsTest {
assertThat(standardCategoryStatistics.getVersion().get()).contains("version");
assertThat(standardCategoryStatistics.getLevel().get()).contains("1");
assertThat(standardCategoryStatistics.hasMoreRules()).isFalse();
+ assertThat(standardCategoryStatistics.getSeverityDistribution()).isEmpty();
+ }
+
+ @Test
+ public void withModifiedVulnerabilities() {
+ SecurityStandardCategoryStatistics standardCategoryStatistics = new SecurityStandardCategoryStatistics(
+ "cat", 1, empty(), 0,
+ 0, 5, null, null, Map.of()
+ );
+
+ SecurityStandardCategoryStatistics modified = standardCategoryStatistics.withModifiedVulnerabilities(2, 3);
+
+ assertThat(modified.getVulnerabilities()).isEqualTo(3);
+ assertThat(modified.getVulnerabilityRating()).isPresent();
+ assertThat(modified.getVulnerabilityRating().getAsInt()).isEqualTo(3);
+ }
+
+ @Test
+ public void withModifiedVulnerabilities_noNewRating() {
+ SecurityStandardCategoryStatistics standardCategoryStatistics = new SecurityStandardCategoryStatistics(
+ "cat", 1, OptionalInt.of(1), 0,
+ 0, 5, null, null, Map.of()
+ );
+
+ SecurityStandardCategoryStatistics modified = standardCategoryStatistics.withModifiedVulnerabilities(2, null);
+
+ assertThat(modified.getVulnerabilities()).isEqualTo(3);
+ assertThat(modified.getVulnerabilityRating()).isPresent();
+ assertThat(modified.getVulnerabilityRating().getAsInt()).isEqualTo(1);
}
+ @Test
+ public void withModifiedVulnerabilities_usesLowestRating() {
+ SecurityStandardCategoryStatistics standardCategoryStatistics = new SecurityStandardCategoryStatistics(
+ "cat", 1, OptionalInt.of(5), 0,
+ 0, 5, null, null, Map.of()
+ );
+
+ SecurityStandardCategoryStatistics modified = standardCategoryStatistics.withModifiedVulnerabilities(2, 3);
+
+ assertThat(modified.getVulnerabilityRating()).isPresent();
+ assertThat(modified.getVulnerabilityRating().getAsInt()).isEqualTo(5);
+ }
}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowForCodeQualityIssuesTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowForCodeQualityIssuesTest.java
index b49f7e03d1e..956ef000a0e 100644
--- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowForCodeQualityIssuesTest.java
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowForCodeQualityIssuesTest.java
@@ -19,10 +19,8 @@
*/
package org.sonar.server.issue.workflow;
-import com.google.common.collect.Collections2;
import java.util.Arrays;
import java.util.Calendar;
-import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Random;
@@ -44,7 +42,6 @@ import org.sonar.server.issue.workflow.codequalityissue.CodeQualityIssueWorkflow
import org.sonar.server.issue.workflow.codequalityissue.CodeQualityIssueWorkflowActionsFactory;
import org.sonar.server.issue.workflow.codequalityissue.CodeQualityIssueWorkflowDefinition;
import org.sonar.server.issue.workflow.codequalityissue.CodeQualityIssueWorkflowTransition;
-import org.sonar.server.issue.workflow.statemachine.Transition;
import static com.google.common.base.Preconditions.checkArgument;
import static org.apache.commons.lang3.time.DateUtils.addDays;
@@ -63,6 +60,7 @@ import static org.sonar.api.issue.Issue.STATUS_OPEN;
import static org.sonar.api.issue.Issue.STATUS_REOPENED;
import static org.sonar.api.issue.Issue.STATUS_RESOLVED;
import static org.sonar.core.issue.IssueChangeContext.issueChangeContextByScanBuilder;
+import static org.sonar.core.rule.RuleType.VULNERABILITY;
class IssueWorkflowForCodeQualityIssuesTest {
@@ -85,35 +83,35 @@ class IssueWorkflowForCodeQualityIssuesTest {
@Test
void list_out_transitions_from_status_open() {
DefaultIssue issue = new DefaultIssue().setStatus(STATUS_OPEN);
- List<Transition> transitions = underTest.outTransitions(issue);
- assertThat(keys(transitions)).containsOnly("confirm", "falsepositive", "resolve", "wontfix", "accept");
+ List<String> transitions = underTest.outTransitionsKeys(issue);
+ assertThat(transitions).containsOnly("confirm", "falsepositive", "resolve", "wontfix", "accept");
}
@Test
void list_out_transitions_from_status_confirmed() {
DefaultIssue issue = new DefaultIssue().setStatus(STATUS_CONFIRMED);
- List<Transition> transitions = underTest.outTransitions(issue);
- assertThat(keys(transitions)).containsOnly("unconfirm", "falsepositive", "resolve", "wontfix", "accept");
+ List<String> transitions = underTest.outTransitionsKeys(issue);
+ assertThat(transitions).containsOnly("unconfirm", "falsepositive", "resolve", "wontfix", "accept");
}
@Test
void list_out_transitions_from_status_resolved() {
DefaultIssue issue = new DefaultIssue().setStatus(STATUS_RESOLVED);
- List<Transition> transitions = underTest.outTransitions(issue);
- assertThat(keys(transitions)).containsOnly("reopen");
+ List<String> transitions = underTest.outTransitionsKeys(issue);
+ assertThat(transitions).containsOnly("reopen");
}
@Test
void list_out_transitions_from_status_reopen() {
DefaultIssue issue = new DefaultIssue().setStatus(STATUS_REOPENED);
- List<Transition> transitions = underTest.outTransitions(issue);
- assertThat(keys(transitions)).containsOnly("confirm", "resolve", "falsepositive", "wontfix", "accept");
+ List<String> transitions = underTest.outTransitionsKeys(issue);
+ assertThat(transitions).containsOnly("confirm", "resolve", "falsepositive", "wontfix", "accept");
}
@Test
void list_no_out_transition_from_status_closed() {
DefaultIssue issue = new DefaultIssue().setStatus(STATUS_CLOSED).setRuleKey(RuleKey.of("java", "R1 "));
- List<Transition> transitions = underTest.outTransitions(issue);
+ List<String> transitions = underTest.outTransitionsKeys(issue);
assertThat(transitions).isEmpty();
}
@@ -121,7 +119,7 @@ class IssueWorkflowForCodeQualityIssuesTest {
void fail_if_unknown_status_when_listing_transitions() {
DefaultIssue issue = new DefaultIssue().setStatus("xxx");
try {
- underTest.outTransitions(issue);
+ underTest.outTransitionsKeys(issue);
fail();
} catch (IllegalArgumentException e) {
assertThat(e).hasMessage("Unknown status: xxx");
@@ -458,6 +456,27 @@ class IssueWorkflowForCodeQualityIssuesTest {
assertThat(issue.assignee()).isNull();
}
+ @Test
+ void doManualTransition_shouldUseAutomaticReopenTransitionOnTaintVulnerability_whenMarkedAsResolvedButStillAlive() {
+ DefaultIssue issue = new DefaultIssue()
+ .setKey("issue_key")
+ .setRuleKey(RuleKey.of("xoo", "S001"))
+ .setStatus(STATUS_RESOLVED)
+ .setResolution(RESOLUTION_FIXED)
+ .setLocationsChanged(true)
+ .setNew(false)
+ .setType(VULNERABILITY)
+ .setBeingClosed(false);
+ when(taintChecker.isTaintVulnerability(issue))
+ .thenReturn(true);
+
+ underTest.doAutomaticTransition(issue, issueChangeContextByScanBuilder(new Date()).build());
+
+ assertThat(issue.issueStatus()).isEqualTo(IssueStatus.OPEN);
+ List<DefaultIssueComment> issueComments = issue.defaultIssueComments();
+ assertThat(issueComments).isEmpty();
+ }
+
private static DefaultIssue newClosedIssue(String resolution) {
return new DefaultIssue()
.setKey("ABCDE")
@@ -500,7 +519,4 @@ class IssueWorkflowForCodeQualityIssuesTest {
return newResolution == null ? "" : newResolution;
}
- private Collection<String> keys(List<Transition> transitions) {
- return Collections2.transform(transitions, Transition::key);
- }
}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowForSecurityHotspotsTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowForSecurityHotspotsTest.java
index 9c7b4b6346c..7f5e8691158 100644
--- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowForSecurityHotspotsTest.java
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowForSecurityHotspotsTest.java
@@ -42,7 +42,6 @@ import org.sonar.server.issue.workflow.securityhotspot.SecurityHotspotWorkflow;
import org.sonar.server.issue.workflow.securityhotspot.SecurityHotspotWorkflowActionsFactory;
import org.sonar.server.issue.workflow.securityhotspot.SecurityHotspotWorkflowDefinition;
import org.sonar.server.issue.workflow.securityhotspot.SecurityHotspotWorkflowTransition;
-import org.sonar.server.issue.workflow.statemachine.Transition;
import static org.apache.commons.lang3.RandomStringUtils.secure;
import static org.assertj.core.api.Assertions.assertThat;
@@ -77,9 +76,9 @@ public class IssueWorkflowForSecurityHotspotsTest {
public void to_review_hotspot_with_any_resolution_can_be_resolved_as_safe_or_fixed(@Nullable String resolution) {
DefaultIssue hotspot = newHotspot(STATUS_TO_REVIEW, resolution);
- List<Transition> transitions = underTest.outTransitions(hotspot);
+ List<String> transitions = underTest.outTransitionsKeys(hotspot);
- assertThat(keys(transitions)).containsExactlyInAnyOrder(RESOLVE_AS_REVIEWED, RESOLVE_AS_SAFE, RESOLVE_AS_ACKNOWLEDGED);
+ assertThat(fromKeys(transitions)).containsExactlyInAnyOrder(RESOLVE_AS_REVIEWED, RESOLVE_AS_SAFE, RESOLVE_AS_ACKNOWLEDGED);
}
@DataProvider
@@ -97,18 +96,18 @@ public class IssueWorkflowForSecurityHotspotsTest {
public void reviewed_as_fixed_hotspot_can_be_resolved_as_safe_or_put_back_to_review() {
DefaultIssue hotspot = newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED);
- List<Transition> transitions = underTest.outTransitions(hotspot);
+ List<String> transitions = underTest.outTransitionsKeys(hotspot);
- assertThat(keys(transitions)).containsExactlyInAnyOrder(RESOLVE_AS_SAFE, RESET_AS_TO_REVIEW, RESOLVE_AS_ACKNOWLEDGED);
+ assertThat(fromKeys(transitions)).containsExactlyInAnyOrder(RESOLVE_AS_SAFE, RESET_AS_TO_REVIEW, RESOLVE_AS_ACKNOWLEDGED);
}
@Test
public void reviewed_as_safe_hotspot_can_be_resolved_as_fixed_or_put_back_to_review() {
DefaultIssue hotspot = newHotspot(STATUS_REVIEWED, RESOLUTION_SAFE);
- List<Transition> transitions = underTest.outTransitions(hotspot);
+ List<String> transitions = underTest.outTransitionsKeys(hotspot);
- assertThat(keys(transitions)).containsExactlyInAnyOrder(RESOLVE_AS_REVIEWED, RESET_AS_TO_REVIEW, RESOLVE_AS_ACKNOWLEDGED);
+ assertThat(fromKeys(transitions)).containsExactlyInAnyOrder(RESOLVE_AS_REVIEWED, RESET_AS_TO_REVIEW, RESOLVE_AS_ACKNOWLEDGED);
}
@Test
@@ -116,7 +115,7 @@ public class IssueWorkflowForSecurityHotspotsTest {
public void reviewed_with_any_resolution_but_safe_or_fixed_can_not_be_changed(String resolution) {
DefaultIssue hotspot = newHotspot(STATUS_REVIEWED, resolution);
- List<Transition> transitions = underTest.outTransitions(hotspot);
+ List<String> transitions = underTest.outTransitionsKeys(hotspot);
assertThat(transitions).isEmpty();
}
@@ -283,8 +282,8 @@ public class IssueWorkflowForSecurityHotspotsTest {
assertThat(hotspot.resolution()).isNull();
}
- private Collection<SecurityHotspotWorkflowTransition> keys(List<Transition> transitions) {
- return transitions.stream().map(Transition::key).map(SecurityHotspotWorkflowTransition::fromKey).flatMap(Optional::stream).toList();
+ private Collection<SecurityHotspotWorkflowTransition> fromKeys(List<String> transitionKeys) {
+ return transitionKeys.stream().map(SecurityHotspotWorkflowTransition::fromKey).flatMap(Optional::stream).toList();
}
private static void setStatusPreviousToClosed(DefaultIssue hotspot, String previousStatus, @Nullable String previousResolution, @Nullable String newResolution) {
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/log/DistributedServerLoggingTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/log/DistributedServerLoggingTest.java
index bbb21ea871a..94a09ff19e4 100644
--- a/server/sonar-server-common/src/test/java/org/sonar/server/log/DistributedServerLoggingTest.java
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/log/DistributedServerLoggingTest.java
@@ -21,7 +21,6 @@ package org.sonar.server.log;
import com.hazelcast.cluster.Cluster;
import com.hazelcast.cluster.Member;
-import com.hazelcast.cp.IAtomicReference;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
@@ -45,6 +44,7 @@ import org.sonar.db.Database;
import org.sonar.process.ProcessId;
import org.sonar.process.ProcessProperties;
import org.sonar.process.cluster.hz.DistributedAnswer;
+import org.sonar.process.cluster.hz.DistributedReference;
import org.sonar.process.cluster.hz.HazelcastMember;
import static org.assertj.core.api.Assertions.assertThat;
@@ -95,7 +95,7 @@ public class DistributedServerLoggingTest {
settings.setProperty(PATH_LOGS.getKey(), dirWithLogs.getAbsolutePath());
settings.setProperty(ProcessProperties.Property.WEB_HOST.getKey(), "anyhost");
- IAtomicReference<Object> reference = mock();
+ DistributedReference<Object> reference = mock();
when(hazelcastMember.getAtomicReference("AUTH_SECRET")).thenReturn(reference);
underTest.start();
@@ -111,7 +111,7 @@ public class DistributedServerLoggingTest {
@Test
public void isValidNodeToNodeCall_whenNodeToNodeSecretIsInvalid() {
- IAtomicReference<Object> reference = mock();
+ DistributedReference<Object> reference = mock();
when(hazelcastMember.getAtomicReference("AUTH_SECRET")).thenReturn(reference);
when(reference.get()).thenReturn("");
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/webhook/NetworkInterfaceProviderTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/network/NetworkInterfaceProviderTest.java
index 2d19b484079..24f30332489 100644
--- a/server/sonar-server-common/src/test/java/org/sonar/server/webhook/NetworkInterfaceProviderTest.java
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/network/NetworkInterfaceProviderTest.java
@@ -17,7 +17,7 @@
* 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.server.webhook;
+package org.sonar.server.network;
import java.net.SocketException;
import java.util.List;
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/rule/index/RuleDocTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/rule/index/RuleDocTest.java
index 21eff8259fc..9676a0ef521 100644
--- a/server/sonar-server-common/src/test/java/org/sonar/server/rule/index/RuleDocTest.java
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/rule/index/RuleDocTest.java
@@ -56,6 +56,7 @@ public class RuleDocTest {
assertThat(ruleDoc.getCwe()).isEqualTo(securityStandards.getCwe());
assertThat(ruleDoc.getOwaspTop10()).isEqualTo(securityStandards.getOwaspTop10());
assertThat(ruleDoc.getOwaspTop10For2021()).isEqualTo(securityStandards.getOwaspTop10For2021());
+ assertThat(ruleDoc.getOwaspMobileTop10For2024()).isEqualTo(securityStandards.getOwaspMobileTop10For2024());
assertThat(ruleDoc.getSansTop25()).isEqualTo(securityStandards.getSansTop25());
assertThat(ruleDoc.getSonarSourceSecurityCategory()).isEqualTo(securityStandards.getSqCategory());
assertThat(ruleDoc.name()).isEqualTo(ruleForIndexingDto.getName());
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/security/SecurityStandardsTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/security/SecurityStandardsTest.java
index c02feb8bb7e..543ac842925 100644
--- a/server/sonar-server-common/src/test/java/org/sonar/server/security/SecurityStandardsTest.java
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/security/SecurityStandardsTest.java
@@ -63,6 +63,14 @@ class SecurityStandardsTest {
}
@Test
+ void fromSecurityStandards_from_empty_set_has_no_OwaspMobileTop10_standard() {
+ SecurityStandards securityStandards = fromSecurityStandards(emptySet());
+
+ assertThat(securityStandards.getStandards()).isEmpty();
+ assertThat(securityStandards.getOwaspMobileTop10For2024()).isEmpty();
+ }
+
+ @Test
void fromSecurityStandards_from_empty_set_has_no_OwaspTop10_standard() {
SecurityStandards securityStandards = fromSecurityStandards(emptySet());
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/webhook/WebhookCallerImplTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/webhook/WebhookCallerImplTest.java
index e406a361301..dfb87f7290d 100644
--- a/server/sonar-server-common/src/test/java/org/sonar/server/webhook/WebhookCallerImplTest.java
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/webhook/WebhookCallerImplTest.java
@@ -39,6 +39,7 @@ import org.sonar.api.impl.utils.TestSystem2;
import org.sonar.api.utils.System2;
import org.sonar.api.utils.Version;
import org.sonar.core.platform.SonarQubeVersion;
+import org.sonar.server.network.NetworkInterfaceProvider;
import org.sonar.server.util.OkHttpClientProvider;
import static org.apache.commons.lang3.RandomStringUtils.secure;
@@ -233,9 +234,8 @@ public class WebhookCallerImplTest {
@Test
public void silently_catch_error_when_url_is_localhost(){
- String url = server.url("/").toString();
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID,
- secure().nextAlphanumeric(40), "my-webhook", url, null);
+ secure().nextAlphanumeric(40), "my-webhook", "http://localhost", null);
WebhookDelivery delivery = newSender(true).call(webhook, PAYLOAD);
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/webhook/WebhookCustomDnsTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/webhook/WebhookCustomDnsTest.java
index 0c0cb261f3e..a164f024106 100644
--- a/server/sonar-server-common/src/test/java/org/sonar/server/webhook/WebhookCustomDnsTest.java
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/webhook/WebhookCustomDnsTest.java
@@ -32,6 +32,7 @@ import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.mockito.Mockito;
import org.sonar.api.config.Configuration;
+import org.sonar.server.network.NetworkInterfaceProvider;
import static org.mockito.Mockito.when;
import static org.sonar.api.CoreProperties.SONAR_VALIDATE_WEBHOOKS_PROPERTY;
@@ -39,10 +40,10 @@ import static org.sonar.api.CoreProperties.SONAR_VALIDATE_WEBHOOKS_PROPERTY;
public class WebhookCustomDnsTest {
private static final String INVALID_URL = "Invalid URL: loopback and wildcard addresses are not allowed for webhooks.";
- private Configuration configuration = Mockito.mock(Configuration.class);
- private NetworkInterfaceProvider networkInterfaceProvider = Mockito.mock(NetworkInterfaceProvider.class);
+ private final Configuration configuration = Mockito.mock(Configuration.class);
+ private final NetworkInterfaceProvider networkInterfaceProvider = Mockito.mock(NetworkInterfaceProvider.class);
- private WebhookCustomDns underTest = new WebhookCustomDns(configuration, networkInterfaceProvider);
+ private final WebhookCustomDns underTest = new WebhookCustomDns(configuration, networkInterfaceProvider);
@Test
public void lookup_fail_on_localhost() {
diff --git a/server/sonar-statemachine/build.gradle b/server/sonar-statemachine/build.gradle
new file mode 100644
index 00000000000..d15abf7c90d
--- /dev/null
+++ b/server/sonar-statemachine/build.gradle
@@ -0,0 +1,29 @@
+description = 'State machine used for issue workflow, can be later extracted and shared with other products'
+
+sonar {
+ properties {
+ property 'sonar.projectName', "${projectTitle} :: Server :: State Machine"
+ }
+}
+
+dependencies {
+ // Please don't add dependency on other SonarQube modules, as this library might be extracted in its own repo
+ implementation 'com.google.guava:guava'
+ implementation 'org.apache.commons:commons-lang3'
+
+ compileOnlyApi 'com.github.spotbugs:spotbugs-annotations'
+
+ testImplementation 'org.assertj:assertj-core'
+ testImplementation 'org.junit.jupiter:junit-jupiter-api'
+ testImplementation 'org.junit.jupiter:junit-jupiter-params'
+ testImplementation 'org.mockito:mockito-core'
+ testImplementation 'org.mockito:mockito-junit-jupiter'
+
+ testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
+ testRuntimeOnly 'org.junit.vintage:junit-vintage-engine'
+}
+
+test {
+ // Enabling the JUnit Platform (see https://github.com/junit-team/junit5-samples/tree/master/junit5-migration-gradle)
+ useJUnitPlatform()
+}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/statemachine/State.java b/server/sonar-statemachine/src/main/java/org/sonar/issue/workflow/statemachine/State.java
index 135bde69311..db02ed2a457 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/statemachine/State.java
+++ b/server/sonar-statemachine/src/main/java/org/sonar/issue/workflow/statemachine/State.java
@@ -17,7 +17,7 @@
* 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.server.issue.workflow.statemachine;
+package org.sonar.issue.workflow.statemachine;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/statemachine/StateMachine.java b/server/sonar-statemachine/src/main/java/org/sonar/issue/workflow/statemachine/StateMachine.java
index c719c64580b..ba7824d46dc 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/statemachine/StateMachine.java
+++ b/server/sonar-statemachine/src/main/java/org/sonar/issue/workflow/statemachine/StateMachine.java
@@ -17,7 +17,7 @@
* 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.server.issue.workflow.statemachine;
+package org.sonar.issue.workflow.statemachine;
import com.google.common.base.Preconditions;
import com.google.common.collect.ArrayListMultimap;
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/statemachine/Transition.java b/server/sonar-statemachine/src/main/java/org/sonar/issue/workflow/statemachine/Transition.java
index 36603adfee1..389983d1145 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/statemachine/Transition.java
+++ b/server/sonar-statemachine/src/main/java/org/sonar/issue/workflow/statemachine/Transition.java
@@ -17,16 +17,14 @@
* 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.server.issue.workflow.statemachine;
+package org.sonar.issue.workflow.statemachine;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
-import javax.annotation.CheckForNull;
import org.apache.commons.lang3.StringUtils;
-import org.sonar.db.permission.ProjectPermission;
import static com.google.common.base.Preconditions.checkArgument;
@@ -42,7 +40,6 @@ public class Transition<E, A> {
private final List<Predicate<E>> conditions;
private final List<Consumer<A>> actions;
private final boolean automatic;
- private final ProjectPermission requiredProjectPermission;
private Transition(TransitionBuilder<E, A> builder) {
key = builder.key;
@@ -51,7 +48,6 @@ public class Transition<E, A> {
conditions = List.copyOf(builder.conditions);
actions = List.copyOf(builder.actions);
automatic = builder.automatic;
- requiredProjectPermission = builder.requiredProjectPermission;
}
public String key() {
@@ -87,11 +83,6 @@ public class Transition<E, A> {
return true;
}
- @CheckForNull
- public ProjectPermission requiredProjectPermission() {
- return requiredProjectPermission;
- }
-
@Override
public String toString() {
return String.format("%s->%s->%s", from, key, to);
@@ -112,7 +103,6 @@ public class Transition<E, A> {
private final List<Predicate<E>> conditions = new ArrayList<>();
private final List<Consumer<A>> actions = new ArrayList<>();
private boolean automatic = false;
- private ProjectPermission requiredProjectPermission;
private TransitionBuilder(String key) {
this.key = key;
@@ -145,11 +135,6 @@ public class Transition<E, A> {
return this;
}
- public TransitionBuilder<E, A> requiredProjectPermission(ProjectPermission requiredProjectPermission) {
- this.requiredProjectPermission = requiredProjectPermission;
- return this;
- }
-
public Transition<E, A> build() {
checkArgument(StringUtils.isNotEmpty(key), "Transition key must be set");
checkArgument(StringUtils.isAllLowerCase(key), "Transition key must be lower-case");
diff --git a/server/sonar-statemachine/src/main/java/org/sonar/issue/workflow/statemachine/package-info.java b/server/sonar-statemachine/src/main/java/org/sonar/issue/workflow/statemachine/package-info.java
new file mode 100644
index 00000000000..8f09e812c72
--- /dev/null
+++ b/server/sonar-statemachine/src/main/java/org/sonar/issue/workflow/statemachine/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.issue.workflow.statemachine;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/statemachine/StateMachineTest.java b/server/sonar-statemachine/src/test/java/org/sonar/issue/workflow/statemachine/StateMachineTest.java
index e1a417b597b..d52b59d910e 100644
--- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/statemachine/StateMachineTest.java
+++ b/server/sonar-statemachine/src/test/java/org/sonar/issue/workflow/statemachine/StateMachineTest.java
@@ -17,7 +17,7 @@
* 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.server.issue.workflow.statemachine;
+package org.sonar.issue.workflow.statemachine;
import org.junit.jupiter.api.Test;
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/statemachine/StateTest.java b/server/sonar-statemachine/src/test/java/org/sonar/issue/workflow/statemachine/StateTest.java
index a9600a7ce16..7feaa50f1f7 100644
--- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/statemachine/StateTest.java
+++ b/server/sonar-statemachine/src/test/java/org/sonar/issue/workflow/statemachine/StateTest.java
@@ -17,7 +17,7 @@
* 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.server.issue.workflow.statemachine;
+package org.sonar.issue.workflow.statemachine;
import java.util.List;
import org.junit.jupiter.api.Test;
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/statemachine/TransitionTest.java b/server/sonar-statemachine/src/test/java/org/sonar/issue/workflow/statemachine/TransitionTest.java
index 9219688961e..6654c2f9c18 100644
--- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/statemachine/TransitionTest.java
+++ b/server/sonar-statemachine/src/test/java/org/sonar/issue/workflow/statemachine/TransitionTest.java
@@ -17,11 +17,10 @@
* 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.server.issue.workflow.statemachine;
+package org.sonar.issue.workflow.statemachine;
import java.util.function.Predicate;
import org.junit.jupiter.api.Test;
-import org.sonar.db.permission.ProjectPermission;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -50,7 +49,6 @@ class TransitionTest {
.from("OPEN").to("CLOSED")
.conditions(condition1, condition2)
.actions(WfEntityActions::action1, WfEntityActions::action2)
- .requiredProjectPermission(ProjectPermission.ISSUE_ADMIN)
.build();
assertThat(transition.key()).isEqualTo("close");
assertThat(transition.from()).isEqualTo("OPEN");
@@ -58,7 +56,6 @@ class TransitionTest {
assertThat(transition.conditions()).containsOnly(condition1, condition2);
assertThat(transition.actions()).hasSize(2);
assertThat(transition.automatic()).isFalse();
- assertThat(transition.requiredProjectPermission()).isEqualTo(ProjectPermission.ISSUE_ADMIN);
}
@Test
@@ -71,7 +68,6 @@ class TransitionTest {
assertThat(transition.to()).isEqualTo("CLOSED");
assertThat(transition.conditions()).isEmpty();
assertThat(transition.actions()).isEmpty();
- assertThat(transition.requiredProjectPermission()).isNull();
}
@Test
diff --git a/server/sonar-telemetry/src/main/java/org/sonar/telemetry/metrics/TelemetryMetricsMapper.java b/server/sonar-telemetry/src/main/java/org/sonar/telemetry/metrics/TelemetryMetricsMapper.java
index 6630ab35a9c..c5dfe9fbcad 100644
--- a/server/sonar-telemetry/src/main/java/org/sonar/telemetry/metrics/TelemetryMetricsMapper.java
+++ b/server/sonar-telemetry/src/main/java/org/sonar/telemetry/metrics/TelemetryMetricsMapper.java
@@ -20,7 +20,6 @@
package org.sonar.telemetry.metrics;
import java.util.Collections;
-import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.sonar.telemetry.core.Granularity;
@@ -40,43 +39,57 @@ public class TelemetryMetricsMapper {
switch (provider.getDimension()) {
case INSTALLATION -> {
return mapInstallationMetric(provider);
- } case PROJECT -> {
+ }
+ case PROJECT -> {
return mapProjectMetric(provider);
- } case USER -> {
+ }
+ case USER -> {
return mapUserMetric(provider);
- } case LANGUAGE -> {
+ }
+ case LANGUAGE -> {
return mapLanguageMetric(provider);
- } default -> throw new IllegalArgumentException("Dimension: " + provider.getDimension() + " not yet implemented.");
+ }
+ default -> throw new IllegalArgumentException("Dimension: " + provider.getDimension() + " not yet implemented.");
}
}
private static Set<Metric> mapInstallationMetric(TelemetryDataProvider<?> provider) {
- Optional<?> optionalValue = provider.getValue();
-
- Granularity granularity = provider.getGranularity();
-
- if (granularity == Granularity.ADHOC && !provider.getValues().isEmpty()) {
- return provider.getValues().entrySet().stream()
+ // Case 1: the provider has implemented getValues() and it is non‐empty
+ var multiValues = provider.getValues();
+ if (!multiValues.isEmpty()) {
+ return multiValues.entrySet().stream()
.map(entry -> new InstallationMetric(
provider.getMetricKey() + "." + entry.getKey(),
entry.getValue(),
provider.getType(),
- granularity
- )).collect(Collectors.toSet());
+ provider.getGranularity()))
+ .collect(Collectors.toSet());
}
- if (granularity == Granularity.ADHOC && optionalValue.isEmpty()) {
- return Collections.emptySet();
+ // Case 2: the provider has implemented getValue() and it is non‐empty
+ var singleValue = provider.getValue();
+ if (singleValue.isPresent()) {
+ return Collections.singleton(new InstallationMetric(
+ provider.getMetricKey(),
+ singleValue.orElse(null),
+ provider.getType(),
+ provider.getGranularity()));
}
- return Collections.singleton(new InstallationMetric(
- provider.getMetricKey(),
- optionalValue.orElse(null),
- provider.getType(),
- granularity
- ));
+ // Case 3: the provider has not implemented getValue() or getValues(), or both are empty
+ if (provider.getGranularity() == Granularity.ADHOC) {
+ return Collections.emptySet();
+ } else {
+ // It's not clear whether we actually want to send a null metric in this case, but we do for now to be consistent with the previous implementation.
+ return Collections.singleton(new InstallationMetric(
+ provider.getMetricKey(),
+ null,
+ provider.getType(),
+ provider.getGranularity()));
+ }
}
+ // Note: Dimension.USER does not currently support getValue(). But we just silently ignore it if a provider tries to use it.
private static Set<Metric> mapUserMetric(TelemetryDataProvider<?> provider) {
return provider.getValues().entrySet().stream()
.map(entry -> new UserMetric(
@@ -84,10 +97,11 @@ public class TelemetryMetricsMapper {
entry.getValue(),
entry.getKey(),
provider.getType(),
- provider.getGranularity()
- )).collect(Collectors.toSet());
+ provider.getGranularity()))
+ .collect(Collectors.toSet());
}
+ // Note: Dimension.PROJECT does not currently support getValue(). But we just silently ignore it if a provider tries to use it.
private static Set<Metric> mapProjectMetric(TelemetryDataProvider<?> provider) {
return provider.getValues().entrySet().stream()
.map(entry -> new ProjectMetric(
@@ -95,10 +109,11 @@ public class TelemetryMetricsMapper {
entry.getValue(),
entry.getKey(),
provider.getType(),
- provider.getGranularity()
- )).collect(Collectors.toSet());
+ provider.getGranularity()))
+ .collect(Collectors.toSet());
}
+ // Note: Dimension.LANGUAGE does not currently support getValue(). But we just silently ignore it if a provider tries to use it.
private static Set<Metric> mapLanguageMetric(TelemetryDataProvider<?> provider) {
return provider.getValues().entrySet().stream()
.map(entry -> new LanguageMetric(
@@ -106,8 +121,7 @@ public class TelemetryMetricsMapper {
entry.getValue(),
entry.getKey(),
provider.getType(),
- provider.getGranularity()
- )).collect(Collectors.toSet());
+ provider.getGranularity()))
+ .collect(Collectors.toSet());
}
-
}
diff --git a/server/sonar-telemetry/src/test/java/org/sonar/telemetry/metrics/TelemetryMetricsMapperTest.java b/server/sonar-telemetry/src/test/java/org/sonar/telemetry/metrics/TelemetryMetricsMapperTest.java
index 69870bf024f..193566b5f68 100644
--- a/server/sonar-telemetry/src/test/java/org/sonar/telemetry/metrics/TelemetryMetricsMapperTest.java
+++ b/server/sonar-telemetry/src/test/java/org/sonar/telemetry/metrics/TelemetryMetricsMapperTest.java
@@ -21,6 +21,8 @@ package org.sonar.telemetry.metrics;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import org.assertj.core.groups.Tuple;
import org.junit.jupiter.api.Test;
@@ -40,8 +42,14 @@ import static org.assertj.core.api.Assertions.tuple;
class TelemetryMetricsMapperTest {
@Test
- void mapFromDataProvider_whenInstallationProvider() {
- TelemetryDataProvider<String> provider = new TestTelemetryBean(Dimension.INSTALLATION);
+ void mapFromDataProvider_withInstallationProviderSingleValue_returnsSingleValue() {
+ // Override multi-value method to return empty. Keep the single-value method defined in TestTelemetryBean.
+ TelemetryDataProvider<String> provider = new TestTelemetryBean(Dimension.INSTALLATION) {
+ @Override
+ public Map<String, String> getValues() {
+ return Map.of();
+ }
+ };
Set<Metric> metrics = TelemetryMetricsMapper.mapFromDataProvider(provider);
List<InstallationMetric> userMetrics = retrieveList(metrics);
@@ -49,17 +57,70 @@ class TelemetryMetricsMapperTest {
assertThat(userMetrics)
.extracting(InstallationMetric::getKey, InstallationMetric::getType, InstallationMetric::getValue, InstallationMetric::getGranularity)
.containsExactlyInAnyOrder(
- tuple("telemetry-bean-a", TelemetryDataType.STRING, "value", Granularity.DAILY)
- );
+ tuple("telemetry-bean-a", TelemetryDataType.STRING, "value", Granularity.DAILY));
}
@Test
- void mapFromDataProvider_whenInstallationProviderWithMultiValue() {
+ void mapFromDataProvider_withInstallationProviderMultiValues_returnsMultipleValues() {
+ // Override single-value method to return empty. Keep the multi-value method defined in TestTelemetryBean.
+ TelemetryDataProvider<String> provider = new TestTelemetryBean(Dimension.INSTALLATION) {
+ @Override
+ public Optional<String> getValue() {
+ return Optional.empty();
+ }
+ };
+
+ Set<Metric> metrics = TelemetryMetricsMapper.mapFromDataProvider(provider);
+ List<InstallationMetric> userMetrics = retrieveList(metrics);
+
+ assertThat(userMetrics)
+ .extracting(InstallationMetric::getKey, InstallationMetric::getType, InstallationMetric::getValue, InstallationMetric::getGranularity)
+ .containsExactlyInAnyOrder(
+ tuple("telemetry-bean-a.key-1", TelemetryDataType.STRING, "value-1", Granularity.DAILY),
+ tuple("telemetry-bean-a.key-2", TelemetryDataType.STRING, "value-2", Granularity.DAILY));
+ }
+
+ @Test
+ void mapFromDataProvider_withInstallationProviderAdhocNoValues_returnEmptySet() {
+ // Override single-value and multi-value methods to return empty.
TelemetryDataProvider<String> provider = new TestTelemetryBean(Dimension.INSTALLATION) {
@Override
public Granularity getGranularity() {
return Granularity.ADHOC;
}
+
+ @Override
+ public Optional<String> getValue() {
+ return Optional.empty();
+ }
+
+ @Override
+ public Map<String, String> getValues() {
+ return Map.of();
+ }
+ };
+
+ Set<Metric> metrics = TelemetryMetricsMapper.mapFromDataProvider(provider);
+ List<InstallationMetric> userMetrics = retrieveList(metrics);
+
+ assertThat(userMetrics)
+ .extracting(InstallationMetric::getKey, InstallationMetric::getType, InstallationMetric::getValue, InstallationMetric::getGranularity)
+ .isEmpty();
+ }
+
+ @Test
+ void mapFromDataProvider_withInstallationProviderDailyNoValues_returnTelemetryWithNullValue() {
+ // Override single-value and multi-value methods to return empty.
+ TelemetryDataProvider<String> provider = new TestTelemetryBean(Dimension.INSTALLATION) {
+ @Override
+ public Optional<String> getValue() {
+ return Optional.empty();
+ }
+
+ @Override
+ public Map<String, String> getValues() {
+ return Map.of();
+ }
};
Set<Metric> metrics = TelemetryMetricsMapper.mapFromDataProvider(provider);
@@ -68,9 +129,7 @@ class TelemetryMetricsMapperTest {
assertThat(userMetrics)
.extracting(InstallationMetric::getKey, InstallationMetric::getType, InstallationMetric::getValue, InstallationMetric::getGranularity)
.containsExactlyInAnyOrder(
- tuple("telemetry-bean-a.key-1", TelemetryDataType.STRING, "value-1", Granularity.ADHOC),
- tuple("telemetry-bean-a.key-2", TelemetryDataType.STRING, "value-2", Granularity.ADHOC)
- );
+ tuple("telemetry-bean-a", TelemetryDataType.STRING, null, Granularity.DAILY));
}
@Test
@@ -83,8 +142,7 @@ class TelemetryMetricsMapperTest {
assertThat(list)
.extracting(UserMetric::getKey, UserMetric::getType, UserMetric::getUserUuid, UserMetric::getValue, UserMetric::getGranularity)
.containsExactlyInAnyOrder(
- expected()
- );
+ expected());
}
@Test
@@ -97,8 +155,7 @@ class TelemetryMetricsMapperTest {
assertThat(list)
.extracting(LanguageMetric::getKey, LanguageMetric::getType, LanguageMetric::getLanguage, LanguageMetric::getValue, LanguageMetric::getGranularity)
.containsExactlyInAnyOrder(
- expected()
- );
+ expected());
}
@Test
@@ -111,8 +168,7 @@ class TelemetryMetricsMapperTest {
assertThat(list)
.extracting(ProjectMetric::getKey, ProjectMetric::getType, ProjectMetric::getProjectUuid, ProjectMetric::getValue, ProjectMetric::getGranularity)
.containsExactlyInAnyOrder(
- expected()
- );
+ expected());
}
@Test
@@ -135,16 +191,14 @@ class TelemetryMetricsMapperTest {
assertThat(userMetrics)
.extracting(InstallationMetric::getKey, InstallationMetric::getType, InstallationMetric::getValue, InstallationMetric::getGranularity)
.containsExactlyInAnyOrder(
- tuple("telemetry-adhoc-bean", TelemetryDataType.BOOLEAN, true, Granularity.ADHOC)
- );
+ tuple("telemetry-adhoc-bean", TelemetryDataType.BOOLEAN, true, Granularity.ADHOC));
}
private static Tuple[] expected() {
- return new Tuple[]
- {
- tuple("telemetry-bean-a", TelemetryDataType.STRING, "key-1", "value-1", Granularity.DAILY),
- tuple("telemetry-bean-a", TelemetryDataType.STRING, "key-2", "value-2", Granularity.DAILY)
- };
+ return new Tuple[] {
+ tuple("telemetry-bean-a", TelemetryDataType.STRING, "key-1", "value-1", Granularity.DAILY),
+ tuple("telemetry-bean-a", TelemetryDataType.STRING, "key-2", "value-2", Granularity.DAILY)
+ };
}
private static <T extends Metric> List<T> retrieveList(Set<Metric> metrics) {
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/TooManyRequestsException.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/TooManyRequestsException.java
new file mode 100644
index 00000000000..c54e898ff16
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/TooManyRequestsException.java
@@ -0,0 +1,27 @@
+/*
+ * 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.server.exceptions;
+
+public class TooManyRequestsException extends RuntimeException {
+
+ public TooManyRequestsException(String message) {
+ super(message);
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java
index ad6f305c361..89a712b5a53 100644
--- a/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java
@@ -32,4 +32,11 @@ public interface QGChangeEventListeners {
*/
void broadcastOnIssueChange(List<DefaultIssue> changedIssues, Collection<QGChangeEvent> qgChangeEvents, boolean fromAlm);
+ /**
+ * Broadcast events regardless of any changed file analysis issues. Used when non-file analysis tools (ex: SCA)
+ * need to send events.
+ *
+ * @param fromAlm: true if issues changes were initiated by an ALM.
+ */
+ void broadcastOnAnyChange(Collection<QGChangeEvent> qgChangeEvents, boolean fromAlm);
}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java
index 65eb2433cbd..2068279f93f 100644
--- a/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java
@@ -62,6 +62,21 @@ public class QGChangeEventListenersImpl implements QGChangeEventListeners {
}
}
+ @Override
+ public void broadcastOnAnyChange(Collection<QGChangeEvent> changeEvents, boolean fromAlm) {
+ if (listeners.isEmpty() || changeEvents.isEmpty()) {
+ return;
+ }
+
+ try {
+ for (var changeEvent : changeEvents) {
+ listeners.forEach(listener -> broadcastChangeEventToListener(Set.of(), changeEvent, listener));
+ }
+ } catch (Error e) {
+ LOG.warn(format("Broadcasting to listeners failed for %s events", changeEvents.size()), e);
+ }
+ }
+
private void broadcastChangeEventsToBranches(List<DefaultIssue> issues, Collection<QGChangeEvent> changeEvents, boolean fromAlm) {
Multimap<String, QGChangeEvent> eventsByBranchUuid = changeEvents.stream()
.collect(MoreCollectors.index(qgChangeEvent -> qgChangeEvent.getBranch().getUuid()));
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java
index 4303a88bfe9..9a056ad8fdb 100644
--- a/server/sonar-webserver-api/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java
@@ -238,6 +238,34 @@ public class QGChangeEventListenersImplTest {
}
@Test
+ public void broadcastOnAnyChange_whenNoListeners_thenDoesNothing() {
+ var qgChangeEventListeners = new QGChangeEventListenersImpl(Set.of());
+
+ qgChangeEventListeners.broadcastOnAnyChange(singletonList(component1QGChangeEvent), false);
+
+ verifyNoInteractions(listener1, listener2, listener3);
+ }
+
+ @Test
+ public void broadcastOnAnyChange_whenNoChangeEvents_thenDoesNothing() {
+ underTest.broadcastOnAnyChange(List.of(), false);
+
+ verifyNoInteractions(listener1, listener2, listener3);
+ }
+
+ @Test
+ public void broadcastOnAnyChange_whenListenersAndChangeEvents_thenBroadcastsToEachListener() {
+ underTest.broadcastOnAnyChange(singletonList(component1QGChangeEvent), false);
+
+ ArgumentCaptor<Set<ChangedIssue>> changedIssuesCaptor = newSetCaptor();
+ inOrder.verify(listener1).onIssueChanges(same(component1QGChangeEvent), changedIssuesCaptor.capture());
+ Set<ChangedIssue> changedIssues = changedIssuesCaptor.getValue();
+ inOrder.verify(listener2).onIssueChanges(same(component1QGChangeEvent), same(changedIssues));
+ inOrder.verify(listener3).onIssueChanges(same(component1QGChangeEvent), same(changedIssues));
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
public void isNotClosed_returns_true_if_issue_in_one_of_opened_states() {
DefaultIssue defaultIssue = new DefaultIssue();
defaultIssue.setStatus(Issue.STATUS_REOPENED);
diff --git a/server/sonar-webserver-auth/src/it/java/org/sonar/server/authentication/JwtHttpHandlerIT.java b/server/sonar-webserver-auth/src/it/java/org/sonar/server/authentication/JwtHttpHandlerIT.java
index d280a8f2384..729cafe8422 100644
--- a/server/sonar-webserver-auth/src/it/java/org/sonar/server/authentication/JwtHttpHandlerIT.java
+++ b/server/sonar-webserver-auth/src/it/java/org/sonar/server/authentication/JwtHttpHandlerIT.java
@@ -23,14 +23,18 @@ import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ClaimsBuilder;
import io.jsonwebtoken.impl.DefaultClaimsBuilder;
import jakarta.servlet.http.HttpSession;
+import java.time.Duration;
import java.util.Date;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nullable;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor;
+import org.sonar.api.config.Configuration;
import org.sonar.api.config.internal.MapSettings;
import org.sonar.api.server.http.Cookie;
import org.sonar.api.server.http.HttpRequest;
@@ -44,8 +48,9 @@ import org.sonar.db.user.UserDto;
import org.sonar.server.http.JakartaHttpRequest;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.entry;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatNoException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
@@ -60,7 +65,7 @@ import static org.mockito.Mockito.when;
import static org.sonar.db.user.UserTesting.newUserDto;
import static org.sonar.server.authentication.Cookies.SET_COOKIE;
-public class JwtHttpHandlerIT {
+class JwtHttpHandlerIT {
private static final String JWT_TOKEN = "TOKEN";
private static final String CSRF_STATE = "CSRF_STATE";
@@ -72,7 +77,7 @@ public class JwtHttpHandlerIT {
private static final long IN_FIVE_MINUTES = NOW + 5 * 60 * 1000L;
- @Rule
+ @RegisterExtension
public DbTester db = DbTester.create();
private final DbClient dbClient = db.getDbClient();
@@ -86,18 +91,21 @@ public class JwtHttpHandlerIT {
private final MapSettings settings = new MapSettings();
private final JwtSerializer jwtSerializer = mock(JwtSerializer.class);
private final JwtCsrfVerifier jwtCsrfVerifier = mock(JwtCsrfVerifier.class);
+ private final ActiveTimeoutProvider activeTimeoutProvider = mock(ActiveTimeoutProvider.class);
- private JwtHttpHandler underTest = new JwtHttpHandler(system2, dbClient, settings.asConfig(), jwtSerializer, jwtCsrfVerifier);
+ private JwtHttpHandler underTest;
- @Before
- public void setUp() {
+ @BeforeEach
+ void setUp() {
when(system2.now()).thenReturn(NOW);
when(jwtSerializer.encode(any(JwtSerializer.JwtSession.class))).thenReturn(JWT_TOKEN);
when(jwtCsrfVerifier.generateState(eq(request), eq(response), anyInt())).thenReturn(CSRF_STATE);
+ when(activeTimeoutProvider.getActiveSessionTimeout()).thenReturn(Duration.ofDays(90));
+ underTest = new JwtHttpHandler(system2, dbClient, settings.asConfig(), jwtSerializer, jwtCsrfVerifier, activeTimeoutProvider);
}
@Test
- public void create_token() {
+ void create_token() {
UserDto user = db.users().insertUser();
underTest.generateToken(user, request, response);
@@ -109,7 +117,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void generate_csrf_state_when_creating_token() {
+ void generate_csrf_state_when_creating_token() {
UserDto user = db.users().insertUser();
underTest.generateToken(user, request, response);
@@ -121,12 +129,13 @@ public class JwtHttpHandlerIT {
}
@Test
- public void generate_token_is_using_session_timeout_from_settings() {
+ void generate_token_is_using_inactive_session_timeout_from_settings() {
UserDto user = db.users().insertUser();
int sessionTimeoutInMinutes = 10;
+ Configuration config = settings.asConfig();
settings.setProperty("sonar.web.sessionTimeoutInMinutes", sessionTimeoutInMinutes);
- underTest = new JwtHttpHandler(system2, dbClient, settings.asConfig(), jwtSerializer, jwtCsrfVerifier);
+ underTest = new JwtHttpHandler(system2, dbClient, config, jwtSerializer, jwtCsrfVerifier, activeTimeoutProvider);
underTest.generateToken(user, request, response);
verify(jwtSerializer).encode(jwtArgumentCaptor.capture());
@@ -134,12 +143,12 @@ public class JwtHttpHandlerIT {
}
@Test
- public void session_timeout_property_cannot_be_updated() {
+ void inactive_session_timeout_property_cannot_be_updated() {
UserDto user = db.users().insertUser();
int firstSessionTimeoutInMinutes = 10;
settings.setProperty("sonar.web.sessionTimeoutInMinutes", firstSessionTimeoutInMinutes);
- underTest = new JwtHttpHandler(system2, dbClient, settings.asConfig(), jwtSerializer, jwtCsrfVerifier);
+ underTest = new JwtHttpHandler(system2, dbClient, settings.asConfig(), jwtSerializer, jwtCsrfVerifier, activeTimeoutProvider);
underTest.generateToken(user, request, response);
// The property is updated, but it won't be taking into account
@@ -150,44 +159,27 @@ public class JwtHttpHandlerIT {
verifyToken(jwtArgumentCaptor.getAllValues().get(1), user, firstSessionTimeoutInMinutes * 60, NOW);
}
- @Test
- public void session_timeout_property_cannot_be_zero() {
- settings.setProperty("sonar.web.sessionTimeoutInMinutes", 0);
-
- assertThatThrownBy(() -> new JwtHttpHandler(system2, dbClient, settings.asConfig(), jwtSerializer, jwtCsrfVerifier))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Property sonar.web.sessionTimeoutInMinutes must be higher than 5 minutes and must not be greater than 3 months. Got 0 minutes");
- }
-
- @Test
- public void session_timeout_property_cannot_be_negative() {
- settings.setProperty("sonar.web.sessionTimeoutInMinutes", -10);
-
- assertThatThrownBy(() -> new JwtHttpHandler(system2, dbClient, settings.asConfig(), jwtSerializer, jwtCsrfVerifier))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Property sonar.web.sessionTimeoutInMinutes must be higher than 5 minutes and must not be greater than 3 months. Got -10 minutes");
- }
-
- @Test
- public void session_timeout_property_cannot_be_set_to_five_minutes() {
- settings.setProperty("sonar.web.sessionTimeoutInMinutes", 5);
+ @ParameterizedTest
+ @ValueSource(ints = {-10, 0, 4 * 30 * 24 * 60})
+ void inactive_session_timeout_must_be_valid(int minutes) {
+ settings.setProperty("sonar.web.sessionTimeoutInMinutes", minutes);
+ Configuration config = settings.asConfig();
- assertThatThrownBy(() -> new JwtHttpHandler(system2, dbClient, settings.asConfig(), jwtSerializer, jwtCsrfVerifier))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Property sonar.web.sessionTimeoutInMinutes must be higher than 5 minutes and must not be greater than 3 months. Got 5 minutes");
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> new JwtHttpHandler(system2, dbClient, config, jwtSerializer, jwtCsrfVerifier, activeTimeoutProvider))
+ .withMessage("Property sonar.web.sessionTimeoutInMinutes must be at least 6 minutes and must not be greater than 90 days (129 600 minutes). Got " + minutes + " minutes");
}
@Test
- public void session_timeout_property_cannot_be_greater_than_three_months() {
- settings.setProperty("sonar.web.sessionTimeoutInMinutes", 4 * 30 * 24 * 60);
+ void inactive_session_timeout_property_can_be_6_minute() {
+ settings.setProperty("sonar.web.sessionTimeoutInMinutes", 6);
+ Configuration config = settings.asConfig();
- assertThatThrownBy(() -> new JwtHttpHandler(system2, dbClient, settings.asConfig(), jwtSerializer, jwtCsrfVerifier))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Property sonar.web.sessionTimeoutInMinutes must be higher than 5 minutes and must not be greater than 3 months. Got 172800 minutes");
+ assertThatNoException().isThrownBy(() -> new JwtHttpHandler(system2, dbClient, config, jwtSerializer, jwtCsrfVerifier, activeTimeoutProvider));
}
@Test
- public void validate_token() {
+ void validate_token() {
UserDto user = db.users().insertUser();
addJwtCookie();
SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES));
@@ -200,7 +192,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void validate_token_refresh_session_when_refresh_time_is_reached() {
+ void validate_token_refresh_session_when_refresh_time_is_reached() {
UserDto user = db.users().insertUser();
addJwtCookie();
// Token was created 10 days ago and refreshed 6 minutes ago
@@ -219,7 +211,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void validate_token_does_not_refresh_session_when_refresh_time_is_not_reached() {
+ void validate_token_does_not_refresh_session_when_refresh_time_is_not_reached() {
UserDto user = db.users().insertUser();
addJwtCookie();
// Token was created 10 days ago and refreshed 4 minutes ago
@@ -235,7 +227,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void validate_token_does_not_refresh_session_when_disconnected_timeout_is_reached() {
+ void validate_token_does_not_refresh_session_when_disconnected_timeout_is_reached() {
UserDto user = db.users().insertUser();
addJwtCookie();
// Token was created 4 months ago, refreshed 4 minutes ago, and it expired in 5 minutes
@@ -249,7 +241,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void validate_token_does_not_refresh_session_when_user_is_disabled() {
+ void validate_token_does_not_refresh_session_when_user_is_disabled() {
addJwtCookie();
UserDto user = addUser(false);
SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES));
@@ -260,7 +252,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void validate_token_does_not_refresh_session_when_token_is_no_more_valid() {
+ void validate_token_does_not_refresh_session_when_token_is_no_more_valid() {
addJwtCookie();
when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.empty());
@@ -269,7 +261,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void validate_token_does_nothing_when_no_jwt_cookie() {
+ void validate_token_does_nothing_when_no_jwt_cookie() {
underTest.validateToken(request, response);
verifyNoInteractions(httpSession, jwtSerializer);
@@ -277,7 +269,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void validate_token_does_nothing_when_empty_value_in_jwt_cookie() {
+ void validate_token_does_nothing_when_empty_value_in_jwt_cookie() {
when(request.getCookies()).thenReturn(new Cookie[] {new JakartaHttpRequest.JakartaCookie(new jakarta.servlet.http.Cookie("JWT-SESSION", ""))});
underTest.validateToken(request, response);
@@ -287,7 +279,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void validate_token_verify_csrf_state() {
+ void validate_token_verify_csrf_state() {
UserDto user = db.users().insertUser();
addJwtCookie();
SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES));
@@ -302,7 +294,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void validate_token_does_nothing_when_no_session_token_in_db() {
+ void validate_token_does_nothing_when_no_session_token_in_db() {
UserDto user = db.users().insertUser();
addJwtCookie();
// No SessionToken in DB
@@ -317,7 +309,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void validate_token_does_nothing_when_expiration_date_from_session_token_is_expired() {
+ void validate_token_does_nothing_when_expiration_date_from_session_token_is_expired() {
UserDto user = db.users().insertUser();
addJwtCookie();
// In SessionToken, the expiration date is expired...
@@ -334,7 +326,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void validate_token_refresh_state_when_refreshing_token() {
+ void validate_token_refresh_state_when_refreshing_token() {
UserDto user = db.users().insertUser();
addJwtCookie();
// Token was created 10 days ago and refreshed 6 minutes ago
@@ -351,7 +343,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void remove_token() {
+ void remove_token() {
addJwtCookie();
UserDto user = db.users().insertUser();
SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES));
@@ -368,7 +360,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void does_not_remove_token_from_db_when_no_jwt_token_in_cookie() {
+ void does_not_remove_token_from_db_when_no_jwt_token_in_cookie() {
addJwtCookie();
UserDto user = db.users().insertUser();
SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES));
@@ -382,7 +374,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void does_not_remove_token_from_db_when_no_cookie() {
+ void does_not_remove_token_from_db_when_no_cookie() {
UserDto user = db.users().insertUser();
SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES));
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/ActiveTimeoutProvider.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/ActiveTimeoutProvider.java
new file mode 100644
index 00000000000..cf408de49cd
--- /dev/null
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/ActiveTimeoutProvider.java
@@ -0,0 +1,26 @@
+/*
+ * 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.server.authentication;
+
+import java.time.Duration;
+
+public interface ActiveTimeoutProvider {
+ Duration getActiveSessionTimeout();
+}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/HardcodedActiveTimeoutProvider.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/HardcodedActiveTimeoutProvider.java
new file mode 100644
index 00000000000..e8e40cd6f60
--- /dev/null
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/HardcodedActiveTimeoutProvider.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.server.authentication;
+
+import java.time.Duration;
+
+public class HardcodedActiveTimeoutProvider implements ActiveTimeoutProvider {
+ public static final Duration DEFAULT_ACTIVE_TIMEOUT_DURATION = Duration.ofDays(90);
+
+ @Override
+ public Duration getActiveSessionTimeout() {
+ return DEFAULT_ACTIVE_TIMEOUT_DURATION;
+ }
+}
+
+
+
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java
index ec640a9b11f..216a0c92fa2 100644
--- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java
@@ -21,6 +21,7 @@ package org.sonar.server.authentication;
import com.google.common.collect.ImmutableMap;
import io.jsonwebtoken.Claims;
+import java.time.Duration;
import java.util.Collections;
import java.util.Date;
import java.util.Map;
@@ -41,7 +42,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;
import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.apache.commons.lang3.time.DateUtils.addSeconds;
-import static org.sonar.process.ProcessProperties.Property.WEB_SESSION_TIMEOUT_IN_MIN;
+import static org.sonar.process.ProcessProperties.Property.WEB_INACTIVE_SESSION_TIMEOUT_IN_MIN;
import static org.sonar.server.authentication.Cookies.SAMESITE_LAX;
import static org.sonar.server.authentication.Cookies.SET_COOKIE;
import static org.sonar.server.authentication.Cookies.findCookie;
@@ -50,39 +51,40 @@ import static org.sonar.server.authentication.JwtSerializer.LAST_REFRESH_TIME_PA
@ServerSide
public class JwtHttpHandler {
- private static final int SESSION_TIMEOUT_DEFAULT_VALUE_IN_MINUTES = 3 * 24 * 60;
- private static final int MAX_SESSION_TIMEOUT_IN_MINUTES = 3 * 30 * 24 * 60;
+ private static final Duration DEFAULT_INACTIVE_TIMEOUT_DURATION = Duration.ofDays(3);
+ private static final Duration MINIMUM_INACTIVE_TIMEOUT_DURATION = Duration.ofMinutes(5);
+ private static final Duration MAXIMUM_INACTIVE_TIMEOUT_DURATION = Duration.ofDays(90);
private static final String JWT_COOKIE = "JWT-SESSION";
-
private static final String CSRF_JWT_PARAM = "xsrfToken";
- // Time after which a user will be disconnected
- private static final int SESSION_DISCONNECT_IN_SECONDS = 3 * 30 * 24 * 60 * 60;
-
// This refresh time is used to refresh the session
- // The value must be lower than sessionTimeoutInSeconds
- private static final int SESSION_REFRESH_IN_SECONDS = 5 * 60;
+ // The value must be lower than inactiveSessionTimeoutInSeconds
+ private static final Duration SESSION_REFRESH = Duration.ofMinutes(5);
private final System2 system2;
private final DbClient dbClient;
private final JwtSerializer jwtSerializer;
// This timeout is used to disconnect the user we he has not browse any page for a while
- private final int sessionTimeoutInSeconds;
+ private final Duration inactiveSessionTimeout;
+ // This timeout is used to disconnect the user regardless of his activity after logging in
+ private final Duration activeSessionTimeout;
private final JwtCsrfVerifier jwtCsrfVerifier;
- public JwtHttpHandler(System2 system2, DbClient dbClient, Configuration config, JwtSerializer jwtSerializer, JwtCsrfVerifier jwtCsrfVerifier) {
+ public JwtHttpHandler(System2 system2, DbClient dbClient, Configuration config, JwtSerializer jwtSerializer,
+ JwtCsrfVerifier jwtCsrfVerifier, ActiveTimeoutProvider activeTimeoutProvider) {
this.jwtSerializer = jwtSerializer;
this.dbClient = dbClient;
this.system2 = system2;
- this.sessionTimeoutInSeconds = getSessionTimeoutInSeconds(config);
+ this.inactiveSessionTimeout = getInactiveSessionTimeout(config);
+ this.activeSessionTimeout = activeTimeoutProvider.getActiveSessionTimeout();
this.jwtCsrfVerifier = jwtCsrfVerifier;
}
public void generateToken(UserDto user, Map<String, Object> properties, HttpRequest request, HttpResponse response) {
- String csrfState = jwtCsrfVerifier.generateState(request, response, sessionTimeoutInSeconds);
- long expirationTime = system2.now() + sessionTimeoutInSeconds * 1000L;
+ String csrfState = jwtCsrfVerifier.generateState(request, response, (int) inactiveSessionTimeout.toSeconds());
+ long expirationTime = system2.now() + (int) inactiveSessionTimeout.toSeconds() * 1000L;
SessionTokenDto sessionToken = createSessionToken(user, expirationTime);
String token = jwtSerializer.encode(new JwtSerializer.JwtSession(
@@ -94,7 +96,7 @@ public class JwtHttpHandler {
.put(LAST_REFRESH_TIME_PARAM, system2.now())
.put(CSRF_JWT_PARAM, csrfState)
.build()));
- response.addHeader(SET_COOKIE, createJwtSession(request, JWT_COOKIE, token, sessionTimeoutInSeconds));
+ response.addHeader(SET_COOKIE, createJwtSession(request, JWT_COOKIE, token, (int) inactiveSessionTimeout.toSeconds()));
}
private SessionTokenDto createSessionToken(UserDto user, long expirationTime) {
@@ -156,12 +158,12 @@ public class JwtHttpHandler {
if (now.getTime() > sessionToken.get().getExpirationDate()) {
return Optional.empty();
}
- if (now.after(addSeconds(token.getIssuedAt(), SESSION_DISCONNECT_IN_SECONDS))) {
+ if (now.after(addSeconds(token.getIssuedAt(), (int) activeSessionTimeout.toSeconds()))) {
return Optional.empty();
}
jwtCsrfVerifier.verifyState(request, (String) token.get(CSRF_JWT_PARAM), token.getSubject());
- if (now.after(addSeconds(getLastRefreshDate(token), SESSION_REFRESH_IN_SECONDS))) {
+ if (now.after(addSeconds(getLastRefreshDate(token), (int) SESSION_REFRESH.toSeconds()))) {
refreshToken(dbSession, sessionToken.get(), token, request, response);
}
@@ -176,10 +178,10 @@ public class JwtHttpHandler {
}
private void refreshToken(DbSession dbSession, SessionTokenDto tokenFromDb, Claims tokenFromCookie, HttpRequest request, HttpResponse response) {
- long expirationTime = system2.now() + sessionTimeoutInSeconds * 1000L;
+ long expirationTime = system2.now() + (int) inactiveSessionTimeout.toSeconds() * 1000L;
String refreshToken = jwtSerializer.refresh(tokenFromCookie, expirationTime);
- response.addHeader(SET_COOKIE, createJwtSession(request, JWT_COOKIE, refreshToken, sessionTimeoutInSeconds));
- jwtCsrfVerifier.refreshState(request, response, (String) tokenFromCookie.get(CSRF_JWT_PARAM), sessionTimeoutInSeconds);
+ response.addHeader(SET_COOKIE, createJwtSession(request, JWT_COOKIE, refreshToken, (int) inactiveSessionTimeout.toSeconds()));
+ jwtCsrfVerifier.refreshState(request, response, (String) tokenFromCookie.get(CSRF_JWT_PARAM), (int) inactiveSessionTimeout.toSeconds());
dbClient.sessionTokensDao().update(dbSession, tokenFromDb.setExpirationDate(expirationTime));
dbSession.commit();
@@ -219,12 +221,12 @@ public class JwtHttpHandler {
return Optional.ofNullable(user != null && user.isActive() ? user : null);
}
- private static int getSessionTimeoutInSeconds(Configuration config) {
- int minutes = config.getInt(WEB_SESSION_TIMEOUT_IN_MIN.getKey()).orElse(SESSION_TIMEOUT_DEFAULT_VALUE_IN_MINUTES);
- checkArgument(minutes > SESSION_REFRESH_IN_SECONDS / 60 && minutes <= MAX_SESSION_TIMEOUT_IN_MINUTES,
- "Property %s must be higher than 5 minutes and must not be greater than 3 months. Got %s minutes", WEB_SESSION_TIMEOUT_IN_MIN.getKey(),
- minutes);
- return minutes * 60;
+ private static Duration getInactiveSessionTimeout(Configuration config) {
+ String key = WEB_INACTIVE_SESSION_TIMEOUT_IN_MIN.getKey();
+ int minutes = config.getInt(key).orElse((int) DEFAULT_INACTIVE_TIMEOUT_DURATION.toMinutes());
+ checkArgument(minutes > MINIMUM_INACTIVE_TIMEOUT_DURATION.toMinutes() && minutes <= MAXIMUM_INACTIVE_TIMEOUT_DURATION.toMinutes(),
+ "Property %s must be at least 6 minutes and must not be greater than 90 days (129 600 minutes). Got %s minutes", key, minutes);
+ return Duration.ofMinutes(minutes);
}
public static class Token {
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposer.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposer.java
index 1d3e9eaed18..e3fc380c07f 100644
--- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposer.java
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposer.java
@@ -79,7 +79,7 @@ public class TokenExpirationEmailComposer extends EmailSender<TokenExpirationEma
format("<br/>If this token is still needed, please consider <a href=\"%s/account/security/\">generating</a> an equivalent.<br/><br/>", server.getPublicRootUrl()))
.append("Don't forget to update the token in the locations where it is in use. "
+ "This may include the CI pipeline that analyzes your projects, "
- + "the IDE settings that connect SonarLint to SonarQube, "
+ + "the IDE settings that connect SonarQube IDE to SonarQube Server, "
+ "and any places where you make calls to web services.");
return builder.toString();
}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/NetworkInterfaceProviderTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/HardcodedActiveTimeoutProviderTest.java
index 3b30ac0263e..6e2bd66b73d 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/NetworkInterfaceProviderTest.java
+++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/HardcodedActiveTimeoutProviderTest.java
@@ -17,21 +17,24 @@
* 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.server.webhook.ws;
+package org.sonar.server.authentication;
-import java.net.SocketException;
-import java.util.List;
-import org.junit.Test;
+import java.time.Duration;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
-public class NetworkInterfaceProviderTest {
- private NetworkInterfaceProvider underTest = new NetworkInterfaceProvider();
+class HardcodedActiveTimeoutProviderTest {
+ private ActiveTimeoutProvider underTest;
+
+ @BeforeEach
+ void setUp() {
+ underTest = new HardcodedActiveTimeoutProvider();
+ }
@Test
- public void itGetsListOfNetworkInterfaceAddresses() throws SocketException {
- assertThat(underTest.getNetworkInterfaceAddresses())
- .isInstanceOf(List.class)
- .hasSizeGreaterThan(0);
+ void getActiveTimeoutInMinutes_whenSessionTimeoutIsNotConfigured_returns90Days() {
+ assertThat(underTest.getActiveSessionTimeout()).isEqualTo(Duration.ofDays(90));
}
}
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/OAuth2AuthenticationParametersImplTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/OAuth2AuthenticationParametersImplTest.java
index ee35d5e9def..0dc172f6f4b 100644
--- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/OAuth2AuthenticationParametersImplTest.java
+++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/OAuth2AuthenticationParametersImplTest.java
@@ -112,7 +112,12 @@ public class OAuth2AuthenticationParametersImplTest {
Optional<String> redirection = underTest.getReturnTo(request);
- assertThat(redirection).contains("/admin/settings");
+ assertThat(redirection).isPresent();
+ String actualUrl = redirection.get();
+ assertThat(actualUrl).satisfiesAnyOf(
+ url -> assertThat(url).isEqualTo("/admin/settings"),
+ url -> assertThat(url).isEqualTo("%5Cadmin%5Csettings")
+ );
}
@Test
@@ -150,15 +155,15 @@ public class OAuth2AuthenticationParametersImplTest {
@DataProvider
public static Object[][] payloadToSanitizeAndExpectedOutcome() {
return new Object[][]{
- {generatePath("/admin/settings"), "/admin/settings"},
- {generatePath("/admin/../../settings"), "/settings"},
- {generatePath("/admin/../settings"), "/settings"},
- {generatePath("/admin/settings/.."), "/admin"},
- {generatePath("/admin/..%2fsettings/"), "/settings"},
- {generatePath("/admin/%2e%2e%2fsettings/"), "/settings"},
- {generatePath("../admin/settings"), null},
- {generatePath("/dashboard?id=project&pullRequest=PRID"), "/dashboard?id=project&pullRequest=PRID"},
- {generatePath("%2Fdashboard%3Fid%3Dproject%26pullRequest%3DPRID&authorizationError=true"), "/dashboard?id=project&pullRequest=PRID&authorizationError=true"},
+ {generatePath("/admin/settings"), "/admin/settings", "%5Cadmin%5Csettings"},
+ {generatePath("/admin/../../settings"), "/settings", "%5Csettings"},
+ {generatePath("/admin/../settings"), "/settings", "%5Csettings"},
+ {generatePath("/admin/settings/.."), "/admin", "%5Cadmin"},
+ {generatePath("/admin/..%2fsettings/"), "/settings", "%5Csettings"},
+ {generatePath("/admin/%2e%2e%2fsettings/"), "/settings", "%5Csettings"},
+ {generatePath("../admin/settings"), null, null},
+ {generatePath("/dashboard?id=project&pullRequest=PRID"), "/dashboard?id=project&pullRequest=PRID", "%5Cdashboard?id=project&pullRequest=PRID"},
+ {generatePath("%2Fdashboard%3Fid%3Dproject%26pullRequest%3DPRID&authorizationError=true"), "/dashboard?id=project&pullRequest=PRID&authorizationError=true", "%5Cdashboard?id=project&pullRequest=PRID&authorizationError=true"},
};
}
@@ -168,12 +173,20 @@ public class OAuth2AuthenticationParametersImplTest {
@Test
@UseDataProvider("payloadToSanitizeAndExpectedOutcome")
- public void getReturnTo_whenContainingPathTraversalCharacters_sanitizeThem(String payload, @Nullable String expectedSanitizedUrl) {
+ public void getReturnTo_whenContainingPathTraversalCharacters_sanitizeThem(String payload, @Nullable String expectedSanitizedUrl, @Nullable String expectedWindowsSanitizedUrl) {
when(request.getCookies()).thenReturn(new Cookie[]{wrapCookie(AUTHENTICATION_COOKIE_NAME, payload)});
Optional<String> redirection = underTest.getReturnTo(request);
- assertThat(redirection).isEqualTo(Optional.ofNullable(expectedSanitizedUrl));
+ if (expectedSanitizedUrl == null) {
+ assertThat(redirection).isEmpty();
+ } else {
+ String actualUrl = redirection.orElseThrow();
+ assertThat(actualUrl).satisfiesAnyOf(
+ url -> assertThat(url).isEqualTo(expectedSanitizedUrl),
+ url -> assertThat(url).isEqualTo(expectedWindowsSanitizedUrl)
+ );
+ }
}
private JakartaHttpRequest.JakartaCookie wrapCookie(String name, String value) {
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposerTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposerTest.java
index ba45ed43dc4..d0ba6ca7224 100644
--- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposerTest.java
+++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposerTest.java
@@ -70,7 +70,7 @@ class TokenExpirationEmailComposerTest {
+ "Last used on: January 01, 2022<br/>"
+ "Expires on: %s<br/><br/>"
+ "If this token is still needed, please consider <a href=\"http://localhost/account/security/\">generating</a> an equivalent.<br/><br/>"
- + "Don't forget to update the token in the locations where it is in use. This may include the CI pipeline that analyzes your projects, the IDE settings that connect SonarLint to SonarQube, and any places where you make calls to web services.",
+ + "Don't forget to update the token in the locations where it is in use. This may include the CI pipeline that analyzes your projects, the IDE settings that connect SonarQube IDE to SonarQube Server, and any places where you make calls to web services.",
parseDate(expiredDate), parseDate(expiredDate)));
}
@@ -91,7 +91,7 @@ class TokenExpirationEmailComposerTest {
+ "Last used on: January 01, 2022<br/>"
+ "Expired on: %s<br/><br/>"
+ "If this token is still needed, please consider <a href=\"http://localhost/account/security/\">generating</a> an equivalent.<br/><br/>"
- + "Don't forget to update the token in the locations where it is in use. This may include the CI pipeline that analyzes your projects, the IDE settings that connect SonarLint to SonarQube, and any places where you make calls to web services.",
+ + "Don't forget to update the token in the locations where it is in use. This may include the CI pipeline that analyzes your projects, the IDE settings that connect SonarQube IDE to SonarQube Server, and any places where you make calls to web services.",
parseDate(expiredDate)));
}
diff --git a/server/sonar-webserver-common/src/it/java/org/sonar/server/common/component/ComponentUpdaterIT.java b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/component/ComponentUpdaterIT.java
index 809c680b6e2..82375857e19 100644
--- a/server/sonar-webserver-common/src/it/java/org/sonar/server/common/component/ComponentUpdaterIT.java
+++ b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/component/ComponentUpdaterIT.java
@@ -40,6 +40,7 @@ import org.sonar.db.component.BranchType;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.ComponentQualifiers;
import org.sonar.db.component.ComponentScopes;
+import org.sonar.db.portfolio.PortfolioDto;
import org.sonar.db.project.CreationMethod;
import org.sonar.db.project.ProjectDto;
import org.sonar.db.user.UserDto;
@@ -569,4 +570,34 @@ public class ComponentUpdaterIT {
ProjectDto projectDto = underTest.create(db.getSession(), creationParameters).projectDto();
assertThat(projectDto.getCreationMethod()).isEqualTo(CreationMethod.ALM_IMPORT_BROWSER);
}
+
+ @Test
+ public void fail_to_create_portfolio_using_duplicate_child_portfolio_key() {
+ var parentPortfolio = db.components().insertPublicPortfolioDto();
+ var childPortfolio = new PortfolioDto()
+ .setKey("ExistingChild")
+ .setName("name_" + "ExistingChild")
+ .setSelectionMode(PortfolioDto.SelectionMode.NONE)
+ .setRootUuid(parentPortfolio.getRootUuid())
+ .setParentUuid(parentPortfolio.getUuid())
+ .setUuid("uuid_" + "ExistingChild");
+ DbSession session = db.getSession();
+ db.getDbClient().portfolioDao().insertWithAudit(session, childPortfolio);
+ session.commit();
+
+ var newPortfolio = NewComponent.newComponentBuilder()
+ .setKey(childPortfolio.getKey())
+ .setName("New Portfolio")
+ .setQualifier(VIEW)
+ .build();
+ var creationParameters = ComponentCreationParameters.builder()
+ .newComponent(newPortfolio)
+ .creationMethod(CreationMethod.LOCAL_API)
+ .mainBranchName("main")
+ .build();
+
+ assertThatThrownBy(() -> underTest.create(session, creationParameters))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("Could not create component with key: \"%s\". Key already in use.", childPortfolio.getKey());
+ }
}
diff --git a/server/sonar-webserver-common/src/it/java/org/sonar/server/common/user/service/UserServiceIT.java b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/user/service/UserServiceIT.java
index d187336783d..f69a9019e4d 100644
--- a/server/sonar-webserver-common/src/it/java/org/sonar/server/common/user/service/UserServiceIT.java
+++ b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/user/service/UserServiceIT.java
@@ -846,6 +846,52 @@ public class UserServiceIT {
assertThat(updatedUser.getExternalLogin()).isEqualTo("prov_login");
}
+ @Test
+ public void updateUser_whenUpdatingScmAccountsAndInstanceManaged_shouldChange() {
+ when(managedInstanceService.isInstanceExternallyManaged()).thenReturn(true);
+
+ UserDto userDto = db.users().insertUser();
+ UpdateUser updateUser = new UpdateUser();
+ updateUser.setScmAccounts(List.of("newaccount"));
+
+ userService.updateUser(userDto.getUuid(), updateUser);
+
+ UserDto updatedUser = db.users().selectUserByLogin(userDto.getLogin()).orElseThrow();
+
+ assertThat(updatedUser.getSortedScmAccounts()).isEqualTo(List.of("newaccount"));
+ }
+
+ @DataProvider
+ public static Object[][] managedInstanceBlockedFields() {
+ return new Object[][] {
+ { "email", (Function<UpdateUser,UpdateUser>) updateUser -> updateUser.setEmail("new@email.com")},
+ { "email", (Function<UpdateUser,UpdateUser>) updateUser -> updateUser.setName("new name")},
+ { "email", (Function<UpdateUser,UpdateUser>) updateUser -> updateUser.setExternalIdentityProviderLogin("new-external-login")},
+ { "email", (Function<UpdateUser,UpdateUser>) updateUser -> updateUser.setExternalIdentityProvider("LDAP")},
+ { "email", (Function<UpdateUser,UpdateUser>) updateUser -> updateUser.setExternalIdentityProviderId("new-provider-id")},
+ };
+ }
+
+ @Test
+ @UseDataProvider("managedInstanceBlockedFields")
+ public void updateUser_whenUpdatingBlockedFieldAndInstanceManaged_shouldThrow(String fieldName,
+ Function<UpdateUser, UpdateUser> updateUserFunction) {
+ doThrow(BadRequestException.create("User information's cannot be updated when the instance is externally managed"))
+ .when(managedInstanceChecker).throwIfInstanceIsManaged(any());
+
+ UserDto userDto = db.users().insertUser();
+
+ assertThatThrownBy(() -> updateUser(userDto, updateUserFunction))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("User information's cannot be updated when the instance is externally managed");
+ }
+
+ private void updateUser(UserDto userDto, Function<UpdateUser, UpdateUser> updateUserFunction) {
+ UpdateUser updateUser = new UpdateUser();
+ updateUser = updateUserFunction.apply(updateUser);
+ userService.updateUser(userDto.getUuid(), updateUser);
+ }
+
@DataProvider
public static Object[][] updateUserProvider() {
return new Object[][] {
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/component/ComponentUpdater.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/component/ComponentUpdater.java
index 10c39b9a7a4..6e7a0c00fa1 100644
--- a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/component/ComponentUpdater.java
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/component/ComponentUpdater.java
@@ -185,8 +185,11 @@ public class ComponentUpdater {
}
private void checkKeyAlreadyExists(DbSession dbSession, NewComponent newComponent) {
+ Optional<PortfolioDto> portfolios = dbClient.portfolioDao().selectByKey(dbSession, newComponent.key());
+ if (portfolios.isPresent()) {
+ throwBadRequestException("Could not create component with key: \"%s\". Key already in use.", newComponent.key());
+ }
List<ComponentDto> componentDtos = dbClient.componentDao().selectByKeyCaseInsensitive(dbSession, newComponent.key());
-
if (!componentDtos.isEmpty()) {
String alreadyExistingKeys = componentDtos
.stream()
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupSearchRequest.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupSearchRequest.java
index 1ef6af378a1..816b971f540 100644
--- a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupSearchRequest.java
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupSearchRequest.java
@@ -24,6 +24,8 @@ import javax.annotation.Nullable;
public record GroupSearchRequest(
@Nullable String query,
@Nullable Boolean managed,
+ @Nullable String userUuid,
+ @Nullable String excludedUserUuid,
int page,
int pageSize
) {
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupService.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupService.java
index fac6c4bb711..2801c905d82 100644
--- a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupService.java
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupService.java
@@ -93,6 +93,8 @@ public class GroupService {
return GroupQuery.builder()
.searchText(groupSearchRequest.query())
.isManagedClause(getManagedInstanceSql(groupSearchRequest.managed()))
+ .userId(groupSearchRequest.userUuid())
+ .excludedUserId(groupSearchRequest.excludedUserUuid())
.build();
}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/GitUrlParser.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/GitUrlParser.java
new file mode 100644
index 00000000000..59bc0eb2e76
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/GitUrlParser.java
@@ -0,0 +1,222 @@
+/*
+ * 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.server.common.projectbindings.service;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
+
+import static org.sonar.server.exceptions.BadRequestException.throwBadRequestException;
+
+/**
+ * Generic parser for Git repository URLs.
+ *
+ * <p>This parser extracts organization, project, and repository information from repository URLs
+ * without trying to identify the specific DevOps platform. It focuses on parsing the path
+ * components after the domain to work with any Git hosting platform including enterprise instances.</p>
+ *
+ * <p>Supported URL patterns:</p>
+ * <ul>
+ * <li><strong>Standard format:</strong> http(s)://domain.com/org/repo</li>
+ * <li><strong>With project:</strong> http(s)://domain.com/org/project/_git/repo (Azure DevOps style)</li>
+ * <li><strong>Nested groups:</strong> http(s)://domain.com/group/subgroup/repo (GitLab style)</li>
+ * <li><strong>SSH format:</strong> git@domain.com:org/repo</li>
+ * <li><strong>SSH with project:</strong> git@domain.com:v3/org/project/repo (Azure DevOps style)</li>
+ * </ul>
+ */
+public final class GitUrlParser {
+
+ // SSH-only patterns (HTTP/HTTPS handled via URI parsing)
+ private static final Pattern SSH_STANDARD_PATTERN = Pattern.compile("^git@([^:]+):([^/]+)/([^/]+?)(?:\\.git)?/*$");
+ private static final Pattern SSH_AZURE_PATTERN = Pattern.compile("^git@([^:]+):v3/([^/]+)/([^/]+)/([^/]+?)(?:\\.git)?/*$");
+ private static final Pattern SSH_NESTED_PATTERN = Pattern.compile("^git@([^:]+):([^/]+/.+)/([^/]+?)(?:\\.git)?/*$");
+
+ // Path-only patterns for HTTP/HTTPS URLs
+ private static final Pattern PATH_STANDARD_PATTERN = Pattern.compile("^/([^/]+)/([^/]+?)(?:\\.git)?/*$");
+ private static final Pattern PATH_AZURE_PATTERN = Pattern.compile("^/([^/]+)/([^/]+)/_git/([^/]+?)(?:\\.git)?/*$");
+ private static final Pattern PATH_NESTED_PATTERN = Pattern.compile("^/([^/]+/.+)/([^/]+?)(?:\\.git)?/*$");
+
+ private static final String GIT_SUFFIX = ".git";
+ private static final String HTTP_PREFIX = "http://";
+ private static final String HTTPS_PREFIX = "https://";
+ private static final String SSH_PREFIX = "git@";
+
+ private GitUrlParser() {
+ // Utility class, no instantiation
+ }
+
+ /**
+ * Parses a Git repository URL to extract path components.
+ *
+ * @param url The Git repository URL to parse
+ * @return RepositoryInfo if the URL matches a supported format
+ * @throws org.sonar.server.exceptions.BadRequestException if the URL is null or empty
+ */
+ public static Optional<RepositoryInfo> parseRepositoryUrl(@Nullable String url) {
+ validateInput(url);
+ String trimmedUrl = url.trim();
+
+ // Protocol detection upfront
+ if (trimmedUrl.startsWith(SSH_PREFIX)) {
+ return parseSshUrl(trimmedUrl);
+ }
+
+ if (trimmedUrl.startsWith(HTTPS_PREFIX) || trimmedUrl.startsWith(HTTP_PREFIX)) {
+ return parseHttpUrl(trimmedUrl);
+ }
+
+ // Fallback for other formats
+ return Optional.empty();
+ }
+
+ /**
+ * Validates the input URL.
+ */
+ private static void validateInput(@Nullable String url) {
+ if (StringUtils.isBlank(url)) {
+ throwBadRequestException("URL cannot be empty");
+ }
+ }
+
+ /**
+ * Parses SSH URLs using regex patterns.
+ */
+ private static Optional<RepositoryInfo> parseSshUrl(String url) {
+ return tryPattern(SSH_AZURE_PATTERN, url, GitUrlParser::parseAzureMatch)
+ .or(() -> tryPattern(SSH_NESTED_PATTERN, url, GitUrlParser::parseNestedMatch))
+ .or(() -> tryPattern(SSH_STANDARD_PATTERN, url, GitUrlParser::parseStandardMatch));
+ }
+
+ /**
+ * Parses HTTP/HTTPS URLs using URI parsing.
+ */
+ private static Optional<RepositoryInfo> parseHttpUrl(String url) {
+ try {
+ URI uri = new URI(url);
+ String path = uri.getPath();
+
+ if (StringUtils.isBlank(path) || path.equals("/")) {
+ return Optional.empty();
+ }
+
+ return parsePathComponents(path);
+ } catch (URISyntaxException e) {
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * Parses path components extracted from URI (much simpler than full URL regex).
+ */
+ private static Optional<RepositoryInfo> parsePathComponents(String path) {
+ return tryPattern(PATH_AZURE_PATTERN, path, GitUrlParser::parseAzureMatch)
+ .or(() -> tryPattern(PATH_NESTED_PATTERN, path, GitUrlParser::parseNestedMatch))
+ .or(() -> tryPattern(PATH_STANDARD_PATTERN, path, GitUrlParser::parseStandardMatch));
+ }
+
+ /**
+ * Tries to match a pattern against input and applies a function to the matcher if successful.
+ */
+ private static Optional<RepositoryInfo> tryPattern(Pattern pattern, String input, Function<Matcher, RepositoryInfo> matchFunction) {
+ Matcher matcher = pattern.matcher(input);
+ return matcher.matches() ? Optional.of(matchFunction.apply(matcher)) : Optional.empty();
+ }
+
+ /**
+ * Parses Azure DevOps format matches (org/project/_git/repo or org/project/repo).
+ */
+ private static RepositoryInfo parseAzureMatch(Matcher matcher) {
+ if (matcher.groupCount() == 4) {
+ // SSH: git@host:v3/org/project/repo
+ return new RepositoryInfo(matcher.group(2), removeGitSuffix(matcher.group(4)), matcher.group(3));
+ } else {
+ // HTTP: /org/project/_git/repo
+ return new RepositoryInfo(matcher.group(1), removeGitSuffix(matcher.group(3)), matcher.group(2));
+ }
+ }
+
+ /**
+ * Parses nested format matches (org/suborg/repo).
+ */
+ private static RepositoryInfo parseNestedMatch(Matcher matcher) {
+ if (matcher.groupCount() == 3) {
+ // SSH: git@host:org/suborg/repo
+ return new RepositoryInfo(matcher.group(2), removeGitSuffix(matcher.group(3)));
+ } else {
+ // HTTP: /org/suborg/repo
+ return new RepositoryInfo(matcher.group(1), removeGitSuffix(matcher.group(2)));
+ }
+ }
+
+ /**
+ * Parses standard format matches (org/repo).
+ */
+ private static RepositoryInfo parseStandardMatch(Matcher matcher) {
+ if (matcher.groupCount() == 3) {
+ // SSH: git@host:org/repo
+ return new RepositoryInfo(matcher.group(2), removeGitSuffix(matcher.group(3)));
+ } else {
+ // HTTP: /org/repo
+ return new RepositoryInfo(matcher.group(1), removeGitSuffix(matcher.group(2)));
+ }
+ }
+
+ /**
+ * Removes .git suffix and trailing slashes from URLs.
+ */
+ private static String removeGitSuffix(String url) {
+ if (url.endsWith(GIT_SUFFIX)) {
+ url = url.substring(0, url.length() - GIT_SUFFIX.length());
+ }
+ // Remove trailing slashes
+ while (url.endsWith("/")) {
+ url = url.substring(0, url.length() - 1);
+ }
+ return url;
+ }
+
+ /**
+ * Information extracted from a parsed Git repository URL.
+ *
+ * @param organization The organization, workspace, group, or namespace name
+ * @param repository The repository name
+ * @param project The project name (used by Azure DevOps, null for other platforms)
+ */
+ public record RepositoryInfo(String organization, String repository, @Nullable String project) {
+
+ RepositoryInfo(String organization, String repository) {
+ this(organization, repository, null);
+ }
+
+ public String slug() {
+ return organization + "/" + repository;
+ }
+
+ public String projectName() {
+ return StringUtils.defaultString(project);
+ }
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingSearchStrategy.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingSearchStrategy.java
new file mode 100644
index 00000000000..b33a3cb4ab3
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingSearchStrategy.java
@@ -0,0 +1,72 @@
+/*
+ * 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.server.common.projectbindings.service;
+
+import java.util.List;
+import java.util.function.Function;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.setting.ProjectAlmSettingDto;
+import org.sonar.db.alm.setting.ProjectAlmSettingQuery;
+
+/**
+ * Enum-based strategy for searching project bindings based on Git repository information.
+ * Each ALM platform (GitHub, Azure DevOps, Bitbucket) has different storage patterns for repository information in the database.
+ */
+public enum ProjectBindingSearchStrategy {
+
+ /**
+ * GitHub search strategy.
+ * GitHub stores repository information in "org/repo" format in the alm_repo column.
+ */
+ GITHUB(info -> ProjectAlmSettingQuery.forAlmRepo(info.slug())),
+
+ /**
+ * Azure DevOps search strategy.
+ * Azure DevOps stores project name in alm_slug and repository name in alm_repo.
+ */
+ AZURE_DEVOPS(info -> ProjectAlmSettingQuery.forAlmRepoAndSlug(info.repository(), info.projectName())),
+
+ /**
+ * Bitbucket search strategy.
+ * Bitbucket stores repository name in the alm_repo column.
+ */
+ BITBUCKET(info -> ProjectAlmSettingQuery.forAlmRepo(info.repository()));
+
+ private final Function<GitUrlParser.RepositoryInfo, ProjectAlmSettingQuery> queryBuilder;
+
+ ProjectBindingSearchStrategy(Function<GitUrlParser.RepositoryInfo, ProjectAlmSettingQuery> queryBuilder) {
+ this.queryBuilder = queryBuilder;
+ }
+
+ /**
+ * Searches for project ALM settings based on the provided repository information.
+ *
+ * @param dbClient the database client
+ * @param session the database session
+ * @param repositoryInfo parsed Git repository information
+ * @return list of matching project ALM settings
+ */
+ public List<ProjectAlmSettingDto> search(DbClient dbClient, DbSession session, GitUrlParser.RepositoryInfo repositoryInfo) {
+ ProjectAlmSettingQuery query = queryBuilder.apply(repositoryInfo);
+ return dbClient.projectAlmSettingDao().selectProjectAlmSettings(session, query, 1, Integer.MAX_VALUE);
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsSearchRequest.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsSearchRequest.java
index 0cb0101ade9..0180aeb2186 100644
--- a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsSearchRequest.java
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsSearchRequest.java
@@ -24,8 +24,7 @@ import javax.annotation.Nullable;
public record ProjectBindingsSearchRequest(
@Nullable String repository,
@Nullable String dopSettingId,
+ @Nullable String repositoryUrl,
Integer page,
- Integer pageSize
-) {
-
+ Integer pageSize) {
}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsService.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsService.java
index 5bbd97fdbf9..2ab7d39431e 100644
--- a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsService.java
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsService.java
@@ -19,14 +19,18 @@
*/
package org.sonar.server.common.projectbindings.service;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
+import org.sonar.db.alm.setting.ALM;
import org.sonar.db.alm.setting.ProjectAlmSettingDto;
import org.sonar.db.alm.setting.ProjectAlmSettingQuery;
import org.sonar.db.project.ProjectDto;
@@ -49,6 +53,12 @@ public class ProjectBindingsService {
}
public SearchResults<ProjectBindingInformation> findProjectBindingsByRequest(ProjectBindingsSearchRequest request) {
+ // Check if repository URL is provided for Git URL search
+ if (StringUtils.isNotBlank(request.repositoryUrl())) {
+ return findProjectBindingsByGitUrl(request.repositoryUrl());
+ }
+
+ // Use traditional repository name search
ProjectAlmSettingQuery query = buildProjectAlmSettingQuery(request);
try (DbSession session = dbClient.openSession(false)) {
int total = dbClient.projectAlmSettingDao().countProjectAlmSettings(session, query);
@@ -61,6 +71,8 @@ public class ProjectBindingsService {
}
private static ProjectAlmSettingQuery buildProjectAlmSettingQuery(ProjectBindingsSearchRequest request) {
+ // Note: repositoryUrl is handled separately in findProjectBindingsByRequest
+ // This method only handles traditional repository name search
return new ProjectAlmSettingQuery(request.repository(), request.dopSettingId());
}
@@ -79,8 +91,13 @@ public class ProjectBindingsService {
private static Function<ProjectAlmSettingDto, ProjectBindingInformation> projectAlmSettingDtoToProjectBindingInformation(Map<String, ProjectDto> projectUuidToProject) {
return projectAlmSettingDto -> {
ProjectDto projectDto = projectUuidToProject.get(projectAlmSettingDto.getProjectUuid());
- return new ProjectBindingInformation(projectAlmSettingDto.getUuid(), projectAlmSettingDto.getAlmSettingUuid(), projectAlmSettingDto.getProjectUuid(), projectDto.getKey(),
- projectAlmSettingDto.getAlmRepo(), projectAlmSettingDto.getAlmSlug());
+ return new ProjectBindingInformation(
+ projectAlmSettingDto.getUuid(),
+ projectAlmSettingDto.getAlmSettingUuid(),
+ projectAlmSettingDto.getProjectUuid(),
+ projectDto.getKey(),
+ projectAlmSettingDto.getAlmRepo(),
+ projectAlmSettingDto.getAlmSlug());
};
}
@@ -90,4 +107,64 @@ public class ProjectBindingsService {
}
}
+ /**
+ * Searches for project bindings using a Git repository URL.
+ * This method performs fuzzy matching by searching for repository names in database fields.
+ *
+ * @param gitUrl The Git repository URL to search for
+ * @return SearchResults containing matching project bindings
+ */
+ public SearchResults<ProjectBindingInformation> findProjectBindingsByGitUrl(@Nullable String gitUrl) {
+ if (StringUtils.isBlank(gitUrl)) {
+ return new SearchResults<>(List.of(), 0);
+ }
+
+ Optional<GitUrlParser.RepositoryInfo> repositoryInfoOpt = GitUrlParser.parseRepositoryUrl(gitUrl);
+ if (repositoryInfoOpt.isEmpty()) {
+ return new SearchResults<>(List.of(), 0);
+ }
+
+ GitUrlParser.RepositoryInfo repositoryInfo = repositoryInfoOpt.get();
+
+ try (DbSession session = dbClient.openSession(false)) {
+ Set<ProjectAlmSettingDto> projectAlmSettings = searchProjectBindings(session, repositoryInfo);
+
+ Set<String> projectUuids = projectAlmSettings.stream()
+ .map(ProjectAlmSettingDto::getProjectUuid)
+ .collect(Collectors.toSet());
+
+ List<ProjectDto> projectDtos = dbClient.projectDao().selectByUuids(session, projectUuids);
+ Map<String, ProjectDto> projectUuidsToProject = projectDtos.stream()
+ .collect(Collectors.toMap(ProjectDto::getUuid, identity()));
+
+ List<ProjectBindingInformation> results = projectAlmSettings.stream()
+ .map(projectAlmSettingDtoToProjectBindingInformation(projectUuidsToProject))
+ .toList();
+
+ return new SearchResults<>(results, results.size());
+ }
+ }
+
+ private Set<ProjectAlmSettingDto> searchProjectBindings(DbSession session, GitUrlParser.RepositoryInfo repositoryInfo) {
+ Set<ProjectAlmSettingDto> allResults = new HashSet<>();
+ for (ALM alm : ALM.values()) {
+ ProjectBindingSearchStrategy strategy = getSearchStrategy(alm);
+ if (strategy != null) {
+ allResults.addAll(strategy.search(dbClient, session, repositoryInfo));
+ }
+ }
+
+ return allResults.stream().collect(Collectors.toSet());
+ }
+
+ private static ProjectBindingSearchStrategy getSearchStrategy(ALM alm) {
+ return switch (alm) {
+ case GITHUB -> ProjectBindingSearchStrategy.GITHUB;
+ case AZURE_DEVOPS -> ProjectBindingSearchStrategy.AZURE_DEVOPS;
+ case BITBUCKET, BITBUCKET_CLOUD -> ProjectBindingSearchStrategy.BITBUCKET;
+ // Skip GitLab for now
+ case GITLAB -> null;
+ };
+ }
+
}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/service/UserService.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/service/UserService.java
index 95f24094f58..23a78e60169 100644
--- a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/service/UserService.java
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/service/UserService.java
@@ -222,7 +222,7 @@ public class UserService {
public UserInformation updateUser(String uuid, UpdateUser updateUser) {
try (DbSession dbSession = dbClient.openSession(false)) {
throwIfInvalidChangeOfExternalProvider(updateUser);
- throwIfManagedInstanceAndNameOrEmailUpdated(updateUser);
+ throwIfManagedInstanceAndNameOrEmailOrExternalInfoUpdated(updateUser);
UserDto userDto = findUserOrThrow(uuid, dbSession);
userUpdater.updateAndCommit(dbSession, userDto, updateUser, u -> {
});
@@ -248,11 +248,15 @@ public class UserService {
Optional.ofNullable(updateUser.externalIdentityProvider()).ifPresent(this::assertProviderIsSupported);
}
- private void throwIfManagedInstanceAndNameOrEmailUpdated(UpdateUser updateUser) {
+ private void throwIfManagedInstanceAndNameOrEmailOrExternalInfoUpdated(UpdateUser updateUser) {
boolean isNameChanged = updateUser.isNameChanged();
boolean isEmailDefined = updateUser.isEmailChanged();
- if (isNameChanged || isEmailDefined) {
- managedInstanceChecker.throwIfInstanceIsManaged("User name and email cannot be updated when the instance is externally managed");
+ boolean isExternalLoginChanged = updateUser.isExternalIdentityProviderLoginChanged();
+ boolean isExternalProviderChanged = updateUser.isExternalIdentityProviderChanged();
+ boolean isExternalIdChanged = updateUser.isExternalIdentityProviderIdChanged();
+
+ if (isNameChanged || isEmailDefined || isExternalLoginChanged || isExternalProviderChanged || isExternalIdChanged) {
+ managedInstanceChecker.throwIfInstanceIsManaged("User information's cannot be updated when the instance is externally managed");
}
}
diff --git a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/group/service/GroupServiceTest.java b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/group/service/GroupServiceTest.java
index 02a37273efa..bafc6e7431d 100644
--- a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/group/service/GroupServiceTest.java
+++ b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/group/service/GroupServiceTest.java
@@ -316,7 +316,7 @@ public class GroupServiceTest {
public void createGroup_whenNameAndDescriptionIsProvided_createsGroup() {
when(uuidFactory.create()).thenReturn("1234");
- GroupDto createdGroup = mockGroupDto();
+ GroupDto createdGroup = mockGroupDto();
when(dbClient.groupDao().insert(eq(dbSession), any())).thenReturn(createdGroup);
mockDefaultGroup();
groupService.createGroup(dbSession, "Name", "Description");
@@ -378,7 +378,7 @@ public class GroupServiceTest {
when(dbClient.groupDao().countByQuery(eq(dbSession), any())).thenReturn(300);
- SearchResults<GroupInformation> searchResults = groupService.search(dbSession, new GroupSearchRequest("query", null, 5, 24));
+ SearchResults<GroupInformation> searchResults = groupService.search(dbSession, new GroupSearchRequest("query", null, null, null, 5, 24));
assertThat(searchResults.total()).isEqualTo(300);
Map<String, GroupInformation> uuidToGroupInformation = searchResults.searchResults().stream()
@@ -390,11 +390,12 @@ public class GroupServiceTest {
assertThat(queryCaptor.getValue().getSearchText()).isEqualTo("%QUERY%");
assertThat(queryCaptor.getValue().getIsManagedSqlClause()).isNull();
}
+
@Test
public void search_whenPageSizeEquals0_returnsOnlyTotal() {
when(dbClient.groupDao().countByQuery(eq(dbSession), any())).thenReturn(10);
- SearchResults<GroupInformation> searchResults = groupService.search(dbSession, new GroupSearchRequest("query", null, 0, 24));
+ SearchResults<GroupInformation> searchResults = groupService.search(dbSession, new GroupSearchRequest("query", null, null, null, 0, 24));
assertThat(searchResults.total()).isEqualTo(10);
assertThat(searchResults.searchResults()).isEmpty();
@@ -406,7 +407,7 @@ public class GroupServiceTest {
mockManagedInstance();
when(dbClient.groupDao().selectByQuery(eq(dbSession), queryCaptor.capture(), anyInt(), anyInt())).thenReturn(List.of());
- groupService.search(dbSession, new GroupSearchRequest("query", true, 5, 24));
+ groupService.search(dbSession, new GroupSearchRequest("query", true, null, null, 5, 24));
assertThat(queryCaptor.getValue().getIsManagedSqlClause()).isEqualTo("managed_filter");
}
@@ -416,7 +417,7 @@ public class GroupServiceTest {
mockManagedInstance();
when(dbClient.groupDao().selectByQuery(eq(dbSession), queryCaptor.capture(), anyInt(), anyInt())).thenReturn(List.of());
- groupService.search(dbSession, new GroupSearchRequest("query", false, 5, 24));
+ groupService.search(dbSession, new GroupSearchRequest("query", false, null, null, 5, 24));
assertThat(queryCaptor.getValue().getIsManagedSqlClause()).isEqualTo("not_managed_filter");
}
@@ -424,7 +425,7 @@ public class GroupServiceTest {
@Test
public void search_whenInstanceNotManagedAndManagedIsTrue_throws() {
assertThatExceptionOfType(BadRequestException.class)
- .isThrownBy(() -> groupService.search(dbSession, new GroupSearchRequest("query", true, 5, 24)))
+ .isThrownBy(() -> groupService.search(dbSession, new GroupSearchRequest("query", true, null, null, 5, 24)))
.withMessage("The 'managed' parameter is only available for managed instances.");
}
@@ -434,6 +435,26 @@ public class GroupServiceTest {
when(managedInstanceService.getManagedGroupsSqlFilter(false)).thenReturn("not_managed_filter");
}
+ @Test
+ public void search_whenUserIdParameterProvided_addsItToQuery() {
+ when(dbClient.groupDao().selectByQuery(eq(dbSession), queryCaptor.capture(), anyInt(), anyInt())).thenReturn(List.of());
+
+ groupService.search(dbSession, new GroupSearchRequest("query", null, "includedUserUuid", null, 5, 24));
+
+ assertThat(queryCaptor.getValue().getUserId()).isEqualTo("includedUserUuid");
+ assertThat(queryCaptor.getValue().getExcludedUserId()).isNull();
+ }
+
+ @Test
+ public void search_whenExcludedUserIdParameterProvided_addsItToQuery() {
+ when(dbClient.groupDao().selectByQuery(eq(dbSession), queryCaptor.capture(), anyInt(), anyInt())).thenReturn(List.of());
+
+ groupService.search(dbSession, new GroupSearchRequest("query", null, null, "excludedUserId", 5, 24));
+
+ assertThat(queryCaptor.getValue().getUserId()).isNull();
+ assertThat(queryCaptor.getValue().getExcludedUserId()).isEqualTo("excludedUserId");
+ }
+
private static void assertGroupInformation(Map<String, GroupInformation> uuidToGroupInformation, GroupDto expectedGroupDto, boolean expectedManaged, boolean expectedDefault) {
assertThat(uuidToGroupInformation.get(expectedGroupDto.getUuid()).groupDto()).isEqualTo(expectedGroupDto);
assertThat(uuidToGroupInformation.get(expectedGroupDto.getUuid()).isManaged()).isEqualTo(expectedManaged);
diff --git a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/projectbindings/service/GitUrlParserTest.java b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/projectbindings/service/GitUrlParserTest.java
new file mode 100644
index 00000000000..3caab3cb684
--- /dev/null
+++ b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/projectbindings/service/GitUrlParserTest.java
@@ -0,0 +1,280 @@
+/*
+ * 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.server.common.projectbindings.service;
+
+import java.util.Optional;
+import java.util.stream.Stream;
+import javax.annotation.Nullable;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.sonar.server.common.projectbindings.service.GitUrlParser.RepositoryInfo;
+import org.sonar.server.exceptions.BadRequestException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+class GitUrlParserTest {
+
+ @ParameterizedTest
+ @MethodSource("successfulParsingCases")
+ void parseRepositoryUrl_shouldParseValidUrls(String url, String expectedOrg, String expectedRepo, @Nullable String expectedProject) {
+ Optional<RepositoryInfo> result = GitUrlParser.parseRepositoryUrl(url);
+
+ assertRepositoryInfo(result, expectedOrg, expectedRepo, expectedProject);
+ }
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ void parseRepositoryUrl_shouldThrowExceptionForNullOrEmptyUrls(@Nullable String url) {
+ assertThatThrownBy(() -> GitUrlParser.parseRepositoryUrl(url))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("URL cannot be empty");
+ }
+
+ @ParameterizedTest
+ @MethodSource("invalidUrls")
+ void parseRepositoryUrl_shouldReturnEmptyForInvalidUrls(@Nullable String url) {
+ assertThat(GitUrlParser.parseRepositoryUrl(url)).isEmpty();
+ }
+
+ private static void assertRepositoryInfo(Optional<RepositoryInfo> result, String expectedOrg, String expectedRepo, @Nullable String expectedProject) {
+ assertThat(result).isPresent();
+ RepositoryInfo info = result.get();
+
+ assertThat(info.organization()).isEqualTo(expectedOrg);
+ assertThat(info.repository()).isEqualTo(expectedRepo);
+ assertThat(info.project()).isEqualTo(expectedProject);
+ assertThat(info.slug()).isEqualTo(expectedOrg + "/" + expectedRepo);
+
+ if (expectedProject != null) {
+ assertThat(info.projectName()).isEqualTo(expectedProject);
+ } else {
+ assertThat(info.projectName()).isEmpty();
+ }
+ }
+
+ private static Stream<Arguments> successfulParsingCases() {
+ return Stream.of(
+ // ===== STANDARD FORMAT (org/repo) =====
+
+ // HTTP/HTTPS Standard - Basic cases
+ arguments("https://github.com/org/repo", "org", "repo", null),
+ arguments("http://github.com/org/repo", "org", "repo", null),
+ arguments("https://bitbucket.org/myorg/myrepo", "myorg", "myrepo", null),
+ arguments("https://gitlab.com/company/project", "company", "project", null),
+ arguments("http://git.company.com/team/service", "team", "service", null),
+
+ // HTTP/HTTPS Standard - With .git suffix
+ arguments("https://github.com/org/repo.git", "org", "repo", null),
+ arguments("http://gitlab.com/user/project.git", "user", "project", null),
+
+ // HTTP/HTTPS Standard - With trailing slashes
+ arguments("https://github.com/org/repo/", "org", "repo", null),
+ arguments("https://github.com/org/repo//", "org", "repo", null),
+ arguments("https://github.com/org/repo///", "org", "repo", null),
+ arguments("https://github.com/org/repo.git/", "org", "repo", null),
+ arguments("https://github.com/org/repo.git///", "org", "repo", null),
+
+ // HTTP/HTTPS Standard - With ports
+ arguments("https://git.company.com:8080/org/repo", "org", "repo", null),
+ arguments("http://gitlab.local:3000/org/repo.git", "org", "repo", null),
+ arguments("https://bitbucket.example.com:9443/team/service/", "team", "service", null),
+
+ // HTTP/HTTPS Standard - With authentication
+ arguments("https://user@github.com/org/repo", "org", "repo", null),
+ arguments("https://user:pass@gitlab.com/org/repo.git", "org", "repo", null),
+
+ // HTTP/HTTPS Standard - With query parameters
+ arguments("https://github.com/org/repo?branch=main", "org", "repo", null),
+ arguments("https://github.com/org/repo.git?ref=develop", "org", "repo", null),
+
+ // SSH Standard - Basic cases
+ arguments("git@github.com:org/repo", "org", "repo", null),
+ arguments("git@gitlab.com:user/project", "user", "project", null),
+ arguments("git@bitbucket.org:company/service", "company", "service", null),
+
+ // SSH Standard - With .git suffix
+ arguments("git@github.com:org/repo.git", "org", "repo", null),
+ arguments("git@gitlab.com:user/project.git", "user", "project", null),
+
+ // SSH Standard - With trailing slashes
+ arguments("git@github.com:org/repo/", "org", "repo", null),
+ arguments("git@github.com:org/repo//", "org", "repo", null),
+ arguments("git@github.com:org/repo.git/", "org", "repo", null),
+
+ // SSH Standard - Custom hosts
+ arguments("git@git.company.com:team/project", "team", "project", null),
+ arguments("git@gitlab.example.org:dept/app.git", "dept", "app", null),
+
+ // ===== AZURE DEVOPS FORMAT (org/project/_git/repo) =====
+
+ // HTTP/HTTPS Azure - Basic cases
+ arguments("https://dev.azure.com/myorg/myproject/_git/myrepo", "myorg", "myrepo", "myproject"),
+ arguments("http://dev.azure.com/company/frontend/_git/webapp", "company", "webapp", "frontend"),
+ arguments("https://devops.company.com/team/backend/_git/api", "team", "api", "backend"),
+
+ // HTTP/HTTPS Azure - With .git suffix
+ arguments("https://dev.azure.com/myorg/myproject/_git/myrepo.git", "myorg", "myrepo", "myproject"),
+ arguments("http://azure.example.com/org/proj/_git/service.git", "org", "service", "proj"),
+
+ // HTTP/HTTPS Azure - With trailing slashes
+ arguments("https://dev.azure.com/myorg/myproject/_git/myrepo/", "myorg", "myrepo", "myproject"),
+ arguments("https://dev.azure.com/myorg/myproject/_git/myrepo//", "myorg", "myrepo", "myproject"),
+ arguments("https://dev.azure.com/myorg/myproject/_git/myrepo.git/", "myorg", "myrepo", "myproject"),
+ arguments("https://dev.azure.com/myorg/myproject/_git/myrepo.git///", "myorg", "myrepo", "myproject"),
+
+ // HTTP/HTTPS Azure - With ports
+ arguments("https://devops.company.com:8443/myorg/myproject/_git/myrepo", "myorg", "myrepo", "myproject"),
+ arguments("http://azure.local:8080/team/proj/_git/app.git", "team", "app", "proj"),
+
+ // SSH Azure - Basic cases
+ arguments("git@ssh.dev.azure.com:v3/myorg/myproject/myrepo", "myorg", "myrepo", "myproject"),
+ arguments("git@azure.company.com:v3/team/frontend/webapp", "team", "webapp", "frontend"),
+
+ // SSH Azure - With .git suffix
+ arguments("git@ssh.dev.azure.com:v3/myorg/myproject/myrepo.git", "myorg", "myrepo", "myproject"),
+ arguments("git@devops.example.com:v3/org/backend/api.git", "org", "api", "backend"),
+
+ // SSH Azure - With trailing slashes
+ arguments("git@ssh.dev.azure.com:v3/myorg/myproject/myrepo/", "myorg", "myrepo", "myproject"),
+ arguments("git@ssh.dev.azure.com:v3/myorg/myproject/myrepo//", "myorg", "myrepo", "myproject"),
+ arguments("git@ssh.dev.azure.com:v3/myorg/myproject/myrepo.git/", "myorg", "myrepo", "myproject"),
+
+ // ===== NESTED FORMAT (group/subgroup/repo) =====
+
+ // HTTP/HTTPS Nested - Basic cases
+ arguments("https://gitlab.com/group/subgroup/project", "group/subgroup", "project", null),
+ arguments("http://gitlab.com/company/team/service", "company/team", "service", null),
+ arguments("https://git.company.com/dept/division/app", "dept/division", "app", null),
+
+ // HTTP/HTTPS Nested - With .git suffix
+ arguments("https://gitlab.com/group/subgroup/project.git", "group/subgroup", "project", null),
+ arguments("http://gitlab.example.com/org/team/repo.git", "org/team", "repo", null),
+
+ // HTTP/HTTPS Nested - With trailing slashes
+ arguments("https://gitlab.com/group/subgroup/project/", "group/subgroup", "project", null),
+ arguments("https://gitlab.com/group/subgroup/project//", "group/subgroup", "project", null),
+ arguments("https://gitlab.com/group/subgroup/project.git/", "group/subgroup", "project", null),
+ arguments("https://gitlab.com/group/subgroup/project.git///", "group/subgroup", "project", null),
+
+ // HTTP/HTTPS Nested - With ports
+ arguments("https://gitlab.company.com:9443/group/subgroup/project", "group/subgroup", "project", null),
+ arguments("http://git.local:3000/team/division/app.git", "team/division", "app", null),
+
+ // HTTP/HTTPS Nested - Deep nesting
+ arguments("https://gitlab.com/org/team/dept/division/project", "org/team/dept/division", "project", null),
+ arguments("https://gitlab.com/a/b/c/d/e/f/repo.git", "a/b/c/d/e/f", "repo", null),
+
+ // SSH Nested - Basic cases
+ arguments("git@gitlab.com:group/subgroup/project", "group/subgroup", "project", null),
+ arguments("git@git.company.com:team/division/service", "team/division", "service", null),
+
+ // SSH Nested - With .git suffix
+ arguments("git@gitlab.com:group/subgroup/project.git", "group/subgroup", "project", null),
+ arguments("git@gitlab.example.com:org/team/app.git", "org/team", "app", null),
+
+ // SSH Nested - With trailing slashes
+ arguments("git@gitlab.com:group/subgroup/project/", "group/subgroup", "project", null),
+ arguments("git@gitlab.com:group/subgroup/project//", "group/subgroup", "project", null),
+ arguments("git@gitlab.com:group/subgroup/project.git/", "group/subgroup", "project", null),
+
+ // SSH Nested - Deep nesting
+ arguments("git@gitlab.com:org/team/dept/division/project.git", "org/team/dept/division", "project", null),
+
+ // ===== EDGE CASES =====
+
+ // URLs with special characters in names
+ arguments("https://github.com/my-org/my-repo", "my-org", "my-repo", null),
+ arguments("git@gitlab.com:user.name/project_name.git", "user.name", "project_name", null),
+ arguments("https://dev.azure.com/my-org/my-project/_git/my-repo", "my-org", "my-repo", "my-project"),
+
+ // Very long paths (potential exponential backtracking)
+ arguments(
+ "https://gitlab.com/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/NOTFOUND",
+ "a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a",
+ "NOTFOUND",
+ null),
+
+ // Mixed case scenarios
+ arguments("https://GitHub.COM/ORG/REPO.GIT", "ORG", "REPO.GIT", null), // .GIT (uppercase) is not removed
+
+ // Numeric organization/project names
+ arguments("https://github.com/123org/456repo", "123org", "456repo", null),
+ arguments("git@gitlab.com:999/project123.git", "999", "project123", null),
+
+ // URLs that might be mistaken as invalid but are parsed as nested format
+ arguments("https://dev.azure.com/org/_git/repo", "org/_git", "repo", null), // parsed as nested, not Azure
+ arguments("https://dev.azure.com/org/project/git/repo", "org/project/git", "repo", null), // parsed as nested
+ arguments("git@ssh.dev.azure.com:v3/org/repo", "v3/org", "repo", null), // parsed as nested, not Azure SSH
+ arguments("git@ssh.dev.azure.com:v2/org/project/repo", "v2/org/project", "repo", null), // parsed as nested
+ arguments("https://dev.azure.com/org/project/_git/", "org/project", "_git", null) // trailing slash parsed as nested
+ );
+ }
+
+ private static Stream<String> invalidUrls() {
+ return Stream.of(
+ // Unsupported protocols
+ "ftp://github.com/user/repo",
+ "svn://server.com/repo",
+ "file:///local/path/repo",
+
+ // Incomplete URLs
+ "https://github.com/", // missing org and repo
+ "https://github.com/org", // missing repo
+ "http://gitlab.com", // no path
+
+ // Invalid SSH formats
+ "git@", // incomplete SSH
+ "git@github.com", // missing colon and path
+ "git@github.com:", // missing path
+ "git@github.com:/", // invalid path
+
+ // Not URLs at all
+ "not-a-url-at-all",
+ "github.com/org/repo", // missing protocol
+ "www.github.com/org/repo", // missing protocol
+
+ // Invalid characters or malformed
+ "https://github.com/org repo/test", // space in path
+ "https://", // protocol only
+ "http://", // protocol only
+
+ // Just domain without path
+ "https://github.com",
+ "http://dev.azure.com",
+ "git@gitlab.com:",
+
+ // URLs with only root path
+ "https://github.com/",
+ "http://gitlab.com/",
+
+ // URLs that result in empty repo names
+ "https://github.com/org/", // missing repo (empty after slash removal)
+
+ // URLs that are too short (after reconsidering the actual parser behavior)
+ "https://a.com/b",
+ "git@a.com:b"
+ );
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/projectbindings/service/ProjectBindingsServiceTest.java b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/projectbindings/service/ProjectBindingsServiceTest.java
index 77324fcd16f..9bb3f622793 100644
--- a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/projectbindings/service/ProjectBindingsServiceTest.java
+++ b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/projectbindings/service/ProjectBindingsServiceTest.java
@@ -22,9 +22,13 @@ package org.sonar.server.common.projectbindings.service;
import java.util.List;
import java.util.Optional;
import java.util.Set;
+import javax.annotation.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
@@ -102,7 +106,7 @@ public class ProjectBindingsServiceTest {
when(dbClient.projectDao().selectByUuids(dbSession, Set.of("projectUuid_1", "projectUuid_2")))
.thenReturn(List.of(mockProjectDto1, mockProjectDto2));
- ProjectBindingsSearchRequest request = new ProjectBindingsSearchRequest(REPO_QUERY, ALM_SETTING_UUID_QUERY, 12, 42);
+ ProjectBindingsSearchRequest request = new ProjectBindingsSearchRequest(REPO_QUERY, ALM_SETTING_UUID_QUERY, null, 12, 42);
List<ProjectBindingInformation> expectedResults = List.of(projectBindingInformation("1"), projectBindingInformation("2"));
@@ -119,7 +123,7 @@ public class ProjectBindingsServiceTest {
when(dbClient.projectAlmSettingDao().countProjectAlmSettings(eq(dbSession), any()))
.thenReturn(12);
- ProjectBindingsSearchRequest request = new ProjectBindingsSearchRequest(null, null, 42, 0);
+ ProjectBindingsSearchRequest request = new ProjectBindingsSearchRequest(null, null, null, 42, 0);
SearchResults<ProjectBindingInformation> actualResults = underTest.findProjectBindingsByRequest(request);
assertThat(actualResults.total()).isEqualTo(12);
@@ -128,6 +132,103 @@ public class ProjectBindingsServiceTest {
verify(dbClient.projectAlmSettingDao(), never()).selectProjectAlmSettings(eq(dbSession), any(), anyInt(), anyInt());
}
+ @Test
+ void findProjectBindingsByRequest_whenRepositoryUrlProvided_usesGitUrlSearch() {
+ when(dbClient.projectAlmSettingDao().selectProjectAlmSettings(eq(dbSession), any(), eq(1), eq(Integer.MAX_VALUE)))
+ .thenReturn(List.of());
+ when(dbClient.projectDao().selectByUuids(dbSession, Set.of()))
+ .thenReturn(List.of());
+
+ ProjectBindingsSearchRequest request = new ProjectBindingsSearchRequest(null, null, "https://github.com/org/repo", 0, 50);
+
+ SearchResults<ProjectBindingInformation> actualResults = underTest.findProjectBindingsByRequest(request);
+
+ assertThat(actualResults.searchResults()).isEmpty();
+ assertThat(actualResults.total()).isZero();
+
+ verify(dbClient.projectAlmSettingDao(), never()).countProjectAlmSettings(eq(dbSession), any());
+ }
+
+ @Test
+ void findProjectFromBinding_whenProjectExists_returnsIt() {
+ ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class);
+ when(projectAlmSettingDto.getProjectUuid()).thenReturn("project-uuid-123");
+
+ ProjectDto projectDto = mock(ProjectDto.class);
+ when(dbClient.projectDao().selectByUuid(dbSession, "project-uuid-123")).thenReturn(Optional.of(projectDto));
+
+ Optional<ProjectDto> result = underTest.findProjectFromBinding(projectAlmSettingDto);
+
+ assertThat(result).isPresent().contains(projectDto);
+ }
+
+ @Test
+ void findProjectFromBinding_whenProjectDoesNotExist_returnsEmpty() {
+ ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class);
+ when(projectAlmSettingDto.getProjectUuid()).thenReturn("non-existent-uuid");
+
+ when(dbClient.projectDao().selectByUuid(dbSession, "non-existent-uuid")).thenReturn(Optional.empty());
+
+ Optional<ProjectDto> result = underTest.findProjectFromBinding(projectAlmSettingDto);
+
+ assertThat(result).isEmpty();
+ }
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ @ValueSource(strings = {" ", "not-a-valid-url"})
+ void findProjectBindingsByGitUrl_whenUrlIsInvalid_returnsEmptyResults(@Nullable String url) {
+ SearchResults<ProjectBindingInformation> result = underTest.findProjectBindingsByGitUrl(url);
+
+ assertThat(result.searchResults()).isEmpty();
+ assertThat(result.total()).isZero();
+ }
+
+ @Test
+ void findProjectBindingsByGitUrl_whenUrlIsValid_returnsResults() {
+ ProjectAlmSettingDto githubSetting = mockProjectAlmSettingDto("1");
+ ProjectAlmSettingDto azureSetting = mockProjectAlmSettingDto("2");
+
+ when(dbClient.projectAlmSettingDao().selectProjectAlmSettings(eq(dbSession), any(), eq(1), eq(Integer.MAX_VALUE)))
+ .thenReturn(List.of(githubSetting))
+ .thenReturn(List.of(azureSetting))
+ .thenReturn(List.of())
+ .thenReturn(List.of());
+
+ ProjectDto projectDto1 = mockProjectDto("1");
+ ProjectDto projectDto2 = mockProjectDto("2");
+ when(dbClient.projectDao().selectByUuids(dbSession, Set.of("projectUuid_1", "projectUuid_2")))
+ .thenReturn(List.of(projectDto1, projectDto2));
+
+ SearchResults<ProjectBindingInformation> result = underTest.findProjectBindingsByGitUrl("https://github.com/org/repo");
+
+ assertThat(result.searchResults()).hasSize(2);
+ assertThat(result.total()).isEqualTo(2);
+ assertThat(result.searchResults()).extracting(ProjectBindingInformation::id)
+ .containsExactlyInAnyOrder("uuid_1", "uuid_2");
+ }
+
+ @Test
+ void findProjectBindingsByGitUrl_whenDuplicateResults_removesThemAndReturnsDistinct() {
+ ProjectAlmSettingDto duplicatedSetting = mockProjectAlmSettingDto("1");
+
+ when(dbClient.projectAlmSettingDao().selectProjectAlmSettings(eq(dbSession), any(), eq(1), eq(Integer.MAX_VALUE)))
+ .thenReturn(List.of(duplicatedSetting))
+ .thenReturn(List.of(duplicatedSetting))
+ .thenReturn(List.of())
+ .thenReturn(List.of());
+
+ ProjectDto projectDto = mockProjectDto("1");
+ when(dbClient.projectDao().selectByUuids(dbSession, Set.of("projectUuid_1")))
+ .thenReturn(List.of(projectDto));
+
+ SearchResults<ProjectBindingInformation> result = underTest.findProjectBindingsByGitUrl("https://github.com/org/repo");
+
+ assertThat(result.searchResults()).hasSize(1);
+ assertThat(result.total()).isEqualTo(1);
+ assertThat(result.searchResults().get(0).id()).isEqualTo("uuid_1");
+ }
+
private static ProjectAlmSettingDto mockProjectAlmSettingDto(String i) {
ProjectAlmSettingDto dto = mock();
when(dto.getUuid()).thenReturn("uuid_" + i);
@@ -146,12 +247,7 @@ public class ProjectBindingsServiceTest {
}
private static ProjectBindingInformation projectBindingInformation(String i) {
- return new ProjectBindingInformation("uuid_" + i,
- "almSettingUuid_" + i,
- "projectUuid_" + i,
- "projectKey_" + i,
- "almRepo_" + i,
- "almSlug_" + i);
+ return new ProjectBindingInformation("uuid_" + i, "almSettingUuid_" + i, "projectUuid_" + i, "projectKey_" + i, "almRepo_" + i, "almSlug_" + i);
}
}
diff --git a/server/sonar-webserver-core/build.gradle b/server/sonar-webserver-core/build.gradle
index 63f2814ea6a..99ab89c9f2d 100644
--- a/server/sonar-webserver-core/build.gradle
+++ b/server/sonar-webserver-core/build.gradle
@@ -63,8 +63,6 @@ dependencies {
testImplementation 'org.apache.logging.log4j:log4j-core'
testImplementation 'org.assertj:assertj-core'
testImplementation 'org.assertj:assertj-guava'
- testImplementation 'org.eclipse.jetty:jetty-server'
- testImplementation 'org.eclipse.jetty:jetty-servlet'
testImplementation 'org.hamcrest:hamcrest'
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testImplementation 'org.junit.jupiter:junit-jupiter-params'
diff --git a/server/sonar-webserver-core/src/it/java/org/sonar/server/startup/RegisterMetricsIT.java b/server/sonar-webserver-core/src/it/java/org/sonar/server/startup/RegisterMetricsIT.java
index 1278205a383..814f5b2b20d 100644
--- a/server/sonar-webserver-core/src/it/java/org/sonar/server/startup/RegisterMetricsIT.java
+++ b/server/sonar-webserver-core/src/it/java/org/sonar/server/startup/RegisterMetricsIT.java
@@ -44,7 +44,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class RegisterMetricsIT {
- public static final int SOON_TO_BE_REMOVED_COMPLEXITY_METRICS_COUNT = 7;
@Rule
public DbTester dbTester = DbTester.create(System2.INSTANCE);
@@ -144,9 +143,7 @@ public class RegisterMetricsIT {
.isEqualTo(CoreMetrics.getMetrics().size()
// Metric CoreMetrics.WONT_FIX_ISSUES was renamed to CoreMetrics.ACCEPTED_ISSUES in 10.3.
// We don't want to insert it anymore
- - 1
- // SONAR-12647 We are exclusing complexity metrics, they will be removed from the plugin API soon
- - SOON_TO_BE_REMOVED_COMPLEXITY_METRICS_COUNT);
+ - 1);
}
@Test
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/issue/index/AsyncIssueIndexCreationTelemetry.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/issue/index/AsyncIssueIndexCreationTelemetry.java
index 213c4e153fc..a1fffb536c7 100644
--- a/server/sonar-webserver-core/src/main/java/org/sonar/server/issue/index/AsyncIssueIndexCreationTelemetry.java
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/issue/index/AsyncIssueIndexCreationTelemetry.java
@@ -19,6 +19,7 @@
*/
package org.sonar.server.issue.index;
+import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.time.Clock;
import java.util.Set;
@@ -50,7 +51,6 @@ public class AsyncIssueIndexCreationTelemetry {
private static final Logger LOGGER = LoggerFactory.getLogger(AsyncIssueIndexCreationTelemetry.class);
- private final IssueIndexSyncProgressChecker issueIndexSyncProgressChecker;
private final DbClient dbClient;
private final TelemetryClient telemetryClient;
private final Server server;
@@ -63,9 +63,8 @@ public class AsyncIssueIndexCreationTelemetry {
private long startTime;
private int nbIndexingTasks;
- public AsyncIssueIndexCreationTelemetry(IssueIndexSyncProgressChecker issueIndexSyncProgressChecker, DbClient dbClient,
- TelemetryClient telemetryClient, Server server, UuidFactory uuidFactory, Clock clock, IssueIndexMonitoringScheduler scheduler, Configuration config) {
- this.issueIndexSyncProgressChecker = issueIndexSyncProgressChecker;
+ public AsyncIssueIndexCreationTelemetry(DbClient dbClient, TelemetryClient telemetryClient, Server server, UuidFactory uuidFactory, Clock clock,
+ IssueIndexMonitoringScheduler scheduler, Configuration config) {
this.dbClient = dbClient;
this.telemetryClient = telemetryClient;
this.server = server;
@@ -86,18 +85,21 @@ public class AsyncIssueIndexCreationTelemetry {
}
startTime = clock.millis();
this.nbIndexingTasks = nbIndexingTasks;
- if(currentMonitoring != null) {
+ if (currentMonitoring != null) {
currentMonitoring.cancel(false);
}
- currentMonitoring = scheduler.scheduleAtFixedRate(() -> {
- try (DbSession dbSession = dbClient.openSession(false)) {
- if (!issueIndexSyncProgressChecker.isIssueSyncInProgress(dbSession)) {
- sendIssueIndexationTelemetry(dbSession);
- currentMonitoring.cancel(false);
- }
- }
- }, 0, 5, TimeUnit.SECONDS);
+ currentMonitoring = scheduler.scheduleAtFixedRate(this::tryToSendTelemetry, 0, 5, TimeUnit.SECONDS);
+
+ }
+ @VisibleForTesting
+ void tryToSendTelemetry() {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ if (!dbClient.ceQueueDao().hasAnyIssueSyncTaskPendingOrInProgress(dbSession)) {
+ sendIssueIndexationTelemetry(dbSession);
+ currentMonitoring.cancel(false);
+ }
+ }
}
private void sendIssueIndexationTelemetry(DbSession dbSession) {
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/RulesRegistrationContext.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/RulesRegistrationContext.java
index a3533a80743..e124a467ae8 100644
--- a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/RulesRegistrationContext.java
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/RulesRegistrationContext.java
@@ -83,9 +83,11 @@ class RulesRegistrationContext {
String ruleUuid = entry.getKey();
RuleDto rule = dbRulesByRuleUuid.get(ruleUuid);
if (rule == null) {
- LOG.warn("Could not retrieve rule with uuid %s referenced by a deprecated rule key. " +
- "The following deprecated rule keys seem to be referencing a non-existing rule",
- ruleUuid, entry.getValue());
+ LOG.warn("Could not retrieve rule with uuid {} referenced by a deprecated rule key. " +
+ "The following deprecated rule keys seem to be referencing a non-existing rule: {}",
+ ruleUuid, entry.getValue().stream()
+ .map(SingleDeprecatedRuleKey::getOldRuleKeyAsRuleKey)
+ .collect(Collectors.toSet()));
} else {
entry.getValue().forEach(d -> rulesByKey.put(d.getOldRuleKeyAsRuleKey(), rule));
}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterMetrics.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterMetrics.java
index 05fcb23d55c..08965b0c895 100644
--- a/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterMetrics.java
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterMetrics.java
@@ -20,11 +20,10 @@
package org.sonar.server.startup;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.FluentIterable;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import org.sonar.api.Startable;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.measures.Metric;
@@ -39,24 +38,12 @@ import org.sonar.db.metric.MetricDto;
import org.sonar.server.metric.MetricToDto;
import org.springframework.beans.factory.annotation.Autowired;
-import static com.google.common.collect.FluentIterable.concat;
import static com.google.common.collect.Lists.newArrayList;
import static org.sonar.db.metric.RemovedMetricConverter.REMOVED_METRIC;
public class RegisterMetrics implements Startable {
private static final Logger LOG = Loggers.get(RegisterMetrics.class);
- /**
- * Those metrics will be removed soon from the plugin API, so let's not register them in the database
- */
- private static final Set<String> SOON_TO_BE_REMOVED_FROM_CORE_API_METRIC = Set.of(
- CoreMetrics.COMPLEXITY_IN_CLASSES_KEY,
- CoreMetrics.COMPLEXITY_IN_FUNCTIONS_KEY,
- CoreMetrics.FILE_COMPLEXITY_DISTRIBUTION_KEY,
- CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION_KEY,
- CoreMetrics.FUNCTION_COMPLEXITY_KEY,
- CoreMetrics.CLASS_COMPLEXITY_KEY,
- CoreMetrics.FILE_COMPLEXITY_KEY);
private final DbClient dbClient;
private final UuidFactory uuidFactory;
@@ -79,8 +66,10 @@ public class RegisterMetrics implements Startable {
@Override
public void start() {
- FluentIterable<Metric> metricsToRegister = concat(getCoreMetrics(), getPluginMetrics())
- .filter(m -> !REMOVED_METRIC.equals(m.getKey()));
+ List<Metric> metricsToRegister = new ArrayList<>();
+ metricsToRegister.addAll(CoreMetrics.getMetrics());
+ metricsToRegister.addAll(getPluginMetrics());
+ metricsToRegister.removeIf(m -> REMOVED_METRIC.equals(m.getKey()));
register(metricsToRegister);
}
@@ -131,13 +120,6 @@ public class RegisterMetrics implements Startable {
}
}
- private static List<Metric> getCoreMetrics() {
- return CoreMetrics.getMetrics()
- .stream()
- .filter(m -> !SOON_TO_BE_REMOVED_FROM_CORE_API_METRIC.contains(m.getKey()))
- .toList();
- }
-
@VisibleForTesting
List<Metric> getPluginMetrics() {
List<Metric> metricsToRegister = newArrayList();
@@ -153,7 +135,7 @@ public class RegisterMetrics implements Startable {
private static void checkMetrics(Map<String, Metrics> metricsByRepository, Metrics metrics) {
for (Metric metric : metrics.getMetrics()) {
String metricKey = metric.getKey();
- if (getCoreMetrics().contains(metric)) {
+ if (CoreMetrics.getMetrics().contains(metric)) {
throw new IllegalStateException(String.format("Metric [%s] is already defined by SonarQube", metricKey));
}
Metrics anotherRepository = metricsByRepository.get(metricKey);
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/issue/index/AsyncIssueIndexCreationTelemetryTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/issue/index/AsyncIssueIndexCreationTelemetryTest.java
index cb8bab7a24d..631d0f5464e 100644
--- a/server/sonar-webserver-core/src/test/java/org/sonar/server/issue/index/AsyncIssueIndexCreationTelemetryTest.java
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/issue/index/AsyncIssueIndexCreationTelemetryTest.java
@@ -20,11 +20,13 @@
package org.sonar.server.issue.index;
import java.io.IOException;
+import java.lang.reflect.Field;
import java.time.Clock;
import java.util.Optional;
import java.util.concurrent.ScheduledFuture;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.platform.commons.util.ReflectionUtils;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.sonar.api.config.Configuration;
@@ -32,6 +34,7 @@ import org.sonar.api.platform.Server;
import org.sonar.core.util.SequenceUuidFactory;
import org.sonar.core.util.UuidFactory;
import org.sonar.db.DbClient;
+import org.sonar.db.ce.CeQueueDao;
import org.sonar.telemetry.core.TelemetryClient;
import static org.assertj.core.api.Assertions.assertThat;
@@ -58,8 +61,7 @@ class AsyncIssueIndexCreationTelemetryTest {
private final IssueIndexMonitoringScheduler scheduler = mock();
private final ArgumentCaptor<Runnable> telemetrySyncRunnable = ArgumentCaptor.forClass(Runnable.class);
private final Configuration configuration = mock();
- private final AsyncIssueIndexCreationTelemetry asyncIssueIndexCreationTelemetry = new AsyncIssueIndexCreationTelemetry(
- issueIndexSyncProgressChecker,
+ private final AsyncIssueIndexCreationTelemetry underTest = new AsyncIssueIndexCreationTelemetry(
dbClient,
telemetryClient,
server,
@@ -84,14 +86,14 @@ class AsyncIssueIndexCreationTelemetryTest {
reset(configuration);
when(configuration.getBoolean(SONAR_TELEMETRY_ENABLE.getKey())).thenReturn(Optional.of(false));
- asyncIssueIndexCreationTelemetry.startIndexCreationMonitoringToSendTelemetry(100);
+ underTest.startIndexCreationMonitoringToSendTelemetry(100);
verify(scheduler, never()).scheduleAtFixedRate(any(), anyLong(), anyLong(), any());
}
@Test
void whenSynchroIsNotFinished_thenRetry() throws IOException {
- asyncIssueIndexCreationTelemetry.startIndexCreationMonitoringToSendTelemetry(100);
+ underTest.startIndexCreationMonitoringToSendTelemetry(100);
verify(scheduler).scheduleAtFixedRate(any(), anyLong(), anyLong(), any());
@@ -107,4 +109,19 @@ class AsyncIssueIndexCreationTelemetryTest {
.contains(AsyncIssueIndexCreationTelemetry.KEY_ASYNC_ISSUE_INDEXING_TASK_TOTAL_COUNT)
.contains(AsyncIssueIndexCreationTelemetry.KEY_ASYNC_ISSUE_INDEXING_TASK_FAILURE_COUNT);
}
+
+ @Test
+ void tryToSendTelemetry_whenNoPendingTasks_thenSendTelemetry() throws IOException, IllegalAccessException {
+ CeQueueDao ceQueueDao = mock();
+ when(ceQueueDao.hasAnyIssueSyncTaskPendingOrInProgress(any())).thenReturn(false);
+ when(dbClient.ceQueueDao()).thenReturn(ceQueueDao);
+
+ Field field = ReflectionUtils.findFields(underTest.getClass(), f -> f.getName().equals("currentMonitoring"), ReflectionUtils.HierarchyTraversalMode.TOP_DOWN).get(0);
+ field.setAccessible(true);
+ field.set(underTest, mock(ScheduledFuture.class));
+
+ underTest.tryToSendTelemetry();
+
+ verify(telemetryClient).uploadMetric(any());
+ }
}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/registration/RulesRegistrationContextTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/registration/RulesRegistrationContextTest.java
new file mode 100644
index 00000000000..4ea0c11e7c6
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/registration/RulesRegistrationContextTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.server.rule.registration;
+
+import java.util.List;
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.slf4j.event.Level;
+import org.sonar.api.testfixtures.log.LogTesterJUnit5;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.rule.DeprecatedRuleKeyDto;
+import org.sonar.db.rule.RuleDao;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class RulesRegistrationContextTest {
+
+ @RegisterExtension
+ private final LogTesterJUnit5 logTester = new LogTesterJUnit5();
+
+ @Test
+ void whenDeprecatedRuleIsNotFound_thenWarningLogTraceIsProduced() {
+ DbClient dbClient = mock();
+ DbSession dbSession = mock();
+ RuleDao ruleDao = mock();
+ when(dbClient.ruleDao()).thenReturn(ruleDao);
+ when(ruleDao.selectAll(dbSession)).thenReturn(List.of());
+ when(ruleDao.selectAllDeprecatedRuleKeys(dbSession)).thenReturn(Set.of(
+ createDeprecatedRuleKeyDto("oldKey", "oldRepo", "newKey", "uuid")
+ ));
+ when(ruleDao.selectAllRuleParams(dbSession)).thenReturn(List.of());
+
+ RulesRegistrationContext.create(dbClient, dbSession);
+
+ assertThat(logTester.logs(Level.WARN)).
+ contains("Could not retrieve rule with uuid newKey referenced by a deprecated rule key. " +
+ "The following deprecated rule keys seem to be referencing a non-existing rule: [oldRepo:oldKey]");
+ }
+
+ private DeprecatedRuleKeyDto createDeprecatedRuleKeyDto(String oldKey, String oldRepo, String newKey, String uuid) {
+ DeprecatedRuleKeyDto dto = new DeprecatedRuleKeyDto();
+ dto.setOldRuleKey(oldKey);
+ dto.setOldRepositoryKey(oldRepo);
+ dto.setRuleUuid(newKey);
+ dto.setUuid(uuid);
+ return dto;
+ }
+}
diff --git a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java
index 72b3bd272f0..6b2c826f2c9 100644
--- a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java
+++ b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java
@@ -26,6 +26,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -54,6 +55,7 @@ import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.BucketOrder;
import org.elasticsearch.search.aggregations.HasAggregations;
+import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation;
import org.elasticsearch.search.aggregations.bucket.filter.FilterAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.filter.FiltersAggregator;
import org.elasticsearch.search.aggregations.bucket.filter.ParsedFilter;
@@ -77,12 +79,13 @@ import org.sonar.api.issue.IssueStatus;
import org.sonar.api.issue.impact.SoftwareQuality;
import org.sonar.api.rule.Severity;
import org.sonar.api.rules.CleanCodeAttributeCategory;
-import org.sonar.core.rule.RuleType;
import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.api.server.rule.RulesDefinition.OwaspMobileTop10Version;
import org.sonar.api.server.rule.RulesDefinition.OwaspTop10Version;
import org.sonar.api.server.rule.RulesDefinition.PciDssVersion;
import org.sonar.api.utils.DateUtils;
import org.sonar.api.utils.System2;
+import org.sonar.core.rule.RuleType;
import org.sonar.server.es.EsClient;
import org.sonar.server.es.EsUtils;
import org.sonar.server.es.SearchOptions;
@@ -120,10 +123,10 @@ import static org.elasticsearch.index.query.QueryBuilders.termQuery;
import static org.elasticsearch.index.query.QueryBuilders.termsQuery;
import static org.elasticsearch.search.aggregations.AggregationBuilders.filters;
import static org.elasticsearch.search.aggregations.AggregationBuilders.reverseNested;
-import static org.sonar.core.rule.RuleType.SECURITY_HOTSPOT;
-import static org.sonar.core.rule.RuleType.VULNERABILITY;
import static org.sonar.core.config.MQRModeConstants.MULTI_QUALITY_MODE_DEFAULT_VALUE;
import static org.sonar.core.config.MQRModeConstants.MULTI_QUALITY_MODE_ENABLED;
+import static org.sonar.core.rule.RuleType.SECURITY_HOTSPOT;
+import static org.sonar.core.rule.RuleType.VULNERABILITY;
import static org.sonar.server.es.EsUtils.escapeSpecialRegexChars;
import static org.sonar.server.es.IndexType.FIELD_INDEX_TYPE;
import static org.sonar.server.es.searchrequest.TopAggregationDefinition.NON_STICKY;
@@ -145,6 +148,7 @@ import static org.sonar.server.issue.index.IssueIndex.Facet.IMPACT_SOFTWARE_QUAL
import static org.sonar.server.issue.index.IssueIndex.Facet.ISSUE_STATUSES;
import static org.sonar.server.issue.index.IssueIndex.Facet.LANGUAGES;
import static org.sonar.server.issue.index.IssueIndex.Facet.OWASP_ASVS_40;
+import static org.sonar.server.issue.index.IssueIndex.Facet.OWASP_MOBILE_TOP_10_2024;
import static org.sonar.server.issue.index.IssueIndex.Facet.OWASP_TOP_10;
import static org.sonar.server.issue.index.IssueIndex.Facet.OWASP_TOP_10_2021;
import static org.sonar.server.issue.index.IssueIndex.Facet.PCI_DSS_32;
@@ -185,6 +189,7 @@ import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_LINE
import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_NEW_CODE_REFERENCE;
import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_NEW_STATUS;
import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_OWASP_ASVS_40;
+import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_OWASP_MOBILE_TOP_10_2024;
import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_OWASP_TOP_10;
import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_OWASP_TOP_10_2021;
import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_PCI_DSS_32;
@@ -224,6 +229,7 @@ import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_IMPACT_SOFT
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE_STATUSES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_LANGUAGES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_ASVS_40;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_MOBILE_TOP_10_2024;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_TOP_10;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_TOP_10_2021;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PCI_DSS_32;
@@ -256,7 +262,6 @@ public class IssueIndex {
private static final String ISSUES_WITH_SECURITY_IMPACT = "issues_with_security_impact";
private static final String AGG_IMPACT_SEVERITIES = "impact_severities";
private static final String AGG_TO_REVIEW_SECURITY_HOTSPOTS = "toReviewSecurityHotspots";
- private static final String AGG_IN_REVIEW_SECURITY_HOTSPOTS = "inReviewSecurityHotspots";
private static final String AGG_REVIEWED_SECURITY_HOTSPOTS = "reviewedSecurityHotspots";
private static final String AGG_DISTRIBUTION = "distribution";
private static final BoolQueryBuilder NON_RESOLVED_VULNERABILITIES_FILTER = boolQuery()
@@ -266,10 +271,6 @@ public class IssueIndex {
.filter(nestedQuery(FIELD_ISSUE_IMPACTS, termsQuery(FIELD_ISSUE_IMPACT_SOFTWARE_QUALITY, SoftwareQuality.SECURITY.name()),
ScoreMode.Avg))
.mustNot(existsQuery(FIELD_ISSUE_RESOLUTION));
- private static final BoolQueryBuilder IN_REVIEW_HOTSPOTS_FILTER = boolQuery()
- .filter(termQuery(FIELD_ISSUE_TYPE, SECURITY_HOTSPOT.name()))
- .filter(termQuery(FIELD_ISSUE_STATUS, Issue.STATUS_IN_REVIEW))
- .mustNot(existsQuery(FIELD_ISSUE_RESOLUTION));
private static final BoolQueryBuilder TO_REVIEW_HOTSPOTS_FILTER = boolQuery()
.filter(termQuery(FIELD_ISSUE_TYPE, SECURITY_HOTSPOT.name()))
.filter(termQuery(FIELD_ISSUE_STATUS, Issue.STATUS_TO_REVIEW))
@@ -313,6 +314,7 @@ public class IssueIndex {
PCI_DSS_32(PARAM_PCI_DSS_32, FIELD_ISSUE_PCI_DSS_32, STICKY, DEFAULT_FACET_SIZE),
PCI_DSS_40(PARAM_PCI_DSS_40, FIELD_ISSUE_PCI_DSS_40, STICKY, DEFAULT_FACET_SIZE),
OWASP_ASVS_40(PARAM_OWASP_ASVS_40, FIELD_ISSUE_OWASP_ASVS_40, STICKY, DEFAULT_FACET_SIZE),
+ OWASP_MOBILE_TOP_10_2024(PARAM_OWASP_MOBILE_TOP_10_2024, FIELD_ISSUE_OWASP_MOBILE_TOP_10_2024, STICKY, DEFAULT_FACET_SIZE),
OWASP_TOP_10(PARAM_OWASP_TOP_10, FIELD_ISSUE_OWASP_TOP_10, STICKY, DEFAULT_FACET_SIZE),
OWASP_TOP_10_2021(PARAM_OWASP_TOP_10_2021, FIELD_ISSUE_OWASP_TOP_10_2021, STICKY, DEFAULT_FACET_SIZE),
STIG_ASD_V5R3(PARAM_STIG_ASD_V5R3, FIELD_ISSUE_STIG_ASD_V5R3, STICKY, DEFAULT_FACET_SIZE),
@@ -531,6 +533,7 @@ public class IssueIndex {
addSecurityCategoryPrefixFilter(FIELD_ISSUE_PCI_DSS_32, PCI_DSS_32, query.pciDss32(), filters);
addSecurityCategoryPrefixFilter(FIELD_ISSUE_PCI_DSS_40, PCI_DSS_40, query.pciDss40(), filters);
addOwaspAsvsFilter(FIELD_ISSUE_OWASP_ASVS_40, OWASP_ASVS_40, query, filters);
+ addSecurityCategoryFilter(FIELD_ISSUE_OWASP_MOBILE_TOP_10_2024, OWASP_MOBILE_TOP_10_2024, query.owaspMobileTop10For2024(), filters);
addSecurityCategoryFilter(FIELD_ISSUE_OWASP_TOP_10, OWASP_TOP_10, query.owaspTop10(), filters);
addSecurityCategoryFilter(FIELD_ISSUE_OWASP_TOP_10_2021, OWASP_TOP_10_2021, query.owaspTop10For2021(), filters);
addSecurityCategoryFilter(FIELD_ISSUE_STIG_ASD_V5R3, STIG_ASD_V5R3, query.stigAsdV5R3(), filters);
@@ -543,9 +546,8 @@ public class IssueIndex {
addImpactFilters(query, filters);
addComponentRelatedFilters(query, filters);
addDatesFilter(filters, query);
- addCreatedAfterByProjectsFilter(filters, query);
+ addNewCodeByProjectsFilter(filters, query);
addNewCodeReferenceFilter(filters, query);
- addNewCodeReferenceFilterByProjectsFilter(filters, query);
return filters;
}
@@ -869,33 +871,23 @@ public class IssueIndex {
if (newCodeOnReference != null) {
filters.addFilter(
FIELD_ISSUE_NEW_CODE_REFERENCE, new SimpleFieldFilterScope(FIELD_ISSUE_NEW_CODE_REFERENCE),
- termQuery(FIELD_ISSUE_NEW_CODE_REFERENCE, true));
- }
- }
-
- private static void addNewCodeReferenceFilterByProjectsFilter(AllFilters allFilters, IssueQuery query) {
- Collection<String> newCodeOnReferenceByProjectUuids = query.newCodeOnReferenceByProjectUuids();
- BoolQueryBuilder boolQueryBuilder = boolQuery();
-
- if (!newCodeOnReferenceByProjectUuids.isEmpty()) {
-
- newCodeOnReferenceByProjectUuids.forEach(projectOrProjectBranchUuid -> boolQueryBuilder.should(boolQuery()
- .filter(termQuery(FIELD_ISSUE_BRANCH_UUID, projectOrProjectBranchUuid))
- .filter(termQuery(FIELD_ISSUE_NEW_CODE_REFERENCE, true))));
-
- allFilters.addFilter("__is_new_code_reference_by_project_uuids",
- new SimpleFieldFilterScope("newCodeReferenceByProjectUuids"), boolQueryBuilder);
+ termQuery(FIELD_ISSUE_NEW_CODE_REFERENCE, newCodeOnReference));
}
}
- private static void addCreatedAfterByProjectsFilter(AllFilters allFilters, IssueQuery query) {
+ private static void addNewCodeByProjectsFilter(AllFilters allFilters, IssueQuery query) {
Map<String, PeriodStart> createdAfterByProjectUuids = query.createdAfterByProjectUuids();
BoolQueryBuilder boolQueryBuilder = boolQuery();
createdAfterByProjectUuids.forEach((projectOrProjectBranchUuid, createdAfterDate) -> boolQueryBuilder.should(boolQuery()
.filter(termQuery(FIELD_ISSUE_BRANCH_UUID, projectOrProjectBranchUuid))
.filter(rangeQuery(FIELD_ISSUE_FUNC_CREATED_AT).from(createdAfterDate.date().getTime(), createdAfterDate.inclusive()))));
- allFilters.addFilter("__created_after_by_project_uuids", new SimpleFieldFilterScope("createdAfterByProjectUuids"), boolQueryBuilder);
+ Collection<String> newCodeOnReferenceByProjectUuids = query.newCodeOnReferenceByProjectUuids();
+ newCodeOnReferenceByProjectUuids.forEach(projectOrProjectBranchUuid -> boolQueryBuilder.should(boolQuery()
+ .filter(termQuery(FIELD_ISSUE_BRANCH_UUID, projectOrProjectBranchUuid))
+ .filter(termQuery(FIELD_ISSUE_NEW_CODE_REFERENCE, true))));
+
+ allFilters.addFilter("__new_code_by_project_uuids", new SimpleFieldFilterScope("newCodeByProjectUuids"), boolQueryBuilder);
}
private void validateCreationDateBounds(@Nullable Date createdBefore, @Nullable Date createdAfter) {
@@ -925,6 +917,7 @@ public class IssueIndex {
addSecurityCategoryFacetIfNeeded(PARAM_PCI_DSS_32, PCI_DSS_32, options, aggregationHelper, esRequest, query.pciDss32().toArray());
addSecurityCategoryFacetIfNeeded(PARAM_PCI_DSS_40, PCI_DSS_40, options, aggregationHelper, esRequest, query.pciDss40().toArray());
addSecurityCategoryFacetIfNeeded(PARAM_OWASP_ASVS_40, OWASP_ASVS_40, options, aggregationHelper, esRequest, query.owaspAsvs40().toArray());
+ addSecurityCategoryFacetIfNeeded(PARAM_OWASP_MOBILE_TOP_10_2024, OWASP_MOBILE_TOP_10_2024, options, aggregationHelper, esRequest, query.owaspMobileTop10For2024().toArray());
addSecurityCategoryFacetIfNeeded(PARAM_OWASP_TOP_10, OWASP_TOP_10, options, aggregationHelper, esRequest, query.owaspTop10().toArray());
addSecurityCategoryFacetIfNeeded(PARAM_OWASP_TOP_10_2021, OWASP_TOP_10_2021, options, aggregationHelper, esRequest, query.owaspTop10For2021().toArray());
addSecurityCategoryFacetIfNeeded(PARAM_STIG_ASD_V5R3, STIG_ASD_V5R3, options, aggregationHelper, esRequest, query.stigAsdV5R3().toArray());
@@ -1301,7 +1294,7 @@ public class IssueIndex {
}
private static SecurityStandardCategoryStatistics emptyCweStatistics(String rule) {
- return new SecurityStandardCategoryStatistics(rule, 0, OptionalInt.of(1), 0, 0, 1, null, null);
+ return new SecurityStandardCategoryStatistics(rule, 0, OptionalInt.of(1), 0, 0, 1, null, null, Map.of());
}
public List<SecurityStandardCategoryStatistics> getSonarSourceReport(String projectUuid, boolean isViewOrApp, boolean includeCwe) {
@@ -1347,6 +1340,17 @@ public class IssueIndex {
return searchWithLevelDistribution(request, version.label(), Integer.toString(level));
}
+ public List<SecurityStandardCategoryStatistics> getOwaspMobileTop10Report(String projectUuid, boolean isViewOrApp, boolean includeCwe, OwaspMobileTop10Version version) {
+ SearchSourceBuilder request = prepareNonClosedVulnerabilitiesAndHotspotSearch(projectUuid, isViewOrApp);
+ IntStream.rangeClosed(1, 10).mapToObj(i -> "m" + i)
+ .forEach(owaspMobileCategory -> request.aggregation(
+ newSecurityReportSubAggregations(
+ AggregationBuilders.filter(owaspMobileCategory, boolQuery().filter(termQuery(version.prefix(), owaspMobileCategory))),
+ includeCwe,
+ null)));
+ return search(request, includeCwe, version.label());
+ }
+
public List<SecurityStandardCategoryStatistics> getOwaspTop10Report(String projectUuid, boolean isViewOrApp, boolean includeCwe, OwaspTop10Version version) {
SearchSourceBuilder request = prepareNonClosedVulnerabilitiesAndHotspotSearch(projectUuid, isViewOrApp);
IntStream.rangeClosed(1, 10).mapToObj(i -> "a" + i)
@@ -1446,10 +1450,11 @@ public class IssueIndex {
Aggregation severitiesAggregations =
((ParsedFilter) categoryBucket.getAggregations().get(AGG_VULNERABILITIES)).getAggregations().get(AGG_SEVERITIES);
- CountAndRating countAndRating = getCountAndRating(severitiesAggregations);
- long vulnerabilities = countAndRating.getCount();
+ SeverityAggregationDetails severityAggregationDetails = getSeverityDetails(severitiesAggregations);
+ long vulnerabilities = severityAggregationDetails.getCount();
// Worst severity having at least one issue
- OptionalInt severityRating = countAndRating.getRating();
+ OptionalInt severityRating = severityAggregationDetails.getRating();
+ Map<String, Long> severityDistribution = severityAggregationDetails.getDistribution();
long toReviewSecurityHotspots = ((ParsedValueCount) ((ParsedFilter) categoryBucket.getAggregations().get(AGG_TO_REVIEW_SECURITY_HOTSPOTS)).getAggregations().get(AGG_COUNT))
.getValue();
@@ -1460,32 +1465,39 @@ public class IssueIndex {
Integer securityReviewRating = computeRating(percent.orElse(null)).getIndex();
return new SecurityStandardCategoryStatistics(categoryName, vulnerabilities, severityRating, toReviewSecurityHotspots,
- reviewedSecurityHotspots, securityReviewRating, children, version);
+ reviewedSecurityHotspots, securityReviewRating, children, version, severityDistribution);
}
- private CountAndRating getCountAndRating(Aggregation severitiesAggregations) {
+ private SeverityAggregationDetails getSeverityDetails(Aggregation severitiesAggregations) {
+ List<? extends Terms.Bucket> severityBuckets;
+ long vulnerabilities;
+ OptionalInt severityRating;
if (isMQRMode()) {
- List<? extends Terms.Bucket> severityBuckets =
+ severityBuckets =
((ParsedStringTerms) ((ParsedFilter) ((ParsedNested) severitiesAggregations).getAggregations().get(ISSUES_WITH_SECURITY_IMPACT)).getAggregations().get(AGG_IMPACT_SEVERITIES)).getBuckets();
- long vulnerabilities =
+ vulnerabilities =
severityBuckets.stream().mapToLong(b -> ((ParsedValueCount) b.getAggregations().get(AGG_COUNT)).getValue()).sum();
// Worst severity having at least one issue
- OptionalInt severityRating = severityBuckets.stream()
+ severityRating = severityBuckets.stream()
.filter(b -> ((ParsedValueCount) b.getAggregations().get(AGG_COUNT)).getValue() != 0)
.mapToInt(b -> org.sonar.api.issue.impact.Severity.valueOf(b.getKeyAsString()).ordinal() + 1)
.max();
- return new CountAndRating(vulnerabilities, severityRating);
} else {
- List<? extends Terms.Bucket> severityBuckets = ((ParsedStringTerms) severitiesAggregations).getBuckets();
- long vulnerabilities =
+ severityBuckets = ((ParsedStringTerms) severitiesAggregations).getBuckets();
+ vulnerabilities =
severityBuckets.stream().mapToLong(b -> ((ParsedValueCount) b.getAggregations().get(AGG_COUNT)).getValue()).sum();
// Worst severity having at least one issue
- OptionalInt severityRating = severityBuckets.stream()
+ severityRating = severityBuckets.stream()
.filter(b -> ((ParsedValueCount) b.getAggregations().get(AGG_COUNT)).getValue() != 0)
.mapToInt(b -> Severity.ALL.indexOf(b.getKeyAsString()) + 1)
.max();
- return new CountAndRating(vulnerabilities, severityRating);
}
+ Map<String, Long> severityDistribution = severityBuckets.stream()
+ .collect(Collectors.toMap(
+ e -> e.getKeyAsString().toLowerCase(Locale.US),
+ MultiBucketsAggregation.Bucket::getDocCount
+ ));
+ return new SeverityAggregationDetails(vulnerabilities, severityRating, severityDistribution);
}
private AggregationBuilder newSecurityReportSubAggregations(AggregationBuilder categoriesAggs, String securityStandardVersionPrefix) {
@@ -1523,9 +1535,6 @@ public class IssueIndex {
.subAggregation(AggregationBuilders.filter(AGG_TO_REVIEW_SECURITY_HOTSPOTS, TO_REVIEW_HOTSPOTS_FILTER)
.subAggregation(
AggregationBuilders.count(AGG_COUNT).field(FIELD_ISSUE_KEY)))
- .subAggregation(AggregationBuilders.filter(AGG_IN_REVIEW_SECURITY_HOTSPOTS, IN_REVIEW_HOTSPOTS_FILTER)
- .subAggregation(
- AggregationBuilders.count(AGG_COUNT).field(FIELD_ISSUE_KEY)))
.subAggregation(AggregationBuilders.filter(AGG_REVIEWED_SECURITY_HOTSPOTS, REVIEWED_HOTSPOTS_FILTER)
.subAggregation(
AggregationBuilders.count(AGG_COUNT).field(FIELD_ISSUE_KEY)));
@@ -1569,7 +1578,6 @@ public class IssueIndex {
componentFilter
.should(getNonResolvedIssuesOrNonResolvedSecurityImpactQueryBuilderBasedOnMode())
.should(TO_REVIEW_HOTSPOTS_FILTER)
- .should(IN_REVIEW_HOTSPOTS_FILTER)
.should(REVIEWED_HOTSPOTS_FILTER)
.minimumShouldMatch(1))
.size(0);
@@ -1580,13 +1588,15 @@ public class IssueIndex {
}
- private static class CountAndRating {
+ private static class SeverityAggregationDetails {
private long count;
private OptionalInt rating;
+ private Map<String, Long> distribution;
- public CountAndRating(long count, OptionalInt rating) {
+ public SeverityAggregationDetails(long count, OptionalInt rating, Map<String, Long> distribution) {
this.count = count;
this.rating = rating;
+ this.distribution = distribution;
}
public long getCount() {
@@ -1596,5 +1606,9 @@ public class IssueIndex {
public OptionalInt getRating() {
return rating;
}
+
+ public Map<String, Long> getDistribution() {
+ return distribution;
+ }
}
}
diff --git a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java
index ddf06536376..9a20bac4775 100644
--- a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java
+++ b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java
@@ -74,6 +74,7 @@ public class IssueQuery {
private final Collection<String> languages;
private final Collection<String> tags;
private final Collection<String> types;
+ private final Collection<String> owaspMobileTop10For2024;
private final Collection<String> owaspTop10;
private final Collection<String> pciDss32;
private final Collection<String> pciDss40;
@@ -129,6 +130,7 @@ public class IssueQuery {
this.pciDss40 = defaultCollection(builder.pciDss40);
this.owaspAsvs40 = defaultCollection(builder.owaspAsvs40);
this.owaspAsvsLevel = builder.owaspAsvsLevel;
+ this.owaspMobileTop10For2024 = defaultCollection(builder.owaspMobileTop10For2024);
this.owaspTop10 = defaultCollection(builder.owaspTop10);
this.owaspTop10For2021 = defaultCollection(builder.owaspTop10For2021);
this.stigAsdV5R3 = defaultCollection(builder.stigAsdV5R3);
@@ -256,6 +258,10 @@ public class IssueQuery {
return Optional.ofNullable(owaspAsvsLevel);
}
+ public Collection<String> owaspMobileTop10For2024() {
+ return owaspMobileTop10For2024;
+ }
+
public Collection<String> owaspTop10() {
return owaspTop10;
}
@@ -402,6 +408,7 @@ public class IssueQuery {
private Collection<String> pciDss40;
private Collection<String> owaspAsvs40;
private Integer owaspAsvsLevel;
+ private Collection<String> owaspMobileTop10For2024;
private Collection<String> owaspTop10;
private Collection<String> owaspTop10For2021;
private Collection<String> stigAsdV5R3;
@@ -556,6 +563,11 @@ public class IssueQuery {
return this;
}
+ public Builder owaspMobileTop10For2024(@Nullable Collection<String> o) {
+ this.owaspMobileTop10For2024 = o;
+ return this;
+ }
+
public Builder owaspTop10(@Nullable Collection<String> o) {
this.owaspTop10 = o;
return this;
diff --git a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java
index e105cd9d175..39e1189e5a6 100644
--- a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java
+++ b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java
@@ -150,6 +150,7 @@ public class IssueQueryFactory {
.pciDss40(request.getPciDss40())
.owaspAsvs40(request.getOwaspAsvs40())
.owaspAsvsLevel(request.getOwaspAsvsLevel())
+ .owaspMobileTop10For2024(request.getOwaspMobileTop10For2024())
.owaspTop10(request.getOwaspTop10())
.owaspTop10For2021(request.getOwaspTop10For2021())
.stigAsdR5V3(request.getStigAsdV5R3())
@@ -167,6 +168,8 @@ public class IssueQueryFactory {
List<ComponentDto> allComponents = new ArrayList<>();
boolean effectiveOnComponentOnly = mergeDeprecatedComponentParameters(dbSession, request, allComponents);
addComponentParameters(builder, dbSession, effectiveOnComponentOnly, allComponents, request);
+ // SONAR-25108
+ unsetMainBranch(builder, issueKeys != null && !issueKeys.isEmpty(), allComponents, request);
setCreatedAfterFromRequest(dbSession, builder, request, allComponents, timeZone);
String sort = request.getSort();
@@ -266,18 +269,22 @@ public class IssueQueryFactory {
ComponentDto component = componentUuids.iterator().next();
if (!QUALIFIERS_WITHOUT_LEAK_PERIOD.contains(component.qualifier()) && request.getPullRequest() == null) {
- Optional<SnapshotDto> snapshot = getLastAnalysis(dbSession, component);
- if (!snapshot.isEmpty() && isLastAnalysisFromReAnalyzedReferenceBranch(dbSession, snapshot.get())) {
- builder.newCodeOnReference(true);
- return;
- }
- // if last analysis has no period date, then no issue should be considered new.
- Date createdAfterFromSnapshot = findCreatedAfterFromComponentUuid(snapshot);
- setCreatedAfterFromDates(builder, createdAfterFromSnapshot, null, false);
+ setInNewCodePeriod(dbSession, builder, component.uuid());
}
}
}
+ private void setInNewCodePeriod(DbSession dbSession, IssueQuery.Builder builder, String componentUuid) {
+ Optional<SnapshotDto> snapshot = getLastAnalysis(dbSession, componentUuid);
+ if (!snapshot.isEmpty() && isLastAnalysisFromReAnalyzedReferenceBranch(dbSession, snapshot.get())) {
+ builder.newCodeOnReference(true);
+ return;
+ }
+ // if last analysis has no period date, then no issue should be considered new.
+ Date createdAfterFromSnapshot = findCreatedAfterFromComponentUuid(snapshot);
+ setCreatedAfterFromDates(builder, createdAfterFromSnapshot, null, false);
+ }
+
private static boolean notInNewCodePeriod(SearchRequest request) {
Boolean inNewCodePeriod = request.getInNewCodePeriod();
inNewCodePeriod = Boolean.TRUE.equals(inNewCodePeriod);
@@ -298,8 +305,8 @@ public class IssueQueryFactory {
.isPresent();
}
- private Optional<SnapshotDto> getLastAnalysis(DbSession dbSession, ComponentDto component) {
- return dbClient.snapshotDao().selectLastAnalysisByComponentUuid(dbSession, component.uuid());
+ private Optional<SnapshotDto> getLastAnalysis(DbSession dbSession, String componentUuid) {
+ return dbClient.snapshotDao().selectLastAnalysisByComponentUuid(dbSession, componentUuid);
}
private List<SnapshotDto> getLastAnalysis(DbSession dbSession, Set<String> projectUuids) {
@@ -511,4 +518,12 @@ public class IssueQueryFactory {
builder.mainBranch(branchDto.isMain());
}
}
+
+ private static void unsetMainBranch(IssueQuery.Builder builder, boolean hasIssueKey, List<ComponentDto> components, SearchRequest request) {
+ var pullRequest = request.getPullRequest();
+ var branch = request.getBranch();
+ if ((components.isEmpty() || UNKNOWN_COMPONENT.equals(components.get(0)) || (pullRequest == null && branch == null)) && hasIssueKey) {
+ builder.mainBranch(null);
+ }
+ }
}
diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java
index 23daad4fed0..e0fa9ca61cc 100644
--- a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java
+++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java
@@ -456,7 +456,7 @@ class IssueIndexFiltersTest extends IssueIndexTestCommon {
}
@Test
- void filter_by_new_code_reference_branches() {
+ void filter_by_new_reference_branches() {
ComponentDto project1 = db.components().insertPrivateProject().getMainBranchComponent();
IssueDoc project1Issue1 = newDocForProject(project1).setIsNewCodeReference(true);
IssueDoc project1Issue2 = newDocForProject(project1).setIsNewCodeReference(false);
@@ -474,14 +474,20 @@ class IssueIndexFiltersTest extends IssueIndexTestCommon {
IssueDoc project2Branch1Issue1 = newDoc(project2Branch1, project2.uuid()).setIsNewCodeReference(false);
IssueDoc project2Branch1Issue2 = newDoc(project2Branch1, project2.uuid()).setIsNewCodeReference(true);
+ ComponentDto project3 = db.components().insertPrivateProject().getMainBranchComponent();
+ ComponentDto project3Branch1 = db.components().insertProjectBranch(project2);
+ IssueDoc project3Issue1 = newDoc(project3Branch1, project3.uuid()).setFuncCreationDate(new Date(1000L));
+ IssueDoc project3Issue2 = newDoc(project3Branch1, project3.uuid()).setFuncCreationDate(new Date(2000L));
+
indexIssues(project1Issue1, project1Issue2, project2Issue1, project2Issue2,
- project1Branch1Issue1, project1Branch1Issue2, project2Branch1Issue1, project2Branch1Issue2);
+ project1Branch1Issue1, project1Branch1Issue2, project2Branch1Issue1, project2Branch1Issue2, project3Issue1, project3Issue2);
// Search for issues of project 1 branch 1 and project 2 branch 1 that are new code on a branch using reference for new code
assertThatSearchReturnsOnly(IssueQuery.builder()
.mainBranch(false)
- .newCodeOnReferenceByProjectUuids(Set.of(project1Branch1.uuid(), project2Branch1.uuid())),
- project1Branch1Issue2.key(), project2Branch1Issue2.key());
+ .newCodeOnReferenceByProjectUuids(Set.of(project1Branch1.uuid(), project2Branch1.uuid()))
+ .createdAfterByProjectUuids(Map.of(project3Branch1.uuid(), new IssueQuery.PeriodStart(new Date(1500), false))),
+ project1Branch1Issue2.key(), project2Branch1Issue2.key(), project3Issue2.key());
}
@Test
diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexSecurityReportsTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexSecurityReportsTest.java
index 0eee21734c2..0c002a4e13c 100644
--- a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexSecurityReportsTest.java
+++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexSecurityReportsTest.java
@@ -30,7 +30,9 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.IssueStatus;
import org.sonar.api.rule.Severity;
+import org.sonar.api.server.rule.RulesDefinition;
import org.sonar.core.rule.RuleType;
import org.sonar.api.server.rule.RulesDefinition.StigVersion;
import org.sonar.db.component.ComponentDto;
@@ -51,6 +53,7 @@ import static org.sonar.api.issue.impact.Severity.MEDIUM;
import static org.sonar.api.issue.impact.SoftwareQuality.MAINTAINABILITY;
import static org.sonar.api.issue.impact.SoftwareQuality.SECURITY;
import static org.sonar.api.server.rule.RulesDefinition.OwaspAsvsVersion;
+import static org.sonar.api.server.rule.RulesDefinition.OwaspMobileTop10Version.Y2024;
import static org.sonar.api.server.rule.RulesDefinition.OwaspTop10Version.Y2017;
import static org.sonar.api.server.rule.RulesDefinition.OwaspTop10Version.Y2021;
import static org.sonar.api.server.rule.RulesDefinition.PciDssVersion;
@@ -357,6 +360,31 @@ class IssueIndexSecurityReportsTest extends IssueIndexTestCommon {
tuple("unknown", 0L, OptionalInt.empty(), 1L /* openhotspot1 */, 0L, 5));
}
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ void getOwaspMobileTop10For2024Report_aggregation_with_cwe(boolean mqrMode) {
+ doReturn(Optional.of(mqrMode)).when(config).getBoolean(MULTI_QUALITY_MODE_ENABLED);
+ List<SecurityStandardCategoryStatistics> owaspTop10Report = indexIssuesAndAssertOwaspMobile2024Report(true);
+
+ Map<String, List<SecurityStandardCategoryStatistics>> cweByOwasp = owaspTop10Report.stream()
+ .collect(Collectors.toMap(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getChildren));
+
+ assertThat(cweByOwasp.get("m1")).extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities,
+ SecurityStandardCategoryStatistics::getVulnerabilityRating, SecurityStandardCategoryStatistics::getToReviewSecurityHotspots,
+ SecurityStandardCategoryStatistics::getReviewedSecurityHotspots, SecurityStandardCategoryStatistics::getSecurityReviewRating)
+ .containsExactlyInAnyOrder(
+ tuple("123", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 1),
+ tuple("456", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 1),
+ tuple("unknown", 0L, OptionalInt.empty(), 1L /* openhotspot1 */, 0L, 5));
+ assertThat(cweByOwasp.get("m3")).extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities,
+ SecurityStandardCategoryStatistics::getVulnerabilityRating, SecurityStandardCategoryStatistics::getToReviewSecurityHotspots,
+ SecurityStandardCategoryStatistics::getReviewedSecurityHotspots, SecurityStandardCategoryStatistics::getSecurityReviewRating)
+ .containsExactlyInAnyOrder(
+ tuple("123", 2L /* openvul1, openvul2 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 1),
+ tuple("456", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 1),
+ tuple("unknown", 0L, OptionalInt.empty(), 1L /* openhotspot1 */, 0L, 5));
+ }
+
private List<SecurityStandardCategoryStatistics> indexIssuesAndAssertOwaspReport(boolean includeCwe) {
ComponentDto project = newPrivateProjectDto();
indexIssues(
@@ -545,6 +573,41 @@ class IssueIndexSecurityReportsTest extends IssueIndexTestCommon {
return owaspTop10Report;
}
+ private List<SecurityStandardCategoryStatistics> indexIssuesAndAssertOwaspMobile2024Report(boolean includeCwe) {
+ ComponentDto project = newPrivateProjectDto();
+ indexIssues(
+ newDocForProject("openvul1", project).setOwaspMobileTop10For2024(asList("m1", "m3")).setCwe(asList("123", "456")).setType(RuleType.VULNERABILITY).setImpacts(Map.of(SECURITY, MEDIUM)).setStatus(IssueStatus.OPEN.name())
+ .setSeverity(Severity.MAJOR),
+ newDocForProject("openvul2", project).setOwaspMobileTop10For2024(asList("m3", "m6")).setCwe(List.of("123")).setType(RuleType.VULNERABILITY).setImpacts(Map.of(SECURITY, LOW)).setStatus(IssueStatus.OPEN.name())
+ .setSeverity(Severity.MINOR),
+ newDocForProject("notowaspvul", project).setOwaspMobileTop10For2024(singletonList(UNKNOWN_STANDARD)).setType(RuleType.VULNERABILITY).setImpacts(Map.of(SECURITY, HIGH)).setStatus(IssueStatus.OPEN.toString())
+ .setSeverity(Severity.CRITICAL),
+ newDocForProject("toreviewhotspot1", project).setOwaspMobileTop10For2024(asList("m1", "m3")).setCwe(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT)
+ .setStatus(Issue.STATUS_TO_REVIEW),
+ newDocForProject("toreviewhotspot2", project).setOwaspMobileTop10For2024(asList("m3", "m6")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW),
+ newDocForProject("reviewedHotspot", project).setOwaspMobileTop10For2024(asList("m3", "m8")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED)
+ .setResolution(Issue.RESOLUTION_FIXED),
+ newDocForProject("notowasphotspot", project).setOwaspMobileTop10For2024(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW));
+
+ List<SecurityStandardCategoryStatistics> owaspTop10Report = underTest.getOwaspMobileTop10Report(project.uuid(), false, includeCwe, Y2024);
+ assertThat(owaspTop10Report)
+ .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities,
+ SecurityStandardCategoryStatistics::getVulnerabilityRating, SecurityStandardCategoryStatistics::getToReviewSecurityHotspots,
+ SecurityStandardCategoryStatistics::getReviewedSecurityHotspots, SecurityStandardCategoryStatistics::getSecurityReviewRating)
+ .containsExactlyInAnyOrder(
+ tuple("m1", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 1L /* toreviewhotspot1 */, 0L, 5),
+ tuple("m2", 0L, OptionalInt.empty(), 0L, 0L, 1),
+ tuple("m3", 2L /* openvul1,openvul2 */, OptionalInt.of(3)/* MAJOR = C */, 2L/* toreviewhotspot1,toreviewhotspot2 */, 1L /* reviewedHotspot */, 4),
+ tuple("m4", 0L, OptionalInt.empty(), 0L, 0L, 1),
+ tuple("m5", 0L, OptionalInt.empty(), 0L, 0L, 1),
+ tuple("m6", 1L /* openvul2 */, OptionalInt.of(2) /* MINOR = B */, 1L /* toreviewhotspot2 */, 0L, 5),
+ tuple("m7", 0L, OptionalInt.empty(), 0L, 0L, 1),
+ tuple("m8", 0L, OptionalInt.empty(), 0L, 1L /* reviewedHotspot */, 1),
+ tuple("m9", 0L, OptionalInt.empty(), 0L, 0L, 1),
+ tuple("m10", 0L, OptionalInt.empty(), 0L, 0L, 1));
+ return owaspTop10Report;
+ }
+
@ParameterizedTest
@ValueSource(booleans = {true, false})
void getPciDssReport_aggregation_on_portfolio(boolean mqrMode) {
diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java
index eb5c05b4ab6..c62293eb309 100644
--- a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java
+++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java
@@ -25,6 +25,7 @@ import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
+import java.util.List;
import java.util.Map;
import org.junit.Rule;
import org.junit.Test;
@@ -741,4 +742,84 @@ public class IssueQueryFactoryTest {
.hasMessageContaining("'unknown-date' cannot be parsed as either a date or date+time");
}
+ @Test
+ public void when_issue_keys_provided_with_no_component_should_not_have_main_branch() {
+ SearchRequest request = new SearchRequest()
+ .setIssues(List.of("issue-key-1", "issue-key-2"));
+
+ IssueQuery query = underTest.create(request);
+
+ assertThat(query.isMainBranch()).isNull();
+ }
+
+ @Test
+ public void when_issue_keys_and_component_provided_should_have_main_branch_set() {
+ // Create a project with main branch
+ ProjectData projectData = db.components().insertPrivateProject();
+ ComponentDto mainBranch = projectData.getMainBranchComponent();
+ String branchName = DEFAULT_MAIN_BRANCH_NAME;
+
+ // Request with issue keys and main branch
+ SearchRequest request = new SearchRequest()
+ .setIssues(List.of("issue-key-1", "issue-key-2"))
+ .setComponentKeys(List.of(mainBranch.getKey()))
+ .setBranch(branchName);
+
+ IssueQuery query = underTest.create(request);
+
+ // Should unset main branch since issue keys are provided
+ assertThat(query.isMainBranch()).isTrue();
+ assertThat(query.branchUuid()).isEqualTo(mainBranch.uuid());
+ }
+
+ @Test
+ public void when_no_issue_keys_provided_should_default_to_main_branch() {
+ ProjectData projectData = db.components().insertPrivateProject();
+ ComponentDto mainBranch = projectData.getMainBranchComponent();
+ String branchName = DEFAULT_MAIN_BRANCH_NAME;
+
+ SearchRequest request = new SearchRequest()
+ .setComponentKeys(List.of(mainBranch.getKey()))
+ .setBranch(branchName);
+
+ IssueQuery query = underTest.create(request);
+
+ assertThat(query.isMainBranch()).isTrue();
+ assertThat(query.branchUuid()).isEqualTo(mainBranch.uuid());
+ }
+
+ @Test
+ public void when_component_is_non_main_branch_should_not_default_to_main_branch() {
+ ProjectData projectData = db.components().insertPrivateProject();
+ ComponentDto mainBranch = projectData.getMainBranchComponent();
+ String branchName = "feature-branch";
+ ComponentDto branch = db.components().insertProjectBranch(mainBranch, b -> b.setKey(branchName));
+
+ SearchRequest request = new SearchRequest()
+ .setComponentKeys(List.of(branch.getKey()))
+ .setBranch(branchName);
+
+ IssueQuery query = underTest.create(request);
+
+ assertThat(query.isMainBranch()).isFalse();
+ assertThat(query.branchUuid()).isEqualTo(branch.uuid());
+ }
+
+ @Test
+ public void when_empty_issue_keys_list_provided_should_default_to_main_branch() {
+ ProjectData projectData = db.components().insertPrivateProject();
+ ComponentDto mainBranch = projectData.getMainBranchComponent();
+ String branchName = DEFAULT_MAIN_BRANCH_NAME;
+
+ SearchRequest request = new SearchRequest()
+ .setIssues(Collections.emptyList())
+ .setComponentKeys(List.of(mainBranch.getKey()))
+ .setBranch(branchName);
+
+ IssueQuery query = underTest.create(request);
+
+ assertThat(query.isMainBranch()).isTrue();
+ assertThat(query.branchUuid()).isEqualTo(mainBranch.uuid());
+ }
+
}
diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java
index c8b9870d547..979e493da47 100644
--- a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java
+++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java
@@ -128,6 +128,15 @@ class IssueQueryTest {
}
@Test
+ void build_owasp_mobile_query() {
+ IssueQuery query = IssueQuery.builder()
+ .owaspMobileTop10For2024(List.of("m5", "m6"))
+ .build();
+
+ assertThat(query.owaspMobileTop10For2024()).containsOnly("m5", "m6");
+ }
+
+ @Test
void build_stig_query() {
IssueQuery query = IssueQuery.builder()
.stigAsdR5V3(List.of("V-222400", "V-222401"))
diff --git a/server/sonar-webserver-monitoring/build.gradle b/server/sonar-webserver-monitoring/build.gradle
index 84fb86e4fb2..d5fe638db08 100644
--- a/server/sonar-webserver-monitoring/build.gradle
+++ b/server/sonar-webserver-monitoring/build.gradle
@@ -9,6 +9,17 @@ dependencies {
testImplementation 'junit:junit'
testImplementation 'org.assertj:assertj-core'
+ testImplementation 'org.awaitility:awaitility'
+ testImplementation 'org.junit.jupiter:junit-jupiter-api'
+ testImplementation 'org.junit.jupiter:junit-jupiter-params'
testImplementation 'org.mockito:mockito-core'
testImplementation 'org.sonarsource.api.plugin:sonar-plugin-api-test-fixtures'
+
+ testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
+ testRuntimeOnly 'org.junit.vintage:junit-vintage-engine'
+}
+
+test {
+ // Enabling the JUnit Platform (see https://github.com/junit-team/junit5-samples/tree/master/junit5-migration-gradle)
+ useJUnitPlatform()
}
diff --git a/server/sonar-webserver-monitoring/src/main/java/org/sonar/server/monitoring/MainCollector.java b/server/sonar-webserver-monitoring/src/main/java/org/sonar/server/monitoring/MainCollector.java
index 02be4e82a0d..8f15ad207f1 100644
--- a/server/sonar-webserver-monitoring/src/main/java/org/sonar/server/monitoring/MainCollector.java
+++ b/server/sonar-webserver-monitoring/src/main/java/org/sonar/server/monitoring/MainCollector.java
@@ -21,14 +21,19 @@ package org.sonar.server.monitoring;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import java.util.Arrays;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.sonar.api.Startable;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
public class MainCollector implements Startable {
+ private static final Logger LOG = LoggerFactory.getLogger(MainCollector.class);
+
private final MonitoringTask[] monitoringTasks;
private ScheduledExecutorService scheduledExecutorService;
@@ -43,9 +48,14 @@ public class MainCollector implements Startable {
.setDaemon(true)
.setNameFormat(getClass().getCanonicalName() + "-thread-%d")
.build());
- for (MonitoringTask task : monitoringTasks) {
- scheduledExecutorService.scheduleWithFixedDelay(task, task.getDelay(), task.getPeriod(), MILLISECONDS);
- }
+ Arrays.stream(monitoringTasks).forEach(task ->
+ scheduledExecutorService.scheduleWithFixedDelay(() -> {
+ try {
+ task.run();
+ } catch (Exception e) {
+ LOG.warn("Error while executing monitoring task in {}: ", task.getClass().getSimpleName(), e);
+ }
+ }, task.getDelay(), task.getPeriod(), MILLISECONDS));
}
@Override
diff --git a/server/sonar-webserver-monitoring/src/main/java/org/sonar/server/monitoring/ServerMonitoringMetrics.java b/server/sonar-webserver-monitoring/src/main/java/org/sonar/server/monitoring/ServerMonitoringMetrics.java
index 9d6c9c9ebc5..39f91037d8c 100644
--- a/server/sonar-webserver-monitoring/src/main/java/org/sonar/server/monitoring/ServerMonitoringMetrics.java
+++ b/server/sonar-webserver-monitoring/src/main/java/org/sonar/server/monitoring/ServerMonitoringMetrics.java
@@ -37,6 +37,7 @@ public class ServerMonitoringMetrics {
private final Gauge cePendingTasksTotal;
private final Summary ceTasksRunningDuration;
+ private final Summary ceSystemTasksRunningDuration;
private final Gauge elasticsearchDiskSpaceFreeBytesGauge;
private final Gauge elasticSearchDiskSpaceTotalBytes;
@@ -80,6 +81,12 @@ public class ServerMonitoringMetrics {
.labelNames("task_type", "project_key")
.register();
+ ceSystemTasksRunningDuration = Summary.build()
+ .name("sonarqube_compute_engine_system_tasks_running_duration_seconds")
+ .help("Compute engine system task running time in seconds")
+ .labelNames("task_type")
+ .register();
+
computeEngineGauge = Gauge.build()
.name("sonarqube_health_compute_engine_status")
.help("Tells whether Compute Engine is up (healthy, ready to take tasks) or down. 1 for up, 0 for down")
@@ -165,8 +172,12 @@ public class ServerMonitoringMetrics {
cePendingTasksTotal.set(numberOfPendingTasks);
}
- public void observeComputeEngineTaskDuration(long durationInSeconds, String taskType, String projectKey) {
- ceTasksRunningDuration.labels(taskType, projectKey).observe(durationInSeconds);
+ public void observeComputeEngineTaskDuration(long durationInSeconds, String taskType, String label) {
+ ceTasksRunningDuration.labels(taskType, label).observe(durationInSeconds);
+ }
+
+ public void observeComputeEngineSystemTaskDuration(long durationInSeconds, String taskType) {
+ ceSystemTasksRunningDuration.labels(taskType).observe(durationInSeconds);
}
public void setComputeEngineStatusToGreen() {
diff --git a/server/sonar-webserver-monitoring/src/main/java/org/sonar/server/monitoring/ce/RecentTasksDurationTask.java b/server/sonar-webserver-monitoring/src/main/java/org/sonar/server/monitoring/ce/RecentTasksDurationTask.java
index 74d3dd85387..5a4c22d15d8 100644
--- a/server/sonar-webserver-monitoring/src/main/java/org/sonar/server/monitoring/ce/RecentTasksDurationTask.java
+++ b/server/sonar-webserver-monitoring/src/main/java/org/sonar/server/monitoring/ce/RecentTasksDurationTask.java
@@ -22,11 +22,12 @@ package org.sonar.server.monitoring.ce;
import java.util.Collection;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.stream.Collectors;
-import org.sonar.api.config.Configuration;
-import org.sonar.api.utils.System2;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.utils.System2;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.ce.CeActivityDto;
@@ -56,6 +57,7 @@ public class RecentTasksDurationTask extends ComputeEngineMetricsTask {
Collection<String> entityUuids = recentSuccessfulTasks.stream()
.map(CeActivityDto::getEntityUuid)
+ .filter(Objects::nonNull)
.toList();
List<EntityDto> entities = dbClient.entityDao().selectByUuids(dbSession, entityUuids);
Map<String, String> entityUuidAndKeys = entities.stream()
@@ -75,20 +77,22 @@ public class RecentTasksDurationTask extends ComputeEngineMetricsTask {
private void reportObservedDurationForTasks(List<CeActivityDto> tasks, Map<String, String> entityUuidAndKeys) {
for (CeActivityDto task : tasks) {
- String mainComponentUuid = task.getEntityUuid();
+ String entityUuid = task.getEntityUuid();
Long executionTimeMs = task.getExecutionTimeMs();
try {
- requireNonNull(mainComponentUuid);
requireNonNull(executionTimeMs);
- String mainComponentKey = entityUuidAndKeys.get(mainComponentUuid);
- requireNonNull(mainComponentKey);
-
- metrics.observeComputeEngineTaskDuration(executionTimeMs, task.getTaskType(), mainComponentKey);
+ if (entityUuid != null) {
+ String label = entityUuidAndKeys.get(entityUuid);
+ requireNonNull(label);
+ metrics.observeComputeEngineTaskDuration(executionTimeMs, task.getTaskType(), label);
+ } else {
+ metrics.observeComputeEngineSystemTaskDuration(executionTimeMs, task.getTaskType());
+ }
} catch (RuntimeException e) {
- LOGGER.warn("Can't report metric data for a CE task with component uuid " + mainComponentUuid, e);
+ LOGGER.warn("Can't report metric data for a CE task with entity uuid " + entityUuid, e);
}
}
-
}
+
}
diff --git a/server/sonar-webserver-monitoring/src/test/java/org/sonar/server/monitoring/MainCollectorTest.java b/server/sonar-webserver-monitoring/src/test/java/org/sonar/server/monitoring/MainCollectorTest.java
index 1ff72da45b3..e22d28cf2ab 100644
--- a/server/sonar-webserver-monitoring/src/test/java/org/sonar/server/monitoring/MainCollectorTest.java
+++ b/server/sonar-webserver-monitoring/src/test/java/org/sonar/server/monitoring/MainCollectorTest.java
@@ -19,26 +19,34 @@
*/
package org.sonar.server.monitoring;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.api.testfixtures.log.LogTesterJUnit5;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-public class MainCollectorTest {
+class MainCollectorTest {
+
+ @RegisterExtension
+ private final LogTesterJUnit5 logTester = new LogTesterJUnit5();
private final MonitoringTask task1 = mock(MonitoringTask.class);
private final MonitoringTask task2 = mock(MonitoringTask.class);
private MainCollector underTest;
- @Before
- public void before() {
+ @BeforeEach
+ void before() {
MonitoringTask[] tasks = {task1, task2};
for(MonitoringTask task : tasks) {
when(task.getDelay()).thenReturn(1L);
@@ -47,13 +55,13 @@ public class MainCollectorTest {
underTest = new MainCollector(tasks);
}
- @After
- public void stop() {
+ @AfterEach
+ void stop() {
underTest.stop();
}
@Test
- public void startAndStop_executorServiceIsShutdown() {
+ void startAndStop_executorServiceIsShutdown() {
underTest.start();
assertFalse(underTest.getScheduledExecutorService().isShutdown());
@@ -64,7 +72,7 @@ public class MainCollectorTest {
}
@Test
- public void start_givenTwoTasks_callsGetsDelayAndPeriodFromTasks() {
+ void start_givenTwoTasks_callsGetsDelayAndPeriodFromTasks() {
underTest.start();
verify(task1, times(1)).getDelay();
@@ -72,4 +80,17 @@ public class MainCollectorTest {
verify(task2, times(1)).getDelay();
verify(task2, times(1)).getPeriod();
}
+
+ @Test
+ void start_logsExceptionAsWarn_whenTaskThrowsException() {
+ doThrow(new RuntimeException()).when(task1).run();
+
+ underTest.start();
+
+ String expectedLogMessage = "Error while executing monitoring task in " + task1.getClass().getSimpleName();
+ await()
+ .atMost(1, TimeUnit.SECONDS)
+ .until(() -> logTester.logs().stream()
+ .anyMatch(log -> log.contains(expectedLogMessage)));
+ }
}
diff --git a/server/sonar-webserver-monitoring/src/test/java/org/sonar/server/monitoring/ServerMonitoringMetricsTest.java b/server/sonar-webserver-monitoring/src/test/java/org/sonar/server/monitoring/ServerMonitoringMetricsTest.java
index 8b0cbd624f0..ecacceefccb 100644
--- a/server/sonar-webserver-monitoring/src/test/java/org/sonar/server/monitoring/ServerMonitoringMetricsTest.java
+++ b/server/sonar-webserver-monitoring/src/test/java/org/sonar/server/monitoring/ServerMonitoringMetricsTest.java
@@ -158,6 +158,18 @@ public class ServerMonitoringMetricsTest {
labelNames, labelValues)).isEqualTo(10);
}
+ @Test
+ public void observeComputeEngineSystemTaskDurationTest() {
+ ServerMonitoringMetrics metrics = new ServerMonitoringMetrics();
+ String[] labelNames = {"task_type"};
+ String[] labelValues = {"AUDIT_PURGE"};
+
+ metrics.observeComputeEngineSystemTaskDuration(10, labelValues[0]);
+
+ assertThat(CollectorRegistry.defaultRegistry.getSampleValue("sonarqube_compute_engine_system_tasks_running_duration_seconds_sum",
+ labelNames, labelValues)).isEqualTo(10);
+ }
+
private int sizeOfDefaultRegistry() {
Enumeration<Collector.MetricFamilySamples> metrics = CollectorRegistry.defaultRegistry.metricFamilySamples();
return Collections.list(metrics).size();
diff --git a/server/sonar-webserver-monitoring/src/test/java/org/sonar/server/monitoring/ce/RecentTasksDurationTaskTest.java b/server/sonar-webserver-monitoring/src/test/java/org/sonar/server/monitoring/ce/RecentTasksDurationTaskTest.java
index 5b5fc0a3570..7fdbfaa9dee 100644
--- a/server/sonar-webserver-monitoring/src/test/java/org/sonar/server/monitoring/ce/RecentTasksDurationTaskTest.java
+++ b/server/sonar-webserver-monitoring/src/test/java/org/sonar/server/monitoring/ce/RecentTasksDurationTaskTest.java
@@ -86,6 +86,22 @@ public class RecentTasksDurationTaskTest {
}
@Test
+ public void run_given1TaskWithEntityUuidAnd1Without_observeDurationFor2Tasks() {
+ RecentTasksDurationTask task = new RecentTasksDurationTask(dbClient, metrics, config, system);
+ List<CeActivityDto> recentTasks = createTasks(2, 0);
+
+ recentTasks.get(0).setEntityUuid(null);
+
+ when(entityDao.selectByUuids(any(), any())).thenReturn(createEntityDtos(1));
+ when(ceActivityDao.selectNewerThan(any(), anyLong())).thenReturn(recentTasks);
+
+ task.run();
+
+ verify(metrics, times(1)).observeComputeEngineTaskDuration(anyLong(), any(), any());
+ verify(metrics, times(1)).observeComputeEngineSystemTaskDuration(anyLong(), any());
+ }
+
+ @Test
public void run_givenNullExecutionTime_dontReportMetricData() {
RecentTasksDurationTask task = new RecentTasksDurationTask(dbClient, metrics, config, system);
List<CeActivityDto> recentTasks = createTasks(1, 0);
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/controller/EmailConfigurationController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/controller/EmailConfigurationController.java
index a62ec04c33a..8fb0f5e05a3 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/controller/EmailConfigurationController.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/controller/EmailConfigurationController.java
@@ -30,7 +30,6 @@ import org.sonar.server.v2.api.email.config.request.EmailConfigurationUpdateRest
import org.sonar.server.v2.api.email.config.resource.EmailConfigurationResource;
import org.sonar.server.v2.api.email.config.response.EmailConfigurationSearchRestResponse;
import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
@@ -44,6 +43,7 @@ import org.springframework.web.bind.annotation.RestController;
import static org.sonar.server.v2.WebApiEndpoints.EMAIL_CONFIGURATION_ENDPOINT;
import static org.sonar.server.v2.WebApiEndpoints.INTERNAL;
import static org.sonar.server.v2.WebApiEndpoints.JSON_MERGE_PATCH_CONTENT_TYPE;
+import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@RequestMapping(EMAIL_CONFIGURATION_ENDPOINT)
@RestController
@@ -58,7 +58,7 @@ public interface EmailConfigurationController {
extensions = @Extension(properties = {@ExtensionProperty(name = INTERNAL, value = "true")}))
EmailConfigurationResource createEmailConfiguration(@Valid @RequestBody EmailConfigurationCreateRestRequest createRequest);
- @GetMapping(path = "/{id}")
+ @GetMapping(path = "/{id}", produces = APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "Fetch an email configuration", description = """
Fetch a Email configuration. Requires 'Administer System' permission.
@@ -76,7 +76,7 @@ public interface EmailConfigurationController {
extensions = @Extension(properties = {@ExtensionProperty(name = INTERNAL, value = "true")}))
EmailConfigurationSearchRestResponse searchEmailConfigurations();
- @PatchMapping(path = "/{id}", consumes = JSON_MERGE_PATCH_CONTENT_TYPE, produces = MediaType.APPLICATION_JSON_VALUE)
+ @PatchMapping(path = "/{id}", consumes = JSON_MERGE_PATCH_CONTENT_TYPE, produces = APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "Update an email configuration", description = """
Update an email configuration. Requires 'Administer System' permission.
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/controller/GithubConfigurationController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/controller/GithubConfigurationController.java
index dda8fdbc4b7..d246cb4af26 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/controller/GithubConfigurationController.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/controller/GithubConfigurationController.java
@@ -30,7 +30,6 @@ import org.sonar.server.v2.api.github.config.request.GithubConfigurationUpdateRe
import org.sonar.server.v2.api.github.config.resource.GithubConfigurationResource;
import org.sonar.server.v2.api.github.config.response.GithubConfigurationSearchRestResponse;
import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
@@ -44,12 +43,13 @@ import org.springframework.web.bind.annotation.RestController;
import static org.sonar.server.v2.WebApiEndpoints.GITHUB_CONFIGURATION_ENDPOINT;
import static org.sonar.server.v2.WebApiEndpoints.INTERNAL;
import static org.sonar.server.v2.WebApiEndpoints.JSON_MERGE_PATCH_CONTENT_TYPE;
+import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@RequestMapping(GITHUB_CONFIGURATION_ENDPOINT)
@RestController
public interface GithubConfigurationController {
- @GetMapping(path = "/{id}")
+ @GetMapping(path = "/{id}", produces = APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "Fetch a GitHub configuration", description = """
Fetch a GitHub configuration. Requires 'Administer System' permission.
@@ -67,7 +67,7 @@ public interface GithubConfigurationController {
extensions = @Extension(properties = {@ExtensionProperty(name = INTERNAL, value = "true")}))
GithubConfigurationSearchRestResponse searchGithubConfiguration();
- @PatchMapping(path = "/{id}", consumes = JSON_MERGE_PATCH_CONTENT_TYPE, produces = MediaType.APPLICATION_JSON_VALUE)
+ @PatchMapping(path = "/{id}", consumes = JSON_MERGE_PATCH_CONTENT_TYPE, produces = APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "Update a GitHub configuration", description = """
Update a GitHub configuration. Requires 'Administer System' permission.
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/controller/GitlabConfigurationController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/controller/GitlabConfigurationController.java
index a00f7cd0891..b3ebde2230f 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/controller/GitlabConfigurationController.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/controller/GitlabConfigurationController.java
@@ -30,7 +30,6 @@ import org.sonar.server.v2.api.gitlab.config.request.GitlabConfigurationUpdateRe
import org.sonar.server.v2.api.gitlab.config.resource.GitlabConfigurationResource;
import org.sonar.server.v2.api.gitlab.config.response.GitlabConfigurationSearchRestResponse;
import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
@@ -44,12 +43,13 @@ import org.springframework.web.bind.annotation.RestController;
import static org.sonar.server.v2.WebApiEndpoints.GITLAB_CONFIGURATION_ENDPOINT;
import static org.sonar.server.v2.WebApiEndpoints.INTERNAL;
import static org.sonar.server.v2.WebApiEndpoints.JSON_MERGE_PATCH_CONTENT_TYPE;
+import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@RequestMapping(GITLAB_CONFIGURATION_ENDPOINT)
@RestController
public interface GitlabConfigurationController {
- @GetMapping(path = "/{id}")
+ @GetMapping(path = "/{id}", produces = APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "Fetch a GitLab configuration", description = """
Fetch a GitLab configuration. Requires 'Administer System' permission.
@@ -67,7 +67,7 @@ public interface GitlabConfigurationController {
extensions = @Extension(properties = {@ExtensionProperty(name = INTERNAL, value = "true")}))
GitlabConfigurationSearchRestResponse searchGitlabConfiguration();
- @PatchMapping(path = "/{id}", consumes = JSON_MERGE_PATCH_CONTENT_TYPE, produces = MediaType.APPLICATION_JSON_VALUE)
+ @PatchMapping(path = "/{id}", consumes = JSON_MERGE_PATCH_CONTENT_TYPE, produces = APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "Update a Gitlab configuration", description = """
Update a Gitlab configuration. Requires 'Administer System' permission.
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/DefaultGroupController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/DefaultGroupController.java
index 1b5b17e5e6a..9727e3b7d04 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/DefaultGroupController.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/DefaultGroupController.java
@@ -20,6 +20,7 @@
package org.sonar.server.v2.api.group.controller;
import java.util.List;
+import org.jetbrains.annotations.Nullable;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.server.common.SearchResults;
@@ -53,10 +54,11 @@ public class DefaultGroupController implements GroupController {
}
@Override
- public GroupsSearchRestResponse search(GroupsSearchRestRequest groupsSearchRestRequest, RestPage restPage) {
+ public GroupsSearchRestResponse search(GroupsSearchRestRequest groupsSearchRestRequest, @Nullable String excludedUserId, RestPage restPage) {
userSession.checkLoggedIn().checkIsSystemAdministrator();
try (DbSession dbSession = dbClient.openSession(false)) {
- GroupSearchRequest groupSearchRequest = new GroupSearchRequest(groupsSearchRestRequest.q(), groupsSearchRestRequest.managed(), restPage.pageIndex(), restPage.pageSize());
+ GroupSearchRequest groupSearchRequest = new GroupSearchRequest(groupsSearchRestRequest.q(), groupsSearchRestRequest.managed(), groupsSearchRestRequest.userId(),
+ excludedUserId, restPage.pageIndex(), restPage.pageSize());
SearchResults<GroupInformation> searchResults = groupService.search(dbSession, groupSearchRequest);
List<GroupRestResponse> groupRestResponses = toGroupRestResponses(searchResults);
return new GroupsSearchRestResponse(groupRestResponses, new PageRestResponse(restPage.pageIndex(), restPage.pageSize(), searchResults.total()));
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/GroupController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/GroupController.java
index 666ba5e049e..66bda515c44 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/GroupController.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/GroupController.java
@@ -22,7 +22,11 @@ package org.sonar.server.v2.api.group.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
+import io.swagger.v3.oas.annotations.extensions.Extension;
+import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
+import javax.annotation.Nullable;
import org.sonar.server.v2.api.group.request.GroupCreateRestRequest;
import org.sonar.server.v2.api.group.request.GroupUpdateRestRequest;
import org.sonar.server.v2.api.group.request.GroupsSearchRestRequest;
@@ -31,7 +35,6 @@ import org.sonar.server.v2.api.group.response.GroupsSearchRestResponse;
import org.sonar.server.v2.api.model.RestPage;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
@@ -39,17 +42,19 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import static org.sonar.server.v2.WebApiEndpoints.GROUPS_ENDPOINT;
import static org.sonar.server.v2.WebApiEndpoints.JSON_MERGE_PATCH_CONTENT_TYPE;
+import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@RequestMapping(GROUPS_ENDPOINT)
@RestController
public interface GroupController {
- @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
+ @GetMapping(produces = APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "Group search", description = """
Get the list of groups.
@@ -57,14 +62,16 @@ public interface GroupController {
""")
GroupsSearchRestResponse search(
@Valid @ParameterObject GroupsSearchRestRequest groupsSearchRestRequest,
+ @RequestParam(name = "userId!") @Nullable @Schema(description = "Find groups without this user. Only available for system administrators.",
+ extensions = @Extension(properties = {@ExtensionProperty(name = "internal", value = "true")}), hidden = true) String excludedUserId,
@Valid @ParameterObject RestPage restPage);
- @GetMapping(path = "/{id}")
+ @GetMapping(path = "/{id}", produces = APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "Fetch a single group", description = "Fetch a single group.")
GroupRestResponse fetchGroup(@PathVariable("id") @Parameter(description = "The id of the group to fetch.", required = true, in = ParameterIn.PATH) String id);
- @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+ @PostMapping(consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.CREATED)
@Operation(summary = "Create a new group", description = "Create a new group.")
GroupRestResponse create(@Valid @RequestBody GroupCreateRestRequest request);
@@ -74,7 +81,7 @@ public interface GroupController {
@Operation(summary = "Deletes a group", description = "Deletes a group.")
void deleteGroup(@PathVariable("id") @Parameter(description = "The ID of the group to delete.", required = true, in = ParameterIn.PATH) String id);
- @PatchMapping(path = "/{id}", consumes = JSON_MERGE_PATCH_CONTENT_TYPE, produces = MediaType.APPLICATION_JSON_VALUE)
+ @PatchMapping(path = "/{id}", consumes = JSON_MERGE_PATCH_CONTENT_TYPE, produces = APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "Update a group", description = """
Update a group name or description.
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/request/GroupsSearchRestRequest.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/request/GroupsSearchRestRequest.java
index ab704c8112b..d829f29716b 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/request/GroupsSearchRestRequest.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/request/GroupsSearchRestRequest.java
@@ -19,6 +19,8 @@
*/
package org.sonar.server.v2.api.group.request;
+import io.swagger.v3.oas.annotations.extensions.Extension;
+import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.annotation.Nullable;
@@ -30,7 +32,12 @@ public record GroupsSearchRestRequest(
@Nullable
@Schema(description = "Filter on name.\n"
+ "This parameter performs a partial match (contains), it is case insensitive.")
- String q
+ String q,
+
+ @Nullable
+ @Schema(description = "Filter groups containing the user. Only available for system administrators. Using != operator will search for groups without the user.",
+ extensions = @Extension(properties = {@ExtensionProperty(name = "internal", value = "true")}))
+ String userId
) {
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/mode/controller/ModeController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/mode/controller/ModeController.java
index 336584a778a..3dec8655db4 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/mode/controller/ModeController.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/mode/controller/ModeController.java
@@ -34,12 +34,13 @@ import org.springframework.web.bind.annotation.RestController;
import static org.sonar.server.v2.WebApiEndpoints.INTERNAL;
import static org.sonar.server.v2.WebApiEndpoints.MODE_ENDPOINT;
+import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@RequestMapping(MODE_ENDPOINT)
@RestController
public interface ModeController {
- @GetMapping(path = "")
+ @GetMapping(path = "", produces = APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "Retrieve current instance Mode", description = """
Fetch the current instance mode. Can be Multi-Quality Rules (MQR) Mode or Standard Experience.
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsController.java
index 3ea79cb5467..19677708455 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsController.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsController.java
@@ -21,6 +21,7 @@ package org.sonar.server.v2.api.projectbindings.controller;
import java.util.List;
import java.util.Optional;
+import org.apache.commons.lang3.StringUtils;
import org.sonar.db.alm.setting.ProjectAlmSettingDto;
import org.sonar.db.project.ProjectDto;
import org.sonar.server.common.SearchResults;
@@ -35,8 +36,9 @@ import org.sonar.server.v2.api.projectbindings.request.ProjectBindingsSearchRest
import org.sonar.server.v2.api.projectbindings.response.ProjectBindingsSearchRestResponse;
import org.sonar.server.v2.api.response.PageRestResponse;
-import static org.sonar.db.permission.ProjectPermission.USER;
import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS;
+import static org.sonar.db.permission.ProjectPermission.USER;
+import static org.sonar.server.exceptions.BadRequestException.throwBadRequestException;
public class DefaultProjectBindingsController implements ProjectBindingsController {
@@ -61,25 +63,26 @@ public class DefaultProjectBindingsController implements ProjectBindingsControll
}
}
- private static ProjectBinding toProjectBinding(ProjectDto projectDto, ProjectAlmSettingDto projectAlmSettingDto) {
- return new ProjectBinding(
- projectAlmSettingDto.getUuid(),
- projectAlmSettingDto.getAlmSettingUuid(),
- projectAlmSettingDto.getProjectUuid(),
- projectDto.getKey(),
- projectAlmSettingDto.getAlmRepo(),
- projectAlmSettingDto.getAlmSlug());
- }
-
@Override
public ProjectBindingsSearchRestResponse searchProjectBindings(ProjectBindingsSearchRestRequest restRequest, RestPage restPage) {
userSession.checkLoggedIn().checkPermission(PROVISION_PROJECTS);
- ProjectBindingsSearchRequest serviceRequest = new ProjectBindingsSearchRequest(restRequest.repository(), restRequest.dopSettingId(), restPage.pageIndex(), restPage.pageSize());
+ validateSearchParameters(restRequest);
+ ProjectBindingsSearchRequest serviceRequest = new ProjectBindingsSearchRequest(
+ restRequest.repository(), restRequest.dopSettingId(), restRequest.repositoryUrl(), restPage.pageIndex(), restPage.pageSize());
SearchResults<ProjectBindingInformation> searchResults = projectBindingsService.findProjectBindingsByRequest(serviceRequest);
List<ProjectBinding> projectBindings = toProjectBindings(searchResults);
return new ProjectBindingsSearchRestResponse(projectBindings, new PageRestResponse(restPage.pageIndex(), restPage.pageSize(), searchResults.total()));
}
+ private static void validateSearchParameters(ProjectBindingsSearchRestRequest request) {
+ boolean hasRepositoryUrl = StringUtils.isNotBlank(request.repositoryUrl());
+ boolean hasOtherParams = StringUtils.isNotBlank(request.repository()) || StringUtils.isNotBlank(request.dopSettingId());
+
+ if (hasRepositoryUrl && hasOtherParams) {
+ throwBadRequestException("'repositoryUrl' parameter cannot be used together with 'repository' or 'dopSettingId' parameters");
+ }
+ }
+
private static List<ProjectBinding> toProjectBindings(SearchResults<ProjectBindingInformation> searchResults) {
return searchResults.searchResults().stream()
.map(DefaultProjectBindingsController::toProjectBinding)
@@ -95,4 +98,15 @@ public class DefaultProjectBindingsController implements ProjectBindingsControll
projectBindingInformation.repository(),
projectBindingInformation.slug());
}
+
+ private static ProjectBinding toProjectBinding(ProjectDto projectDto, ProjectAlmSettingDto projectAlmSettingDto) {
+ return new ProjectBinding(
+ projectAlmSettingDto.getUuid(),
+ projectAlmSettingDto.getAlmSettingUuid(),
+ projectAlmSettingDto.getProjectUuid(),
+ projectDto.getKey(),
+ projectAlmSettingDto.getAlmRepo(),
+ projectAlmSettingDto.getAlmSlug());
+ }
+
}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/ProjectBindingsController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/ProjectBindingsController.java
index 64d3eda84b0..8a7ca41d895 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/ProjectBindingsController.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/ProjectBindingsController.java
@@ -37,12 +37,13 @@ import org.springframework.web.bind.annotation.RestController;
import static org.sonar.server.v2.WebApiEndpoints.INTERNAL;
import static org.sonar.server.v2.WebApiEndpoints.PROJECT_BINDINGS_ENDPOINT;
+import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@RequestMapping(PROJECT_BINDINGS_ENDPOINT)
@RestController
public interface ProjectBindingsController {
- @GetMapping(path = "/{id}")
+ @GetMapping(path = "/{id}", produces = APPLICATION_JSON_VALUE)
@Operation(
operationId = "getProjectBinding",
summary = "Fetch a single Project Binding",
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/request/ProjectBindingsSearchRestRequest.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/request/ProjectBindingsSearchRestRequest.java
index 6df2d7980f7..c3020fd06cc 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/request/ProjectBindingsSearchRestRequest.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/request/ProjectBindingsSearchRestRequest.java
@@ -22,19 +22,21 @@ package org.sonar.server.v2.api.projectbindings.request;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.annotation.Nullable;
-public record ProjectBindingsSearchRestRequest (
+public record ProjectBindingsSearchRestRequest(
- @Nullable
- @Schema(
+ @Nullable @Schema(
description = """
Filter on the repository name.
This parameter performs an exact, case insensitive, match.
- """)
- String repository,
+ """) String repository,
- @Nullable
- @Schema(description = "Filter on the DevOps Platform setting id.")
- String dopSettingId
+ @Nullable @Schema(description = "Filter on the DevOps Platform setting id.") String dopSettingId,
+
+ @Nullable @Schema(
+ description = """
+ Filter on the repository URL.
+ This parameter can be in different formats, the traditional URL or the git remote URL (https or ssh).
+ """) String repositoryUrl
) {
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/UserController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/UserController.java
index 52563f6d997..1e478f0b602 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/UserController.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/UserController.java
@@ -35,7 +35,6 @@ import org.sonar.server.v2.api.user.response.UserRestResponse;
import org.sonar.server.v2.api.user.response.UsersSearchRestResponse;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
@@ -49,12 +48,13 @@ import org.springframework.web.bind.annotation.RestController;
import static org.sonar.server.v2.WebApiEndpoints.JSON_MERGE_PATCH_CONTENT_TYPE;
import static org.sonar.server.v2.WebApiEndpoints.USER_ENDPOINT;
+import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@RequestMapping(USER_ENDPOINT)
@RestController
public interface UserController {
- @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
+ @GetMapping(produces = APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "Users search", description = """
Get a list of users. By default, only active users are returned.
@@ -82,7 +82,7 @@ public interface UserController {
@PathVariable("id") @Parameter(description = "The ID of the user to delete.", required = true, in = ParameterIn.PATH) String id,
@RequestParam(value = "anonymize", required = false, defaultValue = "false") @Parameter(description = "Anonymize user in addition to deactivating it.") Boolean anonymize);
- @GetMapping(path = "/{id}")
+ @GetMapping(path = "/{id}", produces = APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "Fetch a single user", description = """
Fetch a single user.
@@ -98,14 +98,14 @@ public interface UserController {
""")
UserRestResponse fetchUser(@PathVariable("id") @Parameter(description = "The id of the user to fetch.", required = true, in = ParameterIn.PATH) String id);
- @PatchMapping(path = "/{id}", consumes = JSON_MERGE_PATCH_CONTENT_TYPE, produces = MediaType.APPLICATION_JSON_VALUE)
+ @PatchMapping(path = "/{id}", consumes = JSON_MERGE_PATCH_CONTENT_TYPE, produces = APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "Update a user", description = """
Update users attributes.
""")
UserRestResponse updateUser(@PathVariable("id") String id, @Valid @RequestBody UserUpdateRestRequest updateRequest);
- @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+ @PostMapping(consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "User creation", description = """
Create a user.
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGenerator.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGenerator.java
index 8cf5bc05bb1..a158f205091 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGenerator.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGenerator.java
@@ -64,7 +64,7 @@ public class UsersSearchRestResponseGenerator implements UsersSearchResponseGene
String login = userDto.getLogin();
String name = userDto.getName();
if (!userSession.isLoggedIn()) {
- return new UserRestResponseForAnonymousUsers(id, login, name);
+ return new UserRestResponseForAnonymousUsers(login, name);
}
String avatar = userInformation.avatar().orElse(null);
@@ -95,7 +95,7 @@ public class UsersSearchRestResponseGenerator implements UsersSearchResponseGene
slLastConnectionDate,
scmAccounts);
}
- return new UserRestResponseForLoggedInUsers(id, login, name, email, active, local, externalIdentityProvider, avatar);
+ return new UserRestResponseForLoggedInUsers(login, name, active, avatar);
}
private static String toDateTime(@Nullable Long dateTimeMs) {
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/UserRestResponseForAnonymousUsers.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/UserRestResponseForAnonymousUsers.java
index 71c344b814b..c6d787be217 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/UserRestResponseForAnonymousUsers.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/UserRestResponseForAnonymousUsers.java
@@ -20,7 +20,6 @@
package org.sonar.server.v2.api.user.response;
public record UserRestResponseForAnonymousUsers(
- String id,
String login,
String name
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/UserRestResponseForLoggedInUsers.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/UserRestResponseForLoggedInUsers.java
index 25900316432..4b84fbdd0de 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/UserRestResponseForLoggedInUsers.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/UserRestResponseForLoggedInUsers.java
@@ -22,18 +22,11 @@ package org.sonar.server.v2.api.user.response;
import javax.annotation.Nullable;
public record UserRestResponseForLoggedInUsers(
- String id,
String login,
String name,
@Nullable
- String email,
- @Nullable
Boolean active,
@Nullable
- Boolean local,
- @Nullable
- String externalProvider,
- @Nullable
String avatar
) implements UserRestResponse {
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/ErrorMessages.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/ErrorMessages.java
index 08ffe167e3b..f533c95c066 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/ErrorMessages.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/ErrorMessages.java
@@ -38,6 +38,7 @@ enum ErrorMessages {
UNACCEPTABLE_MEDIA_TYPE("The requested media type is not acceptable."),
UNSUPPORTED_MEDIA_TYPE("Unsupported media type."),
VALIDATION_ERROR("Validation error. Please check your input."),
+ TOO_MANY_REQUESTS("Too many requests. Please try again later."),
;
private final String message;
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java
index 409796fbf05..3e5929f83cd 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java
@@ -29,6 +29,7 @@ import org.slf4j.LoggerFactory;
import org.sonar.server.authentication.event.AuthenticationException;
import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.exceptions.ServerException;
+import org.sonar.server.exceptions.TooManyRequestsException;
import org.sonar.server.v2.api.model.RestError;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.http.HttpStatus;
@@ -48,6 +49,8 @@ import org.springframework.web.method.annotation.MethodArgumentTypeMismatchExcep
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.servlet.NoHandlerFoundException;
+import static org.springframework.http.HttpStatus.TOO_MANY_REQUESTS;
+
@RestControllerAdvice
public class RestResponseEntityExceptionHandler {
@@ -130,6 +133,12 @@ public class RestResponseEntityExceptionHandler {
return buildResponse(HttpStatus.PAYLOAD_TOO_LARGE, ErrorMessages.SIZE_EXCEEDED);
}
+ @ExceptionHandler(TooManyRequestsException.class)
+ protected ResponseEntity<RestError> handleTooManyRequestsException(TooManyRequestsException ex) {
+ final String errorMessage = Optional.ofNullable(ex.getMessage()).orElse(ErrorMessages.TOO_MANY_REQUESTS.getMessage());
+ return buildResponse(TOO_MANY_REQUESTS, errorMessage);
+ }
+
// endregion client
// region security
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/CommonWebConfig.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/CommonWebConfig.java
index eb4ce8b9d89..dba11c19740 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/CommonWebConfig.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/CommonWebConfig.java
@@ -22,8 +22,10 @@ package org.sonar.server.v2.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import jakarta.validation.Validation;
-import jakarta.validation.ValidatorFactory;
import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator;
+import org.sonar.api.internal.MetadataLoader;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.Version;
import org.sonar.server.v2.common.RestResponseEntityExceptionHandler;
import org.springdoc.core.properties.SpringDocConfigProperties;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
@@ -32,6 +34,9 @@ import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.http.MediaType;
+import org.springframework.validation.Validator;
+import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
+import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@@ -50,12 +55,26 @@ public class CommonWebConfig implements WebMvcConfigurer {
configurer.setUrlPathHelper(urlPathHelper).setUseTrailingSlashMatch(true);
}
- @Bean
- public ValidatorFactory validator() {
- return Validation.byDefaultProvider()
+ @Override
+ public Validator getValidator() {
+ // This validator gets returned from the
+ // WebMvcConfigurationSupport#mvcValidator bean factory method.
+ // We can create a new one each time here and an instance will be cached
+ // in the Spring context.
+ //
+ // One reason we override the validator is to avoid a dependency
+ // on an expression language implementation like expressly.
+ //
+ // This same validator must also be configured in ControllerTester,
+ // otherwise unit test behavior will not match production behavior.
+ //
+ // The validator errors are formatted in RestResponseEntityExceptionHandler.
+ var jakartaValidator = Validation.byDefaultProvider()
.configure()
.messageInterpolator(new ParameterMessageInterpolator())
- .buildValidatorFactory();
+ .buildValidatorFactory()
+ .getValidator();
+ return new SpringValidatorAdapter(jakartaValidator);
}
@Bean
@@ -65,18 +84,24 @@ public class CommonWebConfig implements WebMvcConfigurer {
@Bean
public OpenAPI customOpenAPI() {
+ Version sqVersion = MetadataLoader.loadSQVersion(System2.INSTANCE);
return new OpenAPI()
.info(new Info()
- .title("SonarQube Web API v2")
- .description("""
- The SonarQube API v2 is a REST API which enables you to interact with SonarQube programmatically. Endpoint listed here should work as expected.
- However, you should not consider the API stable for now as it is still under development. New releases of SonarQube can bring changes to existing endpoint definitions.
- """)
- );
+ .title("SonarQube Web API v2")
+ .description("""
+ The SonarQube API v2 is a REST API which enables you to interact with SonarQube programmatically.
+ While not all endpoints of the former Web API are available yet, the ones available are stable and can be used in production environments.
+ """)
+ .version(sqVersion.toString()));
}
@Bean
public BeanFactoryPostProcessor beanFactoryPostProcessor1(SpringDocConfigProperties springDocConfigProperties) {
return beanFactory -> springDocConfigProperties.setDefaultProducesMediaType(MediaType.APPLICATION_JSON_VALUE);
}
+
+ @Override
+ public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
+ configurer.defaultContentType(MediaType.APPLICATION_JSON, MediaType.ALL);
+ }
}
diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/group/controller/DefaultGroupControllerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/group/controller/DefaultGroupControllerTest.java
index c9d7070816d..2b6fec3c337 100644
--- a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/group/controller/DefaultGroupControllerTest.java
+++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/group/controller/DefaultGroupControllerTest.java
@@ -412,6 +412,8 @@ public class DefaultGroupControllerTest {
assertThat(requestCaptor.getValue().page()).hasToString(DEFAULT_PAGE_INDEX);
assertThat(requestCaptor.getValue().managed()).isNull();
assertThat(requestCaptor.getValue().query()).isNull();
+ assertThat(requestCaptor.getValue().userUuid()).isNull();
+ assertThat(requestCaptor.getValue().excludedUserUuid()).isNull();
}
@Test
@@ -422,6 +424,8 @@ public class DefaultGroupControllerTest {
mockMvc.perform(get(GROUPS_ENDPOINT)
.param("managed", "true")
.param("q", "q")
+ .param("userId", "userId")
+ .param("userId!", "excludedUserId")
.param("pageSize", "100")
.param("pageIndex", "2"))
.andExpect(status().isOk());
@@ -432,6 +436,8 @@ public class DefaultGroupControllerTest {
assertThat(requestCaptor.getValue().page()).isEqualTo(2);
assertThat(requestCaptor.getValue().managed()).isTrue();
assertThat(requestCaptor.getValue().query()).isEqualTo("q");
+ assertThat(requestCaptor.getValue().userUuid()).isEqualTo("userId");
+ assertThat(requestCaptor.getValue().excludedUserUuid()).isEqualTo("excludedUserId");
}
@Test
diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsControllerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsControllerTest.java
index 65ef60637d1..51014f1c901 100644
--- a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsControllerTest.java
+++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsControllerTest.java
@@ -39,8 +39,8 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import static org.sonar.db.permission.ProjectPermission.ADMIN;
import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS;
+import static org.sonar.db.permission.ProjectPermission.ADMIN;
import static org.sonar.server.v2.WebApiEndpoints.PROJECT_BINDINGS_ENDPOINT;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
@@ -223,6 +223,85 @@ class DefaultProjectBindingsControllerTest {
"""));
}
+ @Test
+ void searchProjectBindings_whenRepositoryUrlUsed_shouldForwardRepositoryUrlParameter() throws Exception {
+ userSession.logIn().addPermission(PROVISION_PROJECTS);
+ when(projectBindingsService.findProjectBindingsByRequest(any())).thenReturn(new SearchResults<>(List.of(), 0));
+
+ mockMvc
+ .perform(get(PROJECT_BINDINGS_ENDPOINT)
+ .param("repositoryUrl", "https://github.com/org/repo")
+ .param("pageIndex", "1")
+ .param("pageSize", "50"))
+ .andExpect(status().isOk());
+
+ ArgumentCaptor<ProjectBindingsSearchRequest> requestCaptor = ArgumentCaptor.forClass(ProjectBindingsSearchRequest.class);
+ verify(projectBindingsService).findProjectBindingsByRequest(requestCaptor.capture());
+ assertThat(requestCaptor.getValue().repositoryUrl()).isEqualTo("https://github.com/org/repo");
+ assertThat(requestCaptor.getValue().repository()).isNull();
+ assertThat(requestCaptor.getValue().dopSettingId()).isNull();
+ assertThat(requestCaptor.getValue().page()).isEqualTo(1);
+ assertThat(requestCaptor.getValue().pageSize()).isEqualTo(50);
+ }
+
+ @Test
+ void searchProjectBindings_whenRepositoryUrlWithRepositoryParameter_shouldReturnBadRequest() throws Exception {
+ userSession.logIn().addPermission(PROVISION_PROJECTS);
+
+ mockMvc
+ .perform(get(PROJECT_BINDINGS_ENDPOINT)
+ .param("repositoryUrl", "https://github.com/org/repo")
+ .param("repository", "repo"))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void searchProjectBindings_whenRepositoryUrlWithDopSettingIdParameter_shouldReturnBadRequest() throws Exception {
+ userSession.logIn().addPermission(PROVISION_PROJECTS);
+
+ mockMvc
+ .perform(get(PROJECT_BINDINGS_ENDPOINT)
+ .param("repositoryUrl", "https://github.com/org/repo")
+ .param("dopSettingId", "setting123"))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void searchProjectBindings_whenRepositoryUrlReturnsResults_shouldReturnThem() throws Exception {
+ userSession.logIn().addPermission(PROVISION_PROJECTS);
+
+ ProjectBindingInformation dto1 = projectBindingInformation("1");
+ List<ProjectBindingInformation> expectedResults = List.of(dto1);
+ when(projectBindingsService.findProjectBindingsByRequest(any())).thenReturn(new SearchResults<>(expectedResults, expectedResults.size()));
+
+ mockMvc
+ .perform(get(PROJECT_BINDINGS_ENDPOINT)
+ .param("repositoryUrl", "https://github.com/org/repo")
+ .param("pageIndex", "1")
+ .param("pageSize", "50"))
+ .andExpectAll(
+ status().isOk(),
+ content().json("""
+ {
+ "projectBindings": [
+ {
+ "id": "uuid_1",
+ "devOpsPlatformSettingId": "almSettingUuid_1",
+ "projectId": "projectUuid_1",
+ "projectKey": "projectKey_1",
+ "repository": "almRepo_1",
+ "slug": "almSlug_1"
+ }
+ ],
+ "page": {
+ "pageIndex": 1,
+ "pageSize": 50,
+ "total": 1
+ }
+ }
+ """));
+ }
+
private static ProjectAlmSettingDto mockProjectAlmSettingDto(String i) {
ProjectAlmSettingDto dto = mock();
when(dto.getUuid()).thenReturn("uuid_" + i);
diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGeneratorTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGeneratorTest.java
index 0780e7435ab..bda36f5e806 100644
--- a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGeneratorTest.java
+++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGeneratorTest.java
@@ -121,13 +121,9 @@ public class UsersSearchRestResponseGeneratorTest {
private static UserRestResponseForLoggedInUsers buildExpectedResponseForUser(UserInformation userInformation) {
UserDto userDto = userInformation.userDto();
return new UserRestResponseForLoggedInUsers(
- userDto.getUuid(),
userDto.getLogin(),
userDto.getName(),
- userDto.getEmail(),
userDto.isActive(),
- userDto.isLocal(),
- userDto.getExternalIdentityProvider(),
userInformation.avatar().orElse(null)
);
}
@@ -150,7 +146,6 @@ public class UsersSearchRestResponseGeneratorTest {
private static UserRestResponseForAnonymousUsers buildExpectedResponseForAnonymous(UserInformation userInformation) {
UserDto userDto = userInformation.userDto();
return new UserRestResponseForAnonymousUsers(
- userDto.getUuid(),
userDto.getLogin(),
userDto.getName()
);
diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandlerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandlerTest.java
index 7dbe13a3a18..2db9c240baf 100644
--- a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandlerTest.java
+++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandlerTest.java
@@ -40,6 +40,7 @@ import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.exceptions.ServerException;
import org.sonar.server.exceptions.TemplateMatchingKeyException;
+import org.sonar.server.exceptions.TooManyRequestsException;
import org.sonar.server.exceptions.UnauthorizedException;
import org.sonar.server.v2.api.model.RestError;
import org.springframework.core.MethodParameter;
@@ -325,6 +326,16 @@ class RestResponseEntityExceptionHandlerTest {
assertThat(logs.logs(Level.WARN)).contains(ErrorMessages.SIZE_EXCEEDED.getMessage());
}
+ @Test
+ void handleTooManyRequestsException_shouldReturnCorrectHttpStatus(){
+ var ex = new TooManyRequestsException("Too many requests");
+ ResponseEntity<RestError> response = underTest.handleTooManyRequestsException(ex);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.TOO_MANY_REQUESTS);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().message()).isEqualTo(ex.getMessage());
+ }
+
@ParameterizedTest
@MethodSource("serverExceptionsProvider")
void handleServerException_shouldReturnCorrectHttpStatus(ServerException ex, HttpStatus expectedStatus) {
diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/config/CommonWebConfigTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/config/CommonWebConfigTest.java
index 714c5267a6b..82ecaf12297 100644
--- a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/config/CommonWebConfigTest.java
+++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/config/CommonWebConfigTest.java
@@ -21,12 +21,18 @@ package org.sonar.server.v2.config;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.MockedStatic;
import org.mockito.junit.MockitoJUnitRunner;
+import org.sonar.api.internal.MetadataLoader;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.Version;
+import org.springframework.validation.Validator;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.util.UrlPathHelper;
import static java.util.Objects.requireNonNull;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mockStatic;
@RunWith(MockitoJUnitRunner.class)
public class CommonWebConfigTest {
@@ -42,4 +48,30 @@ public class CommonWebConfigTest {
assertThat(actualUrlPathHelper.isUrlDecode()).isFalse();
}
+ @Test
+ public void customOpenAPI_shouldIncludeNonNullVersion() {
+ Version expectedVersion = Version.parse("1.0.0");
+ try (MockedStatic<MetadataLoader> metadataLoaderMock = mockStatic(MetadataLoader.class)) {
+ metadataLoaderMock.when(() -> MetadataLoader.loadSQVersion(System2.INSTANCE)).thenReturn(expectedVersion);
+
+ var commonWebConfig = new CommonWebConfig();
+ var info = commonWebConfig.customOpenAPI().getInfo();
+
+ assertThat(info.getVersion()).isNotNull();
+ assertThat(info.getDescription()).isEqualTo("""
+ The SonarQube API v2 is a REST API which enables you to interact with SonarQube programmatically.
+ While not all endpoints of the former Web API are available yet, the ones available are stable and can be used in production environments.
+ """);
+ assertThat(info.getVersion()).isEqualTo(expectedVersion.toString());
+ }
+ }
+
+ @Test
+ public void getValidator_shouldReturnNonNull() {
+ CommonWebConfig commonWebConfig = new CommonWebConfig();
+ Validator validator = commonWebConfig.getValidator();
+
+ assertThat(validator).isNotNull();
+ }
+
}
diff --git a/server/sonar-webserver-webapi-v2/src/test/resources/http/http-client.env.json b/server/sonar-webserver-webapi-v2/src/test/resources/http/http-client.env.json
new file mode 100644
index 00000000000..c78a169cfdc
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/test/resources/http/http-client.env.json
@@ -0,0 +1,45 @@
+{
+ "localhost": {
+ "baseUrl": "http://localhost:9000",
+ "sonar": {
+ "token": "TODO: Replace with your SonarQube token in a private configuration file",
+ "tokenNoPermissions": "TODO: Replace with your SonarQube token in a private configuration file"
+ },
+ "github": {
+ "token": "TODO: Replace with your GitHub token in a private configuration file",
+ "repo": {
+ "slug": "jc-infinite-repos/jc-infinite-repos-3600",
+ "url": "https://github.com/jc-infinite-repos/jc-infinite-repos-3600",
+ "httpsUrl": "https://github.com/jc-infinite-repos/jc-infinite-repos-3600.git",
+ "sshUrl": "git@github.com:jc-infinite-repos/jc-infinite-repos-3600.git"
+ }
+ },
+ "gitlab": {
+ "token": "TODO: Replace with your GitLab token in a private configuration file",
+ "repo": {
+ "slug": "julien.camus1/ror-boilerplate",
+ "url": "https://gitlab.com/julien.camus1/ror-boilerplate",
+ "httpsUrl": "https://gitlab.com/julien.camus1/ror-boilerplate.git",
+ "sshUrl": "git@gitlab.com:julien.camus1/ror-boilerplate.git"
+ }
+ },
+ "azure": {
+ "token": "TODO: Replace with your Azure token in a private configuration file",
+ "repo": {
+ "slug": null,
+ "url": "https://dev.azure.com/juliencamus/test/_git/test1",
+ "httpsUrl": "https://juliencamus@dev.azure.com/juliencamus/test/_git/test1",
+ "sshUrl": "git@ssh.dev.azure.com:v3/juliencamus/test/test1"
+ }
+ },
+ "bitbucket": {
+ "token": "TODO: Replace with your Bitbucket token in a private configuration file",
+ "repo": {
+ "slug": "sonar-pk-test/sonarlint-onsite-exercise",
+ "url": "https://bitbucket.org/sonar-pk-test/sonarlint-onsite-exercise",
+ "httpsUrl": "https://juliencamus@bitbucket.org/sonar-pk-test/sonarlint-onsite-exercise.git",
+ "sshUrl": "git@bitbucket.org:sonar-pk-test/sonarlint-onsite-exercise.git"
+ }
+ }
+ }
+}
diff --git a/server/sonar-webserver-webapi-v2/src/test/resources/http/project-bindings.http b/server/sonar-webserver-webapi-v2/src/test/resources/http/project-bindings.http
new file mode 100644
index 00000000000..36247d92551
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/test/resources/http/project-bindings.http
@@ -0,0 +1,89 @@
+### Search project bindings by repository URL (HTTPS URL)
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repositoryUrl={{github.repo.url}}
+Authorization: Bearer {{sonar.token}}
+Accept: application/json
+
+### Search project bindings by repository URL (HTTPS URL with .git suffix)
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repositoryUrl={{github.repo.httpsUrl}}
+Authorization: Bearer {{sonar.token}}
+Accept: application/json
+
+### Search project bindings by repository URL (SSH URL)
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repositoryUrl={{github.repo.sshUrl}}
+Authorization: Bearer {{sonar.token}}
+Accept: application/json
+
+### Search project bindings by repository URL (URL with trailing slash)
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repositoryUrl={{github.repo.url}}/
+Authorization: Bearer {{sonar.token}}
+Accept: application/json
+
+### Search project bindings by repository URL with pagination
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repositoryUrl={{github.repo.url}}
+ &pageIndex=1
+ &pageSize=10
+Authorization: Bearer {{sonar.token}}
+Accept: application/json
+
+### Error case: No ALM configuration found for repository URL
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repositoryUrl=https://github.com/nonexistent/repo
+Authorization: Bearer {{sonar.token}}
+Accept: application/json
+
+### Error case: Invalid URL format
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repositoryUrl=invalid-url-format
+Authorization: Bearer {{sonar.token}}
+Accept: application/json
+
+### Parameter validation error: repositoryUrl with repository parameter
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repositoryUrl={{github.repo.url}}
+ &repository={{github.repo.slug}}
+Authorization: Bearer {{sonar.token}}
+Accept: application/json
+
+### Parameter validation error: repositoryUrl with dopSettingId parameter
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repositoryUrl={{github.repo.url}}
+ &dopSettingId=25e6ceba-ab08-4b43-8655-52422dde80b6
+Authorization: Bearer {{sonar.token}}
+Accept: application/json
+
+### Parameter validation error: repositoryUrl with both repository and dopSettingId
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repositoryUrl={{github.repo.url}}
+ &repository={{github.repo.slug}}
+ &dopSettingId=25e6ceba-ab08-4b43-8655-52422dde80b6
+Authorization: Bearer {{sonar.token}}
+Accept: application/json
+
+### Traditional search by repository and dopSettingId (still supported)
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repository={{github.repo.slug}}
+ &dopSettingId=25e6ceba-ab08-4b43-8655-52422dde80b6
+Authorization: Bearer {{sonar.token}}
+Accept: application/json
+
+### Traditional search by repository only
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repository={{github.repo.slug}}
+Authorization: Bearer {{sonar.token}}
+Accept: application/json
+
+### Error case: Missing authentication
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repositoryUrl={{github.repo.url}}
+Accept: application/json
+
+### Error case: Insufficient permissions (use token without PROVISION_PROJECTS permission)
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repositoryUrl={{github.repo.url}}
+Authorization: Bearer {{sonar.tokenNoPermissions}}
+Accept: application/json
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/TransitionServiceIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/TransitionServiceIT.java
index c9745bd28d4..12740b6e639 100644
--- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/TransitionServiceIT.java
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/TransitionServiceIT.java
@@ -23,7 +23,6 @@ import java.util.Date;
import java.util.List;
import org.junit.Rule;
import org.junit.Test;
-import org.sonar.api.issue.Issue;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.db.DbTester;
import org.sonar.db.component.ComponentDto;
@@ -37,7 +36,6 @@ import org.sonar.server.issue.workflow.codequalityissue.CodeQualityIssueWorkflow
import org.sonar.server.issue.workflow.securityhotspot.SecurityHotspotWorkflow;
import org.sonar.server.issue.workflow.securityhotspot.SecurityHotspotWorkflowActionsFactory;
import org.sonar.server.issue.workflow.securityhotspot.SecurityHotspotWorkflowDefinition;
-import org.sonar.server.issue.workflow.statemachine.Transition;
import org.sonar.server.tester.UserSessionRule;
import static org.assertj.core.api.Assertions.assertThat;
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/SearchActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/SearchActionIT.java
index c89ca62f3c1..442b8272a26 100644
--- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/SearchActionIT.java
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/SearchActionIT.java
@@ -1863,6 +1863,41 @@ class SearchActionIT {
@ParameterizedTest
@ValueSource(booleans = {true, false})
+ void only_vulnerabilities_are_returned_by_owasp_mobile_2024(boolean mqrMode) {
+ doReturn(Optional.of(mqrMode)).when(config).getBoolean(MULTI_QUALITY_MODE_ENABLED);
+ ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
+ ComponentDto file = db.components().insertComponent(newFileDto(project));
+ Consumer<RuleDto> ruleConsumer = ruleDefinitionDto -> ruleDefinitionDto
+ .setSecurityStandards(Sets.newHashSet("cwe:20", "cwe:564", "cwe:89", "cwe:943", "owaspMobileTop10-2024:m3", "owaspTop10-2021:a2"))
+ .setSystemTags(Sets.newHashSet("bad-practice", "cwe", "owasp-a1", "sans-top25-insecure", "sql"));
+ Consumer<IssueDto> issueConsumer = issueDto -> issueDto.setTags(Sets.newHashSet("bad-practice", "cwe", "owasp-a1", "sans-top25" +
+ "-insecure", "sql"));
+ RuleDto hotspotRule = db.rules().insertHotspotRule(ruleConsumer);
+ db.issues().insertHotspot(hotspotRule, project, file, issueConsumer);
+ RuleDto issueRule = db.rules().insertIssueRule(ruleConsumer);
+ IssueDto issueDto1 = db.issues().insertIssue(issueRule, project, file, issueConsumer,
+ issueDto -> issueDto.setType(RuleType.VULNERABILITY).replaceAllImpacts(List.of(new ImpactDto(SECURITY, HIGH))));
+ IssueDto issueDto2 = db.issues().insertIssue(issueRule, project, file, issueConsumer,
+ issueDto -> issueDto.setType(RuleType.VULNERABILITY).replaceAllImpacts(List.of(new ImpactDto(SECURITY, HIGH))));
+ db.issues().insertIssue(issueRule, project, file, issueConsumer, issueDto -> issueDto.setType(CODE_SMELL));
+ indexPermissionsAndIssues();
+
+ SearchWsResponse result = ws.newRequest()
+ .setParam("owaspMobileTop10-2024", "m3")
+ .setParam(FACETS, "owaspMobileTop10-2024")
+ .executeProtobuf(SearchWsResponse.class);
+
+ assertThat(result.getIssuesList())
+ .extracting(Issue::getKey)
+ .containsExactlyInAnyOrder(issueDto1.getKey(), issueDto2.getKey());
+
+ assertThat(result.getFacets().getFacets(0).getValuesList())
+ .extracting(Common.FacetValue::getVal, Common.FacetValue::getCount)
+ .containsExactlyInAnyOrder(tuple("m3", 2L));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
void only_vulnerabilities_are_returned_by_stig_R5V3(boolean mqrMode) {
doReturn(Optional.of(mqrMode)).when(config).getBoolean(MULTI_QUALITY_MODE_ENABLED);
ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
@@ -2292,7 +2327,7 @@ class SearchActionIT {
"createdBefore", "createdInLast", "directories", "facets", "files", "issues", "scopes", "languages", "onComponentOnly",
"p", "projects", "ps", "resolutions", "resolved", "rules", "s", "severities", "statuses", "tags", "types", "pciDss-3.2", "pciDss-4" +
".0",
- "owaspAsvs-4.0",
+ "owaspAsvs-4.0", "owaspMobileTop10-2024",
"owaspAsvsLevel", "owaspTop10", "owaspTop10-2021", "stig-ASD_V5R3", "casa", "sansTop25", "cwe", "sonarsourceSecurity", "timeZone",
"inNewCodePeriod", "codeVariants",
"cleanCodeAttributeCategories", "impactSeverities", "impactSoftwareQualities", "issueStatuses", "fixedInPullRequest",
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionsActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionsActionIT.java
index a6c7a15b94b..7f037d97ff8 100644
--- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionsActionIT.java
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionsActionIT.java
@@ -20,6 +20,7 @@
package org.sonar.server.issue.ws.anticipatedtransition;
import java.io.IOException;
+import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.Rule;
@@ -104,7 +105,7 @@ public class AnticipatedTransitionsActionIT {
}
@Test
- public void givenRequestWithTransitions_whenHandle_thenAllTransitionsAreSaved() throws IOException {
+ public void givenRequestWithTransitions_whenHandle_thenAllTransitionsAreSaved() throws IOException, URISyntaxException {
// given
ProjectDto projectDto = mockProjectDto();
mockUser(projectDto, ISSUE_ADMIN);
@@ -149,7 +150,7 @@ public class AnticipatedTransitionsActionIT {
}
@Test
- public void givenTransitionsForUserAndProjectAlreadyExistInDb_whenHandle_thenTheNewTransitionsShouldReplaceTheOldOnes() throws IOException {
+ public void givenTransitionsForUserAndProjectAlreadyExistInDb_whenHandle_thenTheNewTransitionsShouldReplaceTheOldOnes() throws IOException, URISyntaxException {
// given
ProjectDto projectDto = mockProjectDto();
mockUser(projectDto, ISSUE_ADMIN);
@@ -179,7 +180,7 @@ public class AnticipatedTransitionsActionIT {
}
@Test
- public void givenRequestWithNoTransitions_whenHandle_thenExistingTransitionsForUserAndProjectShouldBePurged() throws IOException {
+ public void givenRequestWithNoTransitions_whenHandle_thenExistingTransitionsForUserAndProjectShouldBePurged() throws IOException, URISyntaxException {
// given
ProjectDto projectDto = mockProjectDto();
mockUser(projectDto, ISSUE_ADMIN);
@@ -198,7 +199,7 @@ public class AnticipatedTransitionsActionIT {
}
@Test
- public void givenUserWithoutAdminIssuesPermission_whenHandle_thenThrowException() throws IOException {
+ public void givenUserWithoutAdminIssuesPermission_whenHandle_thenThrowException() throws IOException, URISyntaxException {
// given
ProjectDto projectDto = mockProjectDto();
mockUser(projectDto, CODEVIEWER);
@@ -234,8 +235,8 @@ public class AnticipatedTransitionsActionIT {
return projectDto;
}
- private String readTestResourceFile(String fileName) throws IOException {
- return Files.readString(Path.of(getClass().getResource(fileName).getPath()));
+ private String readTestResourceFile(String fileName) throws IOException, URISyntaxException {
+ return Files.readString(Path.of(getClass().getResource(fileName).toURI()));
}
}
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/live/LiveMeasureUpdaterWorkflowTest.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/live/LiveMeasureUpdaterWorkflowTest.java
new file mode 100644
index 00000000000..f261155a9db
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/live/LiveMeasureUpdaterWorkflowTest.java
@@ -0,0 +1,169 @@
+/*
+ * 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.server.measure.live;
+
+import java.util.List;
+import java.util.Set;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.measures.Metric;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.server.qualitygate.EvaluatedQualityGate;
+import org.sonar.server.qualitygate.QualityGate;
+import org.sonar.server.setting.ProjectConfigurationLoader;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY;
+
+@ExtendWith(MockitoExtension.class)
+class LiveMeasureUpdaterWorkflowTest {
+
+ private static final String TEST_KEY = "test_key";
+ @RegisterExtension
+ private final DbTester db = DbTester.create();
+ @Mock
+ private ProjectConfigurationLoader projectConfigurationLoader;
+ @Mock
+ private LiveQualityGateComputer liveQualityGateComputer;
+ @Mock
+ private Configuration config;
+ @Mock
+ private QualityGate qualityGate;
+ private DbClient dbClient;
+ private DbSession dbSession;
+ private ComponentDto project;
+ private BranchDto branch;
+
+ @BeforeEach
+ void setUp() {
+ this.dbClient = db.getDbClient();
+ this.dbSession = db.getSession();
+
+ this.project = db.components().insertPublicProject().getMainBranchComponent();
+ this.branch = dbClient.branchDao().selectByUuid(dbSession, project.uuid()).get();
+
+ var projectDto = dbClient.projectDao().selectByUuid(dbSession, branch.getProjectUuid()).get();
+
+ lenient().when(projectConfigurationLoader.loadBranchConfiguration(dbSession, branch)).thenReturn(config);
+ lenient().when(liveQualityGateComputer.loadQualityGate(dbSession, projectDto, branch)).thenReturn(qualityGate);
+ }
+
+ private LiveMeasureUpdaterWorkflow underTest() {
+ return LiveMeasureUpdaterWorkflow.build(
+ dbClient,
+ dbSession,
+ project,
+ projectConfigurationLoader,
+ liveQualityGateComputer);
+ }
+
+ @Test
+ void build_whenNoBranch_thenFails() {
+ var component = new ComponentDto().setUuid("whatever");
+
+ assertThatThrownBy(() -> LiveMeasureUpdaterWorkflow.build(
+ dbClient,
+ dbSession,
+ component,
+ projectConfigurationLoader,
+ liveQualityGateComputer)).isInstanceOf(IllegalStateException.class);
+ }
+
+ @Test
+ void build_whenBranchAndProject_thenLoadsAllData() {
+ var result = underTest();
+
+ assertThat(result.getBranchDto()).isEqualTo(this.branch);
+ assertThat(result.getConfig()).isEqualTo(config);
+ }
+
+ @Test
+ void updateQualityGateMeasures() {
+ var metric = db.measures().insertMetric(m -> m.setKey(TEST_KEY));
+ var measure = db.measures().insertMeasure(project, m -> m.getMetricValues().put(TEST_KEY, 1));
+ var expectedResult = mock(EvaluatedQualityGate.class);
+
+ when(liveQualityGateComputer.refreshGateStatus(eq(project), eq(qualityGate), any(MeasureMatrix.class), eq(config))).thenReturn(expectedResult);
+
+ var measureMatrix = new MeasureMatrix(
+ List.of(project),
+ List.of(metric),
+ List.of(measure));
+
+ measureMatrix.setValue(project, TEST_KEY, 2);
+
+ assertThat(underTest().updateQualityGateMeasures(measureMatrix)).isEqualTo(expectedResult);
+
+ var updatedMeasures = dbClient.measureDao().selectByComponentUuid(dbSession, project.uuid());
+
+ assertThat(updatedMeasures.get().getLong(TEST_KEY)).isEqualTo(2);
+ }
+
+ @Test
+ void getKeysOfAllInvolvedMetrics() {
+ var metric = mock(Metric.class);
+ when(metric.getKey()).thenReturn(TEST_KEY);
+
+ var formulaFactory = mock(MeasureUpdateFormulaFactory.class);
+ when(formulaFactory.getFormulaMetrics()).thenReturn(Set.of(metric));
+ when(liveQualityGateComputer.getMetricsRelatedTo(qualityGate)).thenReturn(Set.of("other"));
+
+ var result = underTest().getKeysOfAllInvolvedMetrics(formulaFactory);
+
+ assertThat(result).containsExactlyInAnyOrder("other", TEST_KEY);
+ }
+
+ @Test
+ void buildMeasureMatrix() {
+ db.measures().insertMetric(m -> m.setKey(TEST_KEY));
+ db.measures().insertMeasure(project, m -> m.getMetricValues().put(TEST_KEY, 1.0));
+
+ var result = underTest().buildMeasureMatrix(
+ List.of(TEST_KEY),
+ Set.of(branch.getUuid()));
+
+ assertThat(result.getMeasure(project, TEST_KEY).get().getValue()).isEqualTo(1.0);
+ }
+
+ @Test
+ void loadPreviousStatus() {
+ db.measures().insertMetric(m -> m.setKey(ALERT_STATUS_KEY));
+ db.measures().insertMeasure(project, m -> m.getMetricValues().put(ALERT_STATUS_KEY, Metric.Level.OK));
+
+ var result = underTest().loadPreviousStatus();
+
+ assertThat(result).isEqualTo(Metric.Level.OK);
+ }
+}
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualitygate/QualityGateCaycCheckerIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualitygate/QualityGateCaycCheckerIT.java
index 343e1854127..7b660e38620 100644
--- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualitygate/QualityGateCaycCheckerIT.java
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualitygate/QualityGateCaycCheckerIT.java
@@ -39,7 +39,6 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.sonar.api.measures.CoreMetrics.BLOCKER_VIOLATIONS;
import static org.sonar.api.measures.CoreMetrics.DUPLICATED_LINES;
-import static org.sonar.api.measures.CoreMetrics.FUNCTION_COMPLEXITY;
import static org.sonar.api.measures.CoreMetrics.LINE_COVERAGE;
import static org.sonar.api.measures.CoreMetrics.NEW_COVERAGE;
import static org.sonar.api.measures.CoreMetrics.NEW_DUPLICATED_LINES_DENSITY;
@@ -107,7 +106,7 @@ public class QualityGateCaycCheckerIT {
@Test
public void isCaycCondition_when_check_non_compliant_condition_should_return_false() {
- List.of(BLOCKER_VIOLATIONS, FUNCTION_COMPLEXITY)
+ List.of(BLOCKER_VIOLATIONS)
.stream().map(this::toMetricDto)
.forEach(metricDto -> assertFalse(underTest.isCaycCondition(metricDto)));
}
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualitygate/QualityGateConditionsUpdaterIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualitygate/QualityGateConditionsUpdaterIT.java
index 74cbd41392e..d15c935d34f 100644
--- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualitygate/QualityGateConditionsUpdaterIT.java
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualitygate/QualityGateConditionsUpdaterIT.java
@@ -153,6 +153,16 @@ class QualityGateConditionsUpdaterIT {
}
@Test
+ void create_whenMetricIsBasedOnScaRating_shouldWork() {
+ MetricDto metric = insertMetric(RATING, "sca_rating_vulnerability");
+ QualityGateDto qualityGate = db.qualityGates().insertQualityGate();
+
+ QualityGateConditionDto result = underTest.createCondition(db.getSession(), qualityGate, metric.getKey(), "GT", "3");
+
+ verifyCondition(result, qualityGate, metric, "GT", "3");
+ }
+
+ @Test
void create_whenEquivalentConditionAlreadyExists_shouldFail() {
MetricDto equivalentMetric = insertMetric(RATING, SoftwareQualitiesMetrics.SOFTWARE_QUALITY_MAINTAINABILITY_RATING_KEY);
MetricDto newMetric = insertMetric(RATING, SQALE_RATING_KEY);
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/ws/ActivateRulesActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/ws/ActivateRulesActionIT.java
index 2621b250f7a..9eb43a55a64 100644
--- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/ws/ActivateRulesActionIT.java
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/ws/ActivateRulesActionIT.java
@@ -92,6 +92,7 @@ class ActivateRulesActionIT {
"cwe",
"owaspTop10",
"owaspTop10-2021",
+ "owaspMobileTop10-2024",
"sansTop25",
"sonarsourceSecurity",
"cleanCodeAttributeCategories",
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/ws/DeactivateRulesActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/ws/DeactivateRulesActionIT.java
index 2d47ed3d4d6..1bc17a606e7 100644
--- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/ws/DeactivateRulesActionIT.java
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/ws/DeactivateRulesActionIT.java
@@ -90,6 +90,7 @@ public class DeactivateRulesActionIT {
"cwe",
"owaspTop10",
"owaspTop10-2021",
+ "owaspMobileTop10-2024",
"sansTop25",
"sonarsourceSecurity",
"cleanCodeAttributeCategories",
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/rule/ws/SearchActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/rule/ws/SearchActionIT.java
index 2006f9a94f0..77a203ddea9 100644
--- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/rule/ws/SearchActionIT.java
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/rule/ws/SearchActionIT.java
@@ -159,7 +159,7 @@ class SearchActionIT {
assertThat(def.since()).isEqualTo("4.4");
assertThat(def.isInternal()).isFalse();
assertThat(def.responseExampleAsString()).isNotEmpty();
- assertThat(def.params()).hasSize(33);
+ assertThat(def.params()).hasSize(34);
WebService.Param compareToProfile = def.param("compareToProfile");
assertThat(compareToProfile.since()).isEqualTo("6.5");
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/setting/ws/SetActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/setting/ws/SetActionIT.java
index cd482f865e3..78c9141725f 100644
--- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/setting/ws/SetActionIT.java
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/setting/ws/SetActionIT.java
@@ -20,18 +20,16 @@
package org.sonar.server.setting.ws;
import com.google.gson.Gson;
-import com.tngtech.java.junit.dataprovider.DataProvider;
-import com.tngtech.java.junit.dataprovider.DataProviderRunner;
-import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.net.HttpURLConnection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import javax.annotation.Nullable;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
import org.sonar.api.PropertyType;
import org.sonar.api.config.PropertyDefinition;
import org.sonar.api.config.PropertyDefinition.ConfigScope;
@@ -40,7 +38,6 @@ import org.sonar.api.config.PropertyFieldDefinition;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.server.ws.WebService.Param;
import org.sonar.api.utils.System2;
-import org.sonar.db.permission.ProjectPermission;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.DbTester;
@@ -48,6 +45,7 @@ import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.ComponentQualifiers;
import org.sonar.db.component.ComponentTesting;
import org.sonar.db.component.ProjectData;
+import org.sonar.db.permission.ProjectPermission;
import org.sonar.db.portfolio.PortfolioDto;
import org.sonar.db.project.ProjectDto;
import org.sonar.db.property.PropertyDbTester;
@@ -78,15 +76,89 @@ import static org.sonar.db.property.PropertyTesting.newComponentPropertyDto;
import static org.sonar.db.property.PropertyTesting.newGlobalPropertyDto;
import static org.sonar.db.user.UserTesting.newUserDto;
-@RunWith(DataProviderRunner.class)
-public class SetActionIT {
+class SetActionIT {
private static final Gson GSON = GsonHelper.create();
- @Rule
- public UserSessionRule userSession = UserSessionRule.standalone().logIn();
- @Rule
- public DbTester db = DbTester.create(System2.INSTANCE);
+ private static final String SECURITY_CUSTOM_CONFIG_INCORRECT_TYPE = """
+ {
+ "S3649": {
+ "sources": [
+ {
+ "methodId": "My\\\\Namespace\\\\ClassName\\\\ServerRequest::getQuery"
+ }
+ ],
+ "sinks": [
+ {
+ "methodId": 12345,
+ "args": [1]
+ }
+ ]
+ }
+ }""";
+
+ private static final String SECURITY_CUSTOM_CONFIG_NO_ARGS = """
+ {
+ "S3649": {
+ "sources": [
+ {
+ "methodId": "My\\\\Namespace\\\\ClassName\\\\ServerRequest::getQuery"
+ }
+ ],
+ "sanitizers": [
+ {
+ "methodId": "SomeSanitizer"
+ }
+ ]
+ }
+ }""";
+
+ private static final String SECURITY_CUSTOM_CONFIG_EMPTY_ARGS_ARRAY = """
+ {
+ "S3649": {
+ "sources": [
+ {
+ "methodId": "My\\\\Namespace\\\\ClassName\\\\ServerRequest::getQuery"
+ }
+ ],
+ "validators": [
+ {
+ "methodId": "SomeValidator",
+ "args": []
+ }
+ ]
+ }
+ }""";
+
+ private static final String SECURITY_CUSTOM_CONFIG_UNKNOWN_ATTRIBUTE = """
+ {
+ "S3649": {
+ "sources": [
+ {
+ "methodId": "My\\\\Namespace\\\\ClassName\\\\ServerRequest::getQuery"
+ }
+ ],
+ "unknown": [
+ {
+ "methodId": 12345,
+ "args": [1]
+ }
+ ]
+ }
+ }""";
+ private static final String SONAR_SECURITY_CONFIG_JAVA_SECURITY = "sonar.security.config.javasecurity";
+ private static final String SONAR_SECURITY_CONFIG_PHP_SECURITY = "sonar.security.config.phpsecurity";
+ private static final String SONAR_SECURITY_CONFIG_PYTHON_SECURITY = "sonar.security.config.pythonsecurity";
+ private static final String SONAR_SECURITY_CONFIG_ROSLYN_SECURITY_CS = "sonar.security.config.roslyn.sonaranalyzer.security.cs";
+ private static final String ERROR_INCORRECT_TYPE = "Provided JSON is invalid : [expected type: string, actual: integer at mem://input#/S3649/sinks/0/methodId (Line 10, character 21)]";
+ private static final String ERROR_NO_ARGS = "Provided JSON is invalid : [required properties are missing: args at mem://input#/S3649/sanitizers/0 (Line 9, character 8)]";
+ private static final String ERROR_EMPTY_ARGS_ARRAY = "Provided JSON is invalid : [expected minimum items: 1, found only 0 at mem://input#/S3649/validators/0/args (Line 11, character 18)]";
+ private static final String ERROR_UNKNOWN_ATTRIBUTE = "Provided JSON is invalid : [false schema always fails at mem://input#/S3649/unknown (Line 8, character 16)]";
+
+ @RegisterExtension
+ private final UserSessionRule userSession = UserSessionRule.standalone().logIn();
+ @RegisterExtension
+ private final DbTester db = DbTester.create(System2.INSTANCE);
private final PropertyDbTester propertyDb = new PropertyDbTester(db);
private final DbClient dbClient = db.getDbClient();
@@ -100,24 +172,47 @@ public class SetActionIT {
private final WsActionTester ws = new WsActionTester(underTest);
- @Before
- public void setUp() {
+ @BeforeEach
+ void setUp() {
// by default test doesn't care about permissions
userSession.logIn().setSystemAdministrator();
}
+
+ static Object[][] securityJsonProperties() {
+ return new Object[][] {
+ {SONAR_SECURITY_CONFIG_JAVA_SECURITY},
+ {SONAR_SECURITY_CONFIG_PHP_SECURITY},
+ {SONAR_SECURITY_CONFIG_PYTHON_SECURITY},
+ {SONAR_SECURITY_CONFIG_ROSLYN_SECURITY_CS}
+ };
+ }
- @DataProvider
- public static Object[][] securityJsonProperties() {
+ static Object[][] securityJsonPropertiesForInvalidJsonTest() {
return new Object[][] {
- {"sonar.security.config.javasecurity"},
- {"sonar.security.config.phpsecurity"},
- {"sonar.security.config.pythonsecurity"},
- {"sonar.security.config.roslyn.sonaranalyzer.security.cs"}
+ {SONAR_SECURITY_CONFIG_JAVA_SECURITY, SECURITY_CUSTOM_CONFIG_INCORRECT_TYPE, ERROR_INCORRECT_TYPE},
+ {SONAR_SECURITY_CONFIG_JAVA_SECURITY, SECURITY_CUSTOM_CONFIG_NO_ARGS, ERROR_NO_ARGS},
+ {SONAR_SECURITY_CONFIG_JAVA_SECURITY, SECURITY_CUSTOM_CONFIG_EMPTY_ARGS_ARRAY, ERROR_EMPTY_ARGS_ARRAY},
+ {SONAR_SECURITY_CONFIG_JAVA_SECURITY, SECURITY_CUSTOM_CONFIG_UNKNOWN_ATTRIBUTE, ERROR_UNKNOWN_ATTRIBUTE},
+
+ {SONAR_SECURITY_CONFIG_PHP_SECURITY, SECURITY_CUSTOM_CONFIG_INCORRECT_TYPE, ERROR_INCORRECT_TYPE},
+ {SONAR_SECURITY_CONFIG_PHP_SECURITY, SECURITY_CUSTOM_CONFIG_NO_ARGS, ERROR_NO_ARGS},
+ {SONAR_SECURITY_CONFIG_PHP_SECURITY, SECURITY_CUSTOM_CONFIG_EMPTY_ARGS_ARRAY, ERROR_EMPTY_ARGS_ARRAY},
+ {SONAR_SECURITY_CONFIG_PHP_SECURITY, SECURITY_CUSTOM_CONFIG_UNKNOWN_ATTRIBUTE, ERROR_UNKNOWN_ATTRIBUTE},
+
+ {SONAR_SECURITY_CONFIG_PYTHON_SECURITY, SECURITY_CUSTOM_CONFIG_INCORRECT_TYPE, ERROR_INCORRECT_TYPE},
+ {SONAR_SECURITY_CONFIG_PYTHON_SECURITY, SECURITY_CUSTOM_CONFIG_NO_ARGS, ERROR_NO_ARGS},
+ {SONAR_SECURITY_CONFIG_PYTHON_SECURITY, SECURITY_CUSTOM_CONFIG_EMPTY_ARGS_ARRAY, ERROR_EMPTY_ARGS_ARRAY},
+ {SONAR_SECURITY_CONFIG_PYTHON_SECURITY, SECURITY_CUSTOM_CONFIG_UNKNOWN_ATTRIBUTE, ERROR_UNKNOWN_ATTRIBUTE},
+
+ {SONAR_SECURITY_CONFIG_ROSLYN_SECURITY_CS, SECURITY_CUSTOM_CONFIG_INCORRECT_TYPE, ERROR_INCORRECT_TYPE},
+ {SONAR_SECURITY_CONFIG_ROSLYN_SECURITY_CS, SECURITY_CUSTOM_CONFIG_NO_ARGS, ERROR_NO_ARGS},
+ {SONAR_SECURITY_CONFIG_ROSLYN_SECURITY_CS, SECURITY_CUSTOM_CONFIG_EMPTY_ARGS_ARRAY, ERROR_EMPTY_ARGS_ARRAY},
+ {SONAR_SECURITY_CONFIG_ROSLYN_SECURITY_CS, SECURITY_CUSTOM_CONFIG_UNKNOWN_ATTRIBUTE, ERROR_UNKNOWN_ATTRIBUTE}
};
}
@Test
- public void empty_204_response() {
+ void empty_204_response() {
TestResponse result = ws.newRequest()
.setParam("key", "my.key")
.setParam("value", "my value")
@@ -128,7 +223,7 @@ public class SetActionIT {
}
@Test
- public void persist_new_global_setting() {
+ void persist_new_global_setting() {
callForGlobalSetting("my.key", "my,value");
assertGlobalSetting("my.key", "my,value");
@@ -136,7 +231,7 @@ public class SetActionIT {
}
@Test
- public void update_existing_global_setting() {
+ void update_existing_global_setting() {
propertyDb.insertProperty(newGlobalPropertyDto("my.key", "my value"), null, null, null, null);
assertGlobalSetting("my.key", "my value");
@@ -147,7 +242,7 @@ public class SetActionIT {
}
@Test
- public void persist_new_project_setting() {
+ void persist_new_project_setting() {
propertyDb.insertProperty(newGlobalPropertyDto("my.key", "my global value"), null, null, null, null);
ProjectDto project = db.components().insertPrivateProject().getProjectDto();
logInAsProjectAdministrator(project);
@@ -160,7 +255,7 @@ public class SetActionIT {
}
@Test
- public void persist_new_subportfolio_setting() {
+ void persist_new_subportfolio_setting() {
propertyDb.insertProperty(newGlobalPropertyDto("my.key", "my global value"), null, null, null, null);
ComponentDto portfolio = db.components().insertPrivatePortfolio();
ComponentDto subportfolio = db.components().insertSubportfolio(portfolio);
@@ -174,7 +269,7 @@ public class SetActionIT {
}
@Test
- public void persist_project_property_with_project_admin_permission() {
+ void persist_project_property_with_project_admin_permission() {
ProjectDto project = db.components().insertPrivateProject().getProjectDto();
logInAsProjectAdministrator(project);
@@ -184,7 +279,7 @@ public class SetActionIT {
}
@Test
- public void update_existing_project_setting() {
+ void update_existing_project_setting() {
propertyDb.insertProperty(newGlobalPropertyDto("my.key", "my global value"), null, null,
null, null);
ProjectDto project = db.components().insertPrivateProject().getProjectDto();
@@ -199,7 +294,7 @@ public class SetActionIT {
}
@Test
- public void persist_several_multi_value_setting() {
+ void persist_several_multi_value_setting() {
callForMultiValueGlobalSetting("my.key", List.of("first,Value", "second,Value", "third,Value"));
String expectedValue = "first%2CValue,second%2CValue,third%2CValue";
@@ -208,14 +303,14 @@ public class SetActionIT {
}
@Test
- public void persist_one_multi_value_setting() {
+ void persist_one_multi_value_setting() {
callForMultiValueGlobalSetting("my.key", List.of("first,Value"));
assertGlobalSetting("my.key", "first%2CValue");
}
@Test
- public void persist_property_set_setting() {
+ void persist_property_set_setting() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -252,7 +347,7 @@ public class SetActionIT {
}
@Test
- public void update_property_set_setting() {
+ void update_property_set_setting() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -299,7 +394,7 @@ public class SetActionIT {
}
@Test
- public void update_property_set_on_component_setting() {
+ void update_property_set_on_component_setting() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -350,7 +445,7 @@ public class SetActionIT {
}
@Test
- public void persist_multi_value_with_type_logIn() {
+ void persist_multi_value_with_type_logIn() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -370,7 +465,7 @@ public class SetActionIT {
}
@Test
- public void user_setting_is_not_updated() {
+ void user_setting_is_not_updated() {
propertyDb.insertProperty(newGlobalPropertyDto("my.key", "my user value").setUserUuid("42"), null, null,
null, "user_login");
propertyDb.insertProperty(newGlobalPropertyDto("my.key", "my global value"), null, null, null, null);
@@ -382,7 +477,7 @@ public class SetActionIT {
}
@Test
- public void persist_global_property_with_deprecated_key() {
+ void persist_global_property_with_deprecated_key() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.deprecatedKey("my.old.key")
@@ -400,7 +495,7 @@ public class SetActionIT {
}
@Test
- public void persist_JSON_property() {
+ void persist_JSON_property() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -416,7 +511,7 @@ public class SetActionIT {
}
@Test
- public void fail_if_JSON_invalid_for_JSON_property() {
+ void fail_if_JSON_invalid_for_JSON_property() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -439,10 +534,10 @@ public class SetActionIT {
.hasMessage("Provided JSON is invalid");
}
- @Test
- @UseDataProvider("securityJsonProperties")
- public void successfully_validate_json_schema(String securityPropertyKey) {
- String security_custom_config = """
+ @ParameterizedTest
+ @MethodSource("securityJsonProperties")
+ void successfully_validate_json_schema(String securityPropertyKey) {
+ String securityCustomConfig = """
{
"S3649": {
"sources": [
@@ -481,128 +576,14 @@ public class SetActionIT {
.type(PropertyType.JSON)
.build());
- callForGlobalSetting(securityPropertyKey, security_custom_config);
-
- assertGlobalSetting(securityPropertyKey, security_custom_config);
- }
-
- @Test
- @UseDataProvider("securityJsonProperties")
- public void fail_json_schema_validation_when_property_has_incorrect_type(String securityPropertyKey) {
- String security_custom_config = """
- {
- "S3649": {
- "sources": [
- {
- "methodId": "My\\\\Namespace\\\\ClassName\\\\ServerRequest::getQuery"
- }
- ],
- "sinks": [
- {
- "methodId": 12345,
- "args": [1]
- }
- ]
- }
- }""";
- definitions.addComponent(PropertyDefinition
- .builder(securityPropertyKey)
- .name("foo")
- .description("desc")
- .category("cat")
- .subCategory("subCat")
- .type(PropertyType.JSON)
- .build());
-
- assertThatThrownBy(() -> callForGlobalSetting(securityPropertyKey, security_custom_config))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessageContaining("expected type: string, actual: integer at line 10, character 21, pointer: #/S3649/sinks/0/methodId");
- }
-
- @Test
- @UseDataProvider("securityJsonProperties")
- public void fail_json_schema_validation_when_sanitizers_have_no_args(String securityPropertyKey) {
- String security_custom_config = """
- {
- "S3649": {
- "sources": [
- {
- "methodId": "My\\\\Namespace\\\\ClassName\\\\ServerRequest::getQuery"
- }
- ],
- "sanitizers": [
- {
- "methodId": "SomeSanitizer"
- }
- ]
- }
- }""";
- definitions.addComponent(PropertyDefinition
- .builder(securityPropertyKey)
- .name("foo")
- .description("desc")
- .category("cat")
- .subCategory("subCat")
- .type(PropertyType.JSON)
- .build());
-
- assertThatThrownBy(() -> callForGlobalSetting(securityPropertyKey, security_custom_config))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessageContaining("required properties are missing: args at line 9, character 8, pointer: #/S3649/sanitizers/0");
- }
-
- @Test
- @UseDataProvider("securityJsonProperties")
- public void fail_json_schema_validation_when_validators_have_empty_args_array(String securityPropertyKey) {
- String security_custom_config = """
- {
- "S3649": {
- "sources": [
- {
- "methodId": "My\\\\Namespace\\\\ClassName\\\\ServerRequest::getQuery"
- }
- ],
- "validators": [
- {
- "methodId": "SomeValidator",
- "args": []
- }
- ]
- }
- }""";
- definitions.addComponent(PropertyDefinition
- .builder(securityPropertyKey)
- .name("foo")
- .description("desc")
- .category("cat")
- .subCategory("subCat")
- .type(PropertyType.JSON)
- .build());
+ callForGlobalSetting(securityPropertyKey, securityCustomConfig);
- assertThatThrownBy(() -> callForGlobalSetting(securityPropertyKey, security_custom_config))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessageContaining("expected minimum items: 1, found only 0 at line 11, character 18, pointer: #/S3649/validators/0/args");
+ assertGlobalSetting(securityPropertyKey, securityCustomConfig);
}
- @Test
- @UseDataProvider("securityJsonProperties")
- public void fail_json_schema_validation_when_property_has_unknown_attribute(String securityPropertyKey) {
- String security_custom_config = """
- {
- "S3649": {
- "sources": [
- {
- "methodId": "My\\\\Namespace\\\\ClassName\\\\ServerRequest::getQuery"
- }
- ],
- "unknown": [
- {
- "methodId": 12345,
- "args": [1]
- }
- ]
- }
- }""";
+ @ParameterizedTest
+ @MethodSource("securityJsonPropertiesForInvalidJsonTest")
+ void fail_json_schema_validation_when_property_has_incorrect_type(String securityPropertyKey, String customConfig, String expectedErrorMessage) {
definitions.addComponent(PropertyDefinition
.builder(securityPropertyKey)
.name("foo")
@@ -612,13 +593,13 @@ public class SetActionIT {
.type(PropertyType.JSON)
.build());
- assertThatThrownBy(() -> callForGlobalSetting(securityPropertyKey, security_custom_config))
+ assertThatThrownBy(() -> callForGlobalSetting(securityPropertyKey, customConfig))
.isInstanceOf(IllegalArgumentException.class)
- .hasMessageContaining("false schema always fails at line 8, character 16, pointer: #/S3649/unknown");
+ .hasMessageContaining(expectedErrorMessage);
}
@Test
- public void persist_global_setting_with_non_ascii_characters() {
+ void persist_global_setting_with_non_ascii_characters() {
callForGlobalSetting("my.key", "fi±∞…");
assertGlobalSetting("my.key", "fi±∞…");
@@ -626,34 +607,34 @@ public class SetActionIT {
}
@Test
- public void fail_when_no_key() {
+ void fail_when_no_key() {
assertThatThrownBy(() -> callForGlobalSetting(null, "my value"))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
- public void fail_when_empty_key_value() {
+ void fail_when_empty_key_value() {
assertThatThrownBy(() -> callForGlobalSetting(" ", "my value"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("The 'key' parameter is missing");
}
@Test
- public void fail_when_no_value() {
+ void fail_when_no_value() {
assertThatThrownBy(() -> callForGlobalSetting("my.key", null))
.isInstanceOf(BadRequestException.class)
.hasMessage("Either 'value', 'values' or 'fieldValues' must be provided");
}
@Test
- public void fail_when_empty_value() {
+ void fail_when_empty_value() {
assertThatThrownBy(() -> callForGlobalSetting("my.key", ""))
.isInstanceOf(BadRequestException.class)
.hasMessage("A non empty value must be provided");
}
@Test
- public void fail_when_one_empty_value_on_multi_value() {
+ void fail_when_one_empty_value_on_multi_value() {
List<String> values = List.of("oneValue", " ", "anotherValue");
assertThatThrownBy(() -> callForMultiValueGlobalSetting("my.key", values))
.isInstanceOf(BadRequestException.class)
@@ -661,7 +642,7 @@ public class SetActionIT {
}
@Test
- public void throw_ForbiddenException_if_not_system_administrator() {
+ void throw_ForbiddenException_if_not_system_administrator() {
userSession.logIn().setNonSystemAdministrator();
assertThatThrownBy(() -> callForGlobalSetting("my.key", "my value"))
@@ -670,7 +651,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_data_and_type_do_not_match() {
+ void fail_when_data_and_type_do_not_match() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -688,7 +669,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_data_and_login_type_with_invalid_logIn() {
+ void fail_when_data_and_login_type_with_invalid_logIn() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -709,7 +690,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_data_and_type_do_not_match_with_unknown_error_key() {
+ void fail_when_data_and_type_do_not_match_with_unknown_error_key() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -727,7 +708,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_global_with_property_only_on_projects() {
+ void fail_when_global_with_property_only_on_projects() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -745,7 +726,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_view_property_when_on_projects_only() {
+ void fail_when_view_property_when_on_projects_only() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -768,7 +749,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_property_with_definition_when_component_qualifier_does_not_match() {
+ void fail_when_property_with_definition_when_component_qualifier_does_not_match() {
PortfolioDto portfolio = db.components().insertPrivatePortfolioDto();
definitions.addComponent(PropertyDefinition
.builder("my.key")
@@ -789,40 +770,40 @@ public class SetActionIT {
}
@Test
- public void succeed_for_property_without_definition_when_set_on_project_component() {
+ void succeed_for_property_without_definition_when_set_on_project_component() {
ProjectDto project = randomPublicOrPrivateProject().getProjectDto();
succeedForPropertyWithoutDefinitionAndValidComponent(project);
}
@Test
- public void fail_for_property_without_definition_when_set_on_directory_component() {
+ void fail_for_property_without_definition_when_set_on_directory_component() {
ProjectData projectData = randomPublicOrPrivateProject();
ComponentDto directory = db.components().insertComponent(ComponentTesting.newDirectory(projectData.getMainBranchComponent(), "A/B"));
failForPropertyWithoutDefinitionOnUnsupportedComponent(projectData.getProjectDto(), directory);
}
@Test
- public void fail_for_property_without_definition_when_set_on_file_component() {
+ void fail_for_property_without_definition_when_set_on_file_component() {
ProjectData projectData = randomPublicOrPrivateProject();
ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(projectData.getMainBranchComponent()));
failForPropertyWithoutDefinitionOnUnsupportedComponent(projectData.getProjectDto(), file);
}
@Test
- public void succeed_for_property_without_definition_when_set_on_view_component() {
+ void succeed_for_property_without_definition_when_set_on_view_component() {
PortfolioDto view = db.components().insertPrivatePortfolioDto();
succeedForPropertyWithoutDefinitionAndValidComponent(view);
}
@Test
- public void succeed_for_property_without_definition_when_set_on_subview_component() {
+ void succeed_for_property_without_definition_when_set_on_subview_component() {
ComponentDto view = db.components().insertPrivatePortfolio();
ComponentDto subview = db.components().insertComponent(ComponentTesting.newSubPortfolio(view));
failForPropertyWithoutDefinitionOnUnsupportedComponent(db.components().getPortfolioDto(view), subview);
}
@Test
- public void fail_for_property_without_definition_when_set_on_projectCopy_component() {
+ void fail_for_property_without_definition_when_set_on_projectCopy_component() {
ComponentDto view = db.components().insertPrivatePortfolio();
ComponentDto projectCopy = db.components().insertComponent(ComponentTesting.newProjectCopy("a", db.components().insertPrivateProject().getMainBranchComponent(), view));
@@ -864,7 +845,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_single_and_multi_value_provided() {
+ void fail_when_single_and_multi_value_provided() {
List<String> value = List.of("Another Value");
assertThatThrownBy(() -> call("my.key", "My Value", value, null, null))
.isInstanceOf(BadRequestException.class)
@@ -872,7 +853,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_multi_definition_and_single_value_provided() {
+ void fail_when_multi_definition_and_single_value_provided() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -888,7 +869,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_single_definition_and_multi_value_provided() {
+ void fail_when_single_definition_and_multi_value_provided() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -904,7 +885,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_empty_values_on_one_property_set() {
+ void fail_when_empty_values_on_one_property_set() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -933,7 +914,7 @@ public class SetActionIT {
}
@Test
- public void do_not_fail_when_only_one_empty_value_on_one_property_set() {
+ void do_not_fail_when_only_one_empty_value_on_one_property_set() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -962,14 +943,14 @@ public class SetActionIT {
}
@Test
- public void fail_when_property_set_setting_is_not_defined() {
+ void fail_when_property_set_setting_is_not_defined() {
assertThatThrownBy(() -> callForGlobalPropertySet("my.key", singletonList("{\"field\":\"value\"}")))
.isInstanceOf(BadRequestException.class)
.hasMessage("Setting 'my.key' is undefined");
}
@Test
- public void fail_when_property_set_with_unknown_field() {
+ void fail_when_property_set_with_unknown_field() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -992,7 +973,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_property_set_has_field_with_incorrect_type() {
+ void fail_when_property_set_has_field_with_incorrect_type() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -1015,7 +996,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_property_set_has_a_null_field_value() {
+ void fail_when_property_set_has_a_null_field_value() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -1038,7 +1019,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_property_set_with_invalid_json() {
+ void fail_when_property_set_with_invalid_json() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -1062,7 +1043,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_property_set_with_json_of_the_wrong_format() {
+ void fail_when_property_set_with_json_of_the_wrong_format() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -1086,7 +1067,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_property_set_on_component_of_global_setting() {
+ void fail_when_property_set_on_component_of_global_setting() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -1108,7 +1089,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_component_not_found() {
+ void fail_when_component_not_found() {
TestRequest testRequest = ws.newRequest()
.setParam("key", "foo")
.setParam("value", "2")
@@ -1119,7 +1100,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_setting_key_is_defined_in_sonar_properties() {
+ void fail_when_setting_key_is_defined_in_sonar_properties() {
ProjectDto project = db.components().insertPrivateProject().getProjectDto();
logInAsProjectAdministrator(project);
String settingKey = ProcessProperties.Property.JDBC_URL.getKey();
@@ -1133,8 +1114,7 @@ public class SetActionIT {
.hasMessage(format("Setting '%s' can only be used in sonar.properties", settingKey));
}
- @DataProvider
- public static Object[][] forbiddenProperties() {
+ static Object[][] forbiddenProperties() {
return new Object[][] {
{GITLAB_AUTH_URL},
{GITHUB_API_URL},
@@ -1143,9 +1123,9 @@ public class SetActionIT {
};
}
- @Test
- @UseDataProvider("forbiddenProperties")
- public void fail_when_setting_key_is_forbidden(String property) {
+ @ParameterizedTest
+ @MethodSource("forbiddenProperties")
+ void fail_when_setting_key_is_forbidden(String property) {
TestRequest testRequest = ws.newRequest()
.setParam("key", property)
.setParam("value", "value");
@@ -1155,7 +1135,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_setting_key_is_forbidden() {
+ void fail_when_setting_key_is_forbidden() {
TestRequest testRequest = ws.newRequest()
.setParam("key", "sonar.auth.gitlab.url")
.setParam("value", "http://malicious.url");
@@ -1165,7 +1145,7 @@ public class SetActionIT {
}
@Test
- public void definition() {
+ void definition() {
WebService.Action definition = ws.getDef();
assertThat(definition.key()).isEqualTo("set");
@@ -1177,7 +1157,7 @@ public class SetActionIT {
}
@Test
- public void call_whenEmailPropertyValid_shouldSucceed() {
+ void call_whenEmailPropertyValid_shouldSucceed() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -1190,7 +1170,7 @@ public class SetActionIT {
}
@Test
- public void call_whenEmailPropertyInvalid_shouldFail() {
+ void call_whenEmailPropertyInvalid_shouldFail() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/webhook/ws/CreateActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/webhook/ws/CreateActionIT.java
index e4d567ae1c5..09fd7d33c2a 100644
--- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/webhook/ws/CreateActionIT.java
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/webhook/ws/CreateActionIT.java
@@ -22,9 +22,7 @@ package org.sonar.server.webhook.ws;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.api.config.Configuration;
-import org.sonar.server.component.ComponentTypes;
import org.sonar.api.server.ws.WebService;
-import org.sonar.db.permission.ProjectPermission;
import org.sonar.core.util.UuidFactory;
import org.sonar.core.util.UuidFactoryFast;
import org.sonar.db.DbClient;
@@ -32,12 +30,15 @@ import org.sonar.db.DbTester;
import org.sonar.db.component.ComponentDbTester;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.permission.GlobalPermission;
+import org.sonar.db.permission.ProjectPermission;
import org.sonar.db.project.ProjectDto;
import org.sonar.db.webhook.WebhookDbTester;
import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.component.ComponentTypes;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.network.NetworkInterfaceProvider;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.ws.TestRequest;
import org.sonar.server.ws.WsActionTester;
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/webhook/ws/DeleteActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/webhook/ws/DeleteActionIT.java
index f77251f2b84..3642cc8e383 100644
--- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/webhook/ws/DeleteActionIT.java
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/webhook/ws/DeleteActionIT.java
@@ -24,12 +24,12 @@ import org.junit.Rule;
import org.junit.Test;
import org.sonar.api.config.Configuration;
import org.sonar.api.server.ws.WebService;
-import org.sonar.db.permission.ProjectPermission;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.DbTester;
import org.sonar.db.component.ComponentDbTester;
import org.sonar.db.permission.GlobalPermission;
+import org.sonar.db.permission.ProjectPermission;
import org.sonar.db.project.ProjectDto;
import org.sonar.db.webhook.WebhookDbTester;
import org.sonar.db.webhook.WebhookDeliveryDao;
@@ -38,6 +38,7 @@ import org.sonar.db.webhook.WebhookDto;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.network.NetworkInterfaceProvider;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.ws.TestRequest;
import org.sonar.server.ws.TestResponse;
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/webhook/ws/ListActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/webhook/ws/ListActionIT.java
index b43dc1ab72b..e47df2a6677 100644
--- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/webhook/ws/ListActionIT.java
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/webhook/ws/ListActionIT.java
@@ -23,23 +23,24 @@ import java.util.List;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.api.config.Configuration;
-import org.sonar.server.component.ComponentTypes;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.server.ws.WebService.Param;
-import org.sonar.db.permission.ProjectPermission;
import org.sonar.db.DbClient;
import org.sonar.db.DbTester;
import org.sonar.db.component.ComponentDbTester;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.permission.GlobalPermission;
+import org.sonar.db.permission.ProjectPermission;
import org.sonar.db.project.ProjectDto;
import org.sonar.db.webhook.WebhookDbTester;
import org.sonar.db.webhook.WebhookDeliveryDbTester;
import org.sonar.db.webhook.WebhookDto;
import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.component.ComponentTypes;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.network.NetworkInterfaceProvider;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.ws.TestRequest;
import org.sonar.server.ws.WsActionTester;
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/webhook/ws/UpdateActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/webhook/ws/UpdateActionIT.java
index b9314bde13b..c5bdcaee357 100644
--- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/webhook/ws/UpdateActionIT.java
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/webhook/ws/UpdateActionIT.java
@@ -23,20 +23,21 @@ import java.util.Optional;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.api.config.Configuration;
-import org.sonar.server.component.ComponentTypes;
import org.sonar.api.server.ws.WebService;
-import org.sonar.db.permission.ProjectPermission;
import org.sonar.db.DbClient;
import org.sonar.db.DbTester;
import org.sonar.db.component.ComponentDbTester;
import org.sonar.db.permission.GlobalPermission;
+import org.sonar.db.permission.ProjectPermission;
import org.sonar.db.project.ProjectDto;
import org.sonar.db.webhook.WebhookDbTester;
import org.sonar.db.webhook.WebhookDto;
import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.component.ComponentTypes;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.network.NetworkInterfaceProvider;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.ws.TestRequest;
import org.sonar.server.ws.TestResponse;
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/feature/ws/ListAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/feature/ws/ListAction.java
index 0d4219e0805..a7db09fc823 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/feature/ws/ListAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/feature/ws/ListAction.java
@@ -51,7 +51,7 @@ public class ListAction implements WsAction {
try (JsonWriter json = response.newJsonWriter()) {
json.beginArray();
sonarQubeFeatures.stream()
- .filter(SonarQubeFeature::isAvailable)
+ .filter(SonarQubeFeature::isEnabled)
.forEach(f -> json.value(f.getName()));
json.endArray();
}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/TransitionService.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/TransitionService.java
index 0e84d93e923..4da0c931330 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/TransitionService.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/TransitionService.java
@@ -20,20 +20,25 @@
package org.sonar.server.issue;
import java.util.List;
+import java.util.Set;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.IssueChangeContext;
+import org.sonar.core.rule.RuleType;
+import org.sonar.db.permission.ProjectPermission;
import org.sonar.server.issue.workflow.IssueWorkflow;
import org.sonar.server.issue.workflow.WorkflowTransition;
-import org.sonar.server.issue.workflow.statemachine.Transition;
import org.sonar.server.user.UserSession;
import static java.util.Objects.requireNonNull;
+import static org.sonar.server.issue.workflow.codequalityissue.CodeQualityIssueWorkflowTransition.CONFIRM;
+import static org.sonar.server.issue.workflow.codequalityissue.CodeQualityIssueWorkflowTransition.UNCONFIRM;
/**
* This service is a kind of overlay of {@link IssueWorkflow} that also deals with permission checking
*/
public class TransitionService {
+ public static final Set<String> CONFIRM_TRANSITION_KEYS = Set.of(UNCONFIRM.getKey(), CONFIRM.getKey());
private final UserSession userSession;
private final IssueWorkflow workflow;
@@ -44,11 +49,16 @@ public class TransitionService {
public List<String> listTransitionKeys(DefaultIssue issue) {
String projectUuid = requireNonNull(issue.projectUuid());
- return workflow.outTransitions(issue)
+ return workflow.outTransitionsKeys(issue)
.stream()
- .filter(transition -> (userSession.isLoggedIn() && transition.requiredProjectPermission() == null)
- || (transition.requiredProjectPermission() != null && userSession.hasComponentUuidPermission(transition.requiredProjectPermission(), projectUuid)))
- .map(Transition::key)
+ .filter(key -> {
+ // Confirm is an exception and is accessible to any logged-in user
+ if (CONFIRM_TRANSITION_KEYS.contains(key)) {
+ return userSession.isLoggedIn();
+ } else {
+ return userSession.hasComponentUuidPermission(getProjectPermissionForIssueType(issue), projectUuid);
+ }
+ })
.toList();
}
@@ -66,10 +76,24 @@ public class TransitionService {
public void checkTransitionPermission(String transitionKey, DefaultIssue defaultIssue) {
String projectUuid = requireNonNull(defaultIssue.projectUuid());
- workflow.outTransitions(defaultIssue)
+ workflow.outTransitionsKeys(defaultIssue)
.stream()
- .filter(transition -> transition.key().equals(transitionKey) && transition.requiredProjectPermission() != null)
- .forEach(transition -> userSession.checkComponentUuidPermission(transition.requiredProjectPermission(), projectUuid));
+ .filter(key -> key.equals(transitionKey))
+ .forEach(transition -> {
+ // Confirm is an exception and is accessible to any logged-in user
+ if (CONFIRM_TRANSITION_KEYS.contains(transitionKey)) {
+ return;
+ }
+ userSession.checkComponentUuidPermission(getProjectPermissionForIssueType(defaultIssue), projectUuid);
+ });
+ }
+
+ private static ProjectPermission getProjectPermissionForIssueType(DefaultIssue defaultIssue) {
+ return isSecurityHotspot(defaultIssue) ? ProjectPermission.SECURITYHOTSPOT_ADMIN : ProjectPermission.ISSUE_ADMIN;
+ }
+
+ private static boolean isSecurityHotspot(DefaultIssue issue) {
+ return issue.type() == RuleType.SECURITY_HOTSPOT;
}
}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java
index c81bb3c4908..d86d4653534 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java
@@ -103,10 +103,8 @@ import static org.sonar.server.issue.workflow.codequalityissue.CodeQualityIssueW
import static org.sonar.server.issue.workflow.codequalityissue.CodeQualityIssueWorkflowTransition.RESOLVE;
import static org.sonar.server.issue.workflow.codequalityissue.CodeQualityIssueWorkflowTransition.UNCONFIRM;
import static org.sonar.server.issue.workflow.codequalityissue.CodeQualityIssueWorkflowTransition.WONT_FIX;
-import static org.sonar.server.issue.workflow.securityhotspot.SecurityHotspotWorkflowTransition.OPEN_AS_VULNERABILITY;
import static org.sonar.server.issue.workflow.securityhotspot.SecurityHotspotWorkflowTransition.RESET_AS_TO_REVIEW;
import static org.sonar.server.issue.workflow.securityhotspot.SecurityHotspotWorkflowTransition.RESOLVE_AS_REVIEWED;
-import static org.sonar.server.issue.workflow.securityhotspot.SecurityHotspotWorkflowTransition.SET_AS_IN_REVIEW;
import static org.sonar.server.ws.WsUtils.writeProtobuf;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_BULK_CHANGE;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ADD_TAGS;
@@ -162,7 +160,7 @@ public class BulkChangeAction implements IssuesWsAction {
new Change("10.4", "Transition '%s' is now supported.".formatted(ACCEPT)),
new Change("10.2", format("Parameters '%s' and '%s' are now deprecated.", PARAM_SET_SEVERITY, PARAM_SET_TYPE)),
new Change("8.2", "Security hotspots are no longer supported and will be ignored."),
- new Change("8.2", format("Transitions '%s', '%s' and '%s' are no more supported", SET_AS_IN_REVIEW, RESOLVE_AS_REVIEWED, OPEN_AS_VULNERABILITY)),
+ new Change("8.2", format("Transitions '%s', '%s' and '%s' are no more supported", "setinreview", RESOLVE_AS_REVIEWED, "openasvulnerability")),
new Change("6.3", "'actions' parameter is ignored"))
.setHandler(this)
.setResponseExample(getClass().getResource("bulk_change-example.json"))
@@ -194,7 +192,6 @@ public class BulkChangeAction implements IssuesWsAction {
RESOLVE.getKey(),
FALSE_POSITIVE.getKey(),
WONT_FIX.getKey(),
- SET_AS_IN_REVIEW.getKey(),
RESOLVE_AS_REVIEWED.getKey(),
RESET_AS_TO_REVIEW.getKey(),
ACCEPT.getKey()));
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java
index db6e3d28029..12dfc14d9ec 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java
@@ -51,10 +51,8 @@ import static org.sonar.server.issue.workflow.codequalityissue.CodeQualityIssueW
import static org.sonar.server.issue.workflow.codequalityissue.CodeQualityIssueWorkflowTransition.RESOLVE;
import static org.sonar.server.issue.workflow.codequalityissue.CodeQualityIssueWorkflowTransition.UNCONFIRM;
import static org.sonar.server.issue.workflow.codequalityissue.CodeQualityIssueWorkflowTransition.WONT_FIX;
-import static org.sonar.server.issue.workflow.securityhotspot.SecurityHotspotWorkflowTransition.OPEN_AS_VULNERABILITY;
import static org.sonar.server.issue.workflow.securityhotspot.SecurityHotspotWorkflowTransition.RESET_AS_TO_REVIEW;
import static org.sonar.server.issue.workflow.securityhotspot.SecurityHotspotWorkflowTransition.RESOLVE_AS_REVIEWED;
-import static org.sonar.server.issue.workflow.securityhotspot.SecurityHotspotWorkflowTransition.SET_AS_IN_REVIEW;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_DO_TRANSITION;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_TRANSITION;
@@ -104,8 +102,8 @@ public class DoTransitionAction implements IssuesWsAction {
new Change("10.2", "Add 'impacts', 'cleanCodeAttribute', 'cleanCodeAttributeCategory' fields to the response"),
new Change("9.6", "Response field 'ruleDescriptionContextKey' added"),
new Change("8.8", "The response field components.uuid is removed"),
- new Change("8.1", format("transitions '%s' and '%s' are no more supported", SET_AS_IN_REVIEW, OPEN_AS_VULNERABILITY)),
- new Change("7.8", format("added '%s', %s, %s and %s transitions for security hotspots ", SET_AS_IN_REVIEW, RESOLVE_AS_REVIEWED, OPEN_AS_VULNERABILITY, RESET_AS_TO_REVIEW)),
+ new Change("8.1", format("transitions '%s' and '%s' are no more supported", "setinreview", "openasvulnerability")),
+ new Change("7.8", format("added '%s', %s, %s and %s transitions for security hotspots ", "setinreview", RESOLVE_AS_REVIEWED, "openasvulnerability", RESET_AS_TO_REVIEW)),
new Change("7.3", "added transitions for security hotspots"),
new Change("6.5", "the database ids of the components are removed from the response"),
new Change("6.5", "the response field components.uuid is deprecated. Use components.key instead."))
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java
index f16ce3bfaf5..5fe19dd0908 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java
@@ -37,7 +37,6 @@ import org.sonar.api.issue.IssueStatus;
import org.sonar.api.issue.impact.SoftwareQuality;
import org.sonar.api.rule.Severity;
import org.sonar.api.rules.CleanCodeAttributeCategory;
-import org.sonar.core.rule.RuleType;
import org.sonar.api.server.ws.Change;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
@@ -45,6 +44,7 @@ import org.sonar.api.server.ws.WebService;
import org.sonar.api.server.ws.WebService.Param;
import org.sonar.api.utils.Paging;
import org.sonar.api.utils.System2;
+import org.sonar.core.rule.RuleType;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.user.UserDto;
@@ -72,7 +72,6 @@ import static java.util.Optional.ofNullable;
import static org.sonar.api.issue.Issue.RESOLUTIONS;
import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
import static org.sonar.api.issue.Issue.RESOLUTION_REMOVED;
-import static org.sonar.api.issue.Issue.STATUS_IN_REVIEW;
import static org.sonar.api.issue.Issue.STATUS_OPEN;
import static org.sonar.api.issue.Issue.STATUS_REOPENED;
import static org.sonar.api.issue.Issue.STATUS_REVIEWED;
@@ -126,6 +125,7 @@ import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_LANGUAGES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ON_COMPONENT_ONLY;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_ASVS_40;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_ASVS_LEVEL;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_MOBILE_TOP_10_2024;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_TOP_10;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_TOP_10_2021;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PCI_DSS_32;
@@ -169,6 +169,7 @@ public class SearchAction implements IssuesWsAction {
PARAM_PCI_DSS_32,
PARAM_PCI_DSS_40,
PARAM_OWASP_ASVS_40,
+ PARAM_OWASP_MOBILE_TOP_10_2024,
PARAM_OWASP_TOP_10,
PARAM_OWASP_TOP_10_2021,
PARAM_STIG_ASD_V5R3,
@@ -188,6 +189,7 @@ public class SearchAction implements IssuesWsAction {
"the componentKeys parameter. ";
private static final String NEW_FACET_ADDED_MESSAGE = "Facet '%s' has been added";
private static final String NEW_PARAM_ADDED_MESSAGE = "Param '%s' has been added";
+ private static final String V_2025_3 = "2025.3";
private static final Set<String> FACETS_REQUIRING_PROJECT = newHashSet(PARAM_FILES, PARAM_DIRECTORIES);
private final UserSession userSession;
@@ -222,6 +224,8 @@ public class SearchAction implements IssuesWsAction {
+ "<br/>When issue indexing is in progress returns 503 service unavailable HTTP code.")
.setSince("3.6")
.setChangelog(
+ new Change(V_2025_3, format(NEW_FACET_ADDED_MESSAGE, PARAM_OWASP_MOBILE_TOP_10_2024)),
+ new Change(V_2025_3, format(NEW_PARAM_ADDED_MESSAGE, PARAM_OWASP_MOBILE_TOP_10_2024)),
new Change("10.8", "The response fields 'severity' and 'type' are not deprecated anymore.."),
new Change("10.8", "The fields 'severity' and 'type' are not deprecated anymore."),
new Change("10.8", format("The parameters '%s' and '%s' are not deprecated anymore.", PARAM_SEVERITIES, PARAM_TYPES)),
@@ -284,7 +288,7 @@ public class SearchAction implements IssuesWsAction {
"api/hotspots"),
new Change("8.2", "response field 'fromHotspot' has been deprecated and is no more populated"),
new Change("8.2", "Status 'IN_REVIEW' for Security Hotspots has been deprecated"),
- new Change("7.8", format("added new Security Hotspots statuses : %s, %s and %s", STATUS_TO_REVIEW, STATUS_IN_REVIEW,
+ new Change("7.8", format("added new Security Hotspots statuses : %s, %s and %s", STATUS_TO_REVIEW, "IN_REVIEW",
STATUS_REVIEWED)),
new Change("7.8", "Security hotspots are returned by default"),
new Change("7.7", format("Value 'authors' in parameter '%s' is deprecated, please use '%s' instead", FACETS, PARAM_AUTHOR)),
@@ -379,6 +383,10 @@ public class SearchAction implements IssuesWsAction {
.setDescription("Comma-separated list of OWASP ASVS v4.0 categories.")
.setSince("9.7")
.setExampleValue("6,10.1.1");
+ action.createParam(PARAM_OWASP_MOBILE_TOP_10_2024)
+ .setDescription("Comma-separated list of OWASP Mobile Top 10 2024 lowercase categories.")
+ .setSince(V_2025_3)
+ .setPossibleValues("m1", "m2", "m3", "m4", "m5", "m6", "m7", "m8", "m9", "m10");
action.createParam(PARAM_OWASP_TOP_10)
.setDescription("Comma-separated list of OWASP Top 10 2017 lowercase categories.")
.setSince("7.3")
@@ -621,6 +629,7 @@ public class SearchAction implements IssuesWsAction {
addMandatoryValuesToFacet(facets, PARAM_PCI_DSS_32, request.getPciDss32());
addMandatoryValuesToFacet(facets, PARAM_PCI_DSS_40, request.getPciDss40());
addMandatoryValuesToFacet(facets, PARAM_OWASP_ASVS_40, request.getOwaspAsvs40());
+ addMandatoryValuesToFacet(facets, PARAM_OWASP_MOBILE_TOP_10_2024, request.getOwaspMobileTop10For2024());
addMandatoryValuesToFacet(facets, PARAM_OWASP_TOP_10, request.getOwaspTop10());
addMandatoryValuesToFacet(facets, PARAM_OWASP_TOP_10_2021, request.getOwaspTop10For2021());
addMandatoryValuesToFacet(facets, PARAM_STIG_ASD_V5R3, request.getStigAsdV5R3());
@@ -710,6 +719,7 @@ public class SearchAction implements IssuesWsAction {
.setPciDss40(request.paramAsStrings(PARAM_PCI_DSS_40))
.setOwaspAsvsLevel(request.paramAsInt(PARAM_OWASP_ASVS_LEVEL))
.setOwaspAsvs40(request.paramAsStrings(PARAM_OWASP_ASVS_40))
+ .setOwaspMobileTop10For2024(request.paramAsStrings(PARAM_OWASP_MOBILE_TOP_10_2024))
.setOwaspTop10(request.paramAsStrings(PARAM_OWASP_TOP_10))
.setOwaspTop10For2021(request.paramAsStrings(PARAM_OWASP_TOP_10_2021))
.setStigAsdV5R3(request.paramAsStrings(PARAM_STIG_ASD_V5R3))
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java
index a5844238eb2..9414b31e895 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java
@@ -21,36 +21,23 @@ package org.sonar.server.measure.live;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-import javax.annotation.CheckForNull;
-import org.slf4j.LoggerFactory;
-import org.sonar.api.config.Configuration;
import org.sonar.api.measures.Metric;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
-import org.sonar.db.component.BranchDto;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.SnapshotDto;
-import org.sonar.db.measure.MeasureDto;
-import org.sonar.db.metric.MetricDto;
-import org.sonar.db.project.ProjectDto;
import org.sonar.server.es.Indexers;
import org.sonar.server.qualitygate.EvaluatedQualityGate;
-import org.sonar.server.qualitygate.QualityGate;
import org.sonar.server.qualitygate.changeevent.QGChangeEvent;
import org.sonar.server.setting.ProjectConfigurationLoader;
import static java.util.Collections.emptyList;
import static java.util.Collections.singleton;
import static java.util.stream.Collectors.groupingBy;
-import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY;
public class LiveMeasureComputerImpl implements LiveMeasureComputer {
@@ -96,81 +83,36 @@ public class LiveMeasureComputerImpl implements LiveMeasureComputer {
return Optional.empty();
}
- BranchDto branch = loadBranch(dbSession, branchComponent);
- ProjectDto project = loadProject(dbSession, branch.getProjectUuid());
- Configuration config = projectConfigurationLoader.loadBranchConfiguration(dbSession, branch);
- QualityGate qualityGate = qGateComputer.loadQualityGate(dbSession, project, branch);
- MeasureMatrix matrix = loadMeasureMatrix(dbSession, components.getAllUuids(), qualityGate);
-
- treeUpdater.update(dbSession, lastAnalysis.get(), config, components, branch, matrix);
-
- Metric.Level previousStatus = loadPreviousStatus(dbSession, branchComponent);
- EvaluatedQualityGate evaluatedQualityGate = qGateComputer.refreshGateStatus(branchComponent, qualityGate, matrix, config);
- persistAndIndex(dbSession, matrix, branch);
-
- return Optional.of(new QGChangeEvent(project, branch, lastAnalysis.get(), config, previousStatus, () -> Optional.of(evaluatedQualityGate)));
- }
-
- private MeasureMatrix loadMeasureMatrix(DbSession dbSession, Set<String> componentUuids, QualityGate qualityGate) {
- Collection<String> metricKeys = getKeysOfAllInvolvedMetrics(qualityGate);
- Map<String, MetricDto> metricPerKey =
- dbClient.metricDao().selectByKeys(dbSession, metricKeys).stream().collect(Collectors.toMap(MetricDto::getKey, Function.identity()));
- List<MeasureDto> measures = dbClient.measureDao()
- .selectByComponentUuidsAndMetricKeys(dbSession, componentUuids, metricPerKey.keySet());
- return new MeasureMatrix(componentUuids, metricPerKey.values(), measures);
- }
-
- private void persistAndIndex(DbSession dbSession, MeasureMatrix matrix, BranchDto branch) {
- // persist the measures that have been created or updated
- Map<String, MeasureDto> measureDtoPerComponent = new HashMap<>();
- matrix.getChanged().sorted(MeasureMatrix.Measure.COMPARATOR)
- .filter(m -> m.getValue() != null)
- .forEach(m -> measureDtoPerComponent.compute(m.getComponentUuid(), (componentUuid, measureDto) -> {
- if (measureDto == null) {
- measureDto = new MeasureDto()
- .setComponentUuid(componentUuid)
- .setBranchUuid(m.getBranchUuid());
- }
- return measureDto.addValue(
- m.getMetricKey(),
- m.getValue()
- );
- }));
- measureDtoPerComponent.values().forEach(m -> dbClient.measureDao().insertOrUpdate(dbSession, m));
- projectIndexer.commitAndIndexBranches(dbSession, singleton(branch), Indexers.BranchEvent.MEASURE_CHANGE);
- }
-
- @CheckForNull
- private Metric.Level loadPreviousStatus(DbSession dbSession, ComponentDto branchComponent) {
- return dbClient.measureDao().selectByComponentUuid(dbSession, branchComponent.uuid())
- .map(m -> m.getString(ALERT_STATUS_KEY))
- .map(m -> {
- try {
- return Metric.Level.valueOf(m);
- } catch (IllegalArgumentException e) {
- LoggerFactory.getLogger(LiveMeasureComputerImpl.class).trace("Failed to parse value of metric '{}'", ALERT_STATUS_KEY, e);
- return null;
- }
- })
- .orElse(null);
- }
-
- private Set<String> getKeysOfAllInvolvedMetrics(QualityGate gate) {
- Set<String> metricKeys = new HashSet<>();
- for (Metric<?> metric : formulaFactory.getFormulaMetrics()) {
- metricKeys.add(metric.getKey());
- }
- metricKeys.addAll(qGateComputer.getMetricsRelatedTo(gate));
- return metricKeys;
- }
-
- private BranchDto loadBranch(DbSession dbSession, ComponentDto branchComponent) {
- return dbClient.branchDao().selectByUuid(dbSession, branchComponent.uuid())
- .orElseThrow(() -> new IllegalStateException("Branch not found: " + branchComponent.uuid()));
- }
-
- private ProjectDto loadProject(DbSession dbSession, String uuid) {
- return dbClient.projectDao().selectByUuid(dbSession, uuid)
- .orElseThrow(() -> new IllegalStateException("Project not found: " + uuid));
+ LiveMeasureUpdaterWorkflow liveMeasureUpdaterWorkflow = LiveMeasureUpdaterWorkflow.build(
+ dbClient,
+ dbSession,
+ branchComponent,
+ projectConfigurationLoader,
+ qGateComputer);
+
+ Set<String> metricKeys = liveMeasureUpdaterWorkflow.getKeysOfAllInvolvedMetrics(formulaFactory);
+ MeasureMatrix matrix = liveMeasureUpdaterWorkflow.buildMeasureMatrix(
+ metricKeys,
+ components.getAllUuids());
+
+ treeUpdater.update(
+ dbSession,
+ lastAnalysis.get(),
+ liveMeasureUpdaterWorkflow.getConfig(),
+ components,
+ liveMeasureUpdaterWorkflow.getBranchDto(),
+ matrix);
+
+ Metric.Level previousStatus = liveMeasureUpdaterWorkflow.loadPreviousStatus();
+ EvaluatedQualityGate evaluatedQualityGate = liveMeasureUpdaterWorkflow.updateQualityGateMeasures(matrix);
+
+ projectIndexer.commitAndIndexBranches(dbSession, singleton(liveMeasureUpdaterWorkflow.getBranchDto()), Indexers.BranchEvent.MEASURE_CHANGE);
+
+ return Optional.of(new QGChangeEvent(
+ liveMeasureUpdaterWorkflow.getProjectDto(),
+ liveMeasureUpdaterWorkflow.getBranchDto(),
+ lastAnalysis.get(),
+ liveMeasureUpdaterWorkflow.getConfig(),
+ previousStatus, () -> Optional.of(evaluatedQualityGate)));
}
}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureUpdaterWorkflow.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureUpdaterWorkflow.java
new file mode 100644
index 00000000000..26906ab9c35
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureUpdaterWorkflow.java
@@ -0,0 +1,204 @@
+/*
+ * 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.server.measure.live;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import javax.annotation.CheckForNull;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.measures.Metric;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.measure.MeasureDto;
+import org.sonar.db.metric.MetricDto;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.server.qualitygate.EvaluatedQualityGate;
+import org.sonar.server.qualitygate.QualityGate;
+import org.sonar.server.setting.ProjectConfigurationLoader;
+
+import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY;
+
+/**
+ * This class breaks apart the various steps required to update an
+ * existing quality gate's measures with new ones:
+ * <ul>
+ * <li>
+ * Get related metric keys
+ * <ul><li>After this step is where you would inject additional keys</li></ul>
+ * </li>
+ * <li>
+ * Build a "measure matrix", a special hash table that contains the original measures mapped to the related metric keys
+ * <ul><li>This matrix is then updated with new measure values</li></ul>
+ * </li>
+ * <li>
+ * Load the previous quality gate status
+ * </li>
+ * <li>
+ * Update the quality gate's measures, persisting them to the database
+ * </li>
+ * </ul>
+ */
+public class LiveMeasureUpdaterWorkflow {
+ private final Configuration config;
+ private final QualityGate qualityGate;
+ private final DbClient dbClient;
+ private final DbSession dbSession;
+ private final LiveQualityGateComputer qualityGateComputer;
+ private final Dtos dtos;
+
+ private LiveMeasureUpdaterWorkflow(
+ Dtos dtos,
+ DbClient dbClient,
+ DbSession dbSession,
+ Configuration config,
+ QualityGate qualityGate,
+ LiveQualityGateComputer qualityGateComputer) {
+ this.dtos = dtos;
+ this.config = config;
+ this.qualityGate = qualityGate;
+ this.dbClient = dbClient;
+ this.dbSession = dbSession;
+ this.qualityGateComputer = qualityGateComputer;
+ }
+
+ public static LiveMeasureUpdaterWorkflow build(
+ DbClient dbClient,
+ DbSession dbSession,
+ ComponentDto branchComponentDto,
+ ProjectConfigurationLoader projectConfigurationLoader,
+ LiveQualityGateComputer qualityGateComputer) {
+ BranchDto branchDto = loadBranch(dbClient, dbSession, branchComponentDto);
+ ProjectDto projectDto = loadProject(dbClient, dbSession, branchDto.getProjectUuid());
+ Configuration config = projectConfigurationLoader.loadBranchConfiguration(dbSession, branchDto);
+ QualityGate qualityGate = qualityGateComputer.loadQualityGate(dbSession, projectDto, branchDto);
+
+ return new LiveMeasureUpdaterWorkflow(
+ new Dtos(branchComponentDto, branchDto, projectDto),
+ dbClient,
+ dbSession,
+ config,
+ qualityGate,
+ qualityGateComputer);
+ }
+
+ private static BranchDto loadBranch(
+ DbClient dbClient,
+ DbSession dbSession,
+ ComponentDto branchComponent) {
+ return dbClient.branchDao().selectByUuid(dbSession, branchComponent.uuid())
+ .orElseThrow(() -> new IllegalStateException("Branch not found: " + branchComponent.uuid()));
+ }
+
+ private static ProjectDto loadProject(
+ DbClient dbClient,
+ DbSession dbSession,
+ String uuid) {
+ return dbClient.projectDao().selectByUuid(dbSession, uuid)
+ .orElseThrow(() -> new IllegalStateException("Project not found: " + uuid));
+ }
+
+ public BranchDto getBranchDto() {
+ return dtos.branchDto;
+ }
+
+ public ProjectDto getProjectDto() {
+ return dtos.projectDto;
+ }
+
+ public Configuration getConfig() {
+ return config;
+ }
+
+ public EvaluatedQualityGate updateQualityGateMeasures(MeasureMatrix matrix) {
+ var result = qualityGateComputer.refreshGateStatus(
+ dtos.branchComponentDto,
+ qualityGate,
+ matrix,
+ config);
+
+ persistUpdatedMeasures(matrix);
+
+ return result;
+ }
+
+ public Set<String> getKeysOfAllInvolvedMetrics(MeasureUpdateFormulaFactory formulaFactory) {
+ Set<String> metricKeys = new HashSet<>();
+ for (Metric<?> metric : formulaFactory.getFormulaMetrics()) {
+ metricKeys.add(metric.getKey());
+ }
+ metricKeys.addAll(qualityGateComputer.getMetricsRelatedTo(qualityGate));
+
+ return metricKeys;
+ }
+
+ public MeasureMatrix buildMeasureMatrix(
+ Collection<String> metricKeys,
+ Set<String> branchUuids) {
+ Map<String, MetricDto> metricPerKey = dbClient.metricDao().selectByKeys(dbSession, metricKeys).stream().collect(Collectors.toMap(MetricDto::getKey, Function.identity()));
+ List<MeasureDto> measures = dbClient.measureDao()
+ .selectByComponentUuidsAndMetricKeys(dbSession, branchUuids, metricPerKey.keySet());
+ return new MeasureMatrix(branchUuids, metricPerKey.values(), measures);
+ }
+
+ private void persistUpdatedMeasures(MeasureMatrix matrix) {
+ // persist the measures that have been created or updated
+ Map<String, MeasureDto> measureDtoPerComponent = new HashMap<>();
+ matrix.getChanged().sorted(MeasureMatrix.Measure.COMPARATOR)
+ .filter(m -> m.getValue() != null)
+ .forEach(m -> measureDtoPerComponent.compute(m.getComponentUuid(), (componentUuid, measureDto) -> {
+ if (measureDto == null) {
+ measureDto = new MeasureDto()
+ .setComponentUuid(componentUuid)
+ .setBranchUuid(m.getBranchUuid());
+ }
+ return measureDto.addValue(
+ m.getMetricKey(),
+ m.getValue());
+ }));
+ measureDtoPerComponent.values().forEach(m -> dbClient.measureDao().insertOrUpdate(dbSession, m));
+ }
+
+ @CheckForNull
+ public Metric.Level loadPreviousStatus() {
+ return dbClient.measureDao().selectByComponentUuid(dbSession, dtos.branchDto.getUuid())
+ .map(m -> m.getString(ALERT_STATUS_KEY))
+ .map(m -> {
+ try {
+ return Metric.Level.valueOf(m);
+ } catch (IllegalArgumentException e) {
+ LoggerFactory.getLogger(LiveMeasureUpdaterWorkflow.class).trace("Failed to parse value of metric '{}'", ALERT_STATUS_KEY, e);
+ return null;
+ }
+ })
+ .orElse(null);
+ }
+
+ private record Dtos(ComponentDto branchComponentDto, BranchDto branchDto, ProjectDto projectDto) {
+ }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java
index da4df11d0fa..fc82532d94e 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java
@@ -51,17 +51,17 @@ import static java.util.Objects.requireNonNull;
* <li>the refreshed values</li>
* </ul>
*/
-class MeasureMatrix {
+public class MeasureMatrix {
// component uuid -> metric key -> measure
private final Table<String, String, MeasureCell> table;
private final Map<String, MetricDto> metricsByKeys = new HashMap<>();
- MeasureMatrix(Collection<ComponentDto> components, Collection<MetricDto> metrics, List<MeasureDto> dbMeasures) {
+ public MeasureMatrix(Collection<ComponentDto> components, Collection<MetricDto> metrics, List<MeasureDto> dbMeasures) {
this(components.stream().map(ComponentDto::uuid).collect(Collectors.toSet()), metrics, dbMeasures);
}
- MeasureMatrix(Set<String> componentUuids, Collection<MetricDto> metrics, List<MeasureDto> dbMeasures) {
+ public MeasureMatrix(Set<String> componentUuids, Collection<MetricDto> metrics, List<MeasureDto> dbMeasures) {
for (MetricDto metric : metrics) {
this.metricsByKeys.put(metric.getKey(), metric);
}
@@ -86,19 +86,19 @@ class MeasureMatrix {
return cell == null ? Optional.empty() : Optional.of(cell.measure);
}
- void setValue(ComponentDto component, String metricKey, double value) {
+ public void setValue(ComponentDto component, String metricKey, double value) {
changeCell(component, metricKey, m -> m.setValue(scale(getMetric(metricKey), value)));
}
- void setValue(ComponentDto component, String metricKey, Rating value) {
+ public void setValue(ComponentDto component, String metricKey, Rating value) {
changeCell(component, metricKey, m -> m.setValue((double) value.getIndex()));
}
- void setValue(ComponentDto component, String metricKey, @Nullable String data) {
+ public void setValue(ComponentDto component, String metricKey, @Nullable String data) {
changeCell(component, metricKey, m -> m.setValue(data));
}
- Stream<Measure> getChanged() {
+ public Stream<Measure> getChanged() {
return table.values().stream()
.filter(Objects::nonNull)
.filter(MeasureCell::isChanged)
@@ -145,8 +145,8 @@ class MeasureMatrix {
}
}
- static class Measure {
- static final Comparator<Measure> COMPARATOR = Comparator
+ public static class Measure {
+ public static final Comparator<Measure> COMPARATOR = Comparator
.comparing((Measure m) -> m.componentUuid)
.thenComparing(m -> m.metricDto.getKey());
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/ActiveVersionEvaluator.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/ActiveVersionEvaluator.java
index 2f045a824ed..01e86f3a35e 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/ActiveVersionEvaluator.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/ActiveVersionEvaluator.java
@@ -19,12 +19,12 @@
*/
package org.sonar.server.platform.ws;
-import com.google.common.collect.Lists;
+import java.time.LocalDate;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
-import java.util.List;
import java.util.SortedSet;
+import org.sonar.api.internal.MetadataLoader;
import org.sonar.api.utils.System2;
import org.sonar.core.platform.SonarQubeVersion;
import org.sonar.updatecenter.common.Product;
@@ -67,11 +67,11 @@ public class ActiveVersionEvaluator {
return initialLtaReleaseDate.after(c.getTime());
} else {
- return compareWithoutPatchVersion(installedVersion, findPreviousReleaseIgnoringPatch(allReleases).getVersion()) >= 0;
+ // if installed version is not LTA or past LTA, check if it is still supported
+ return LocalDate.now().isBefore(LocalDate.parse(MetadataLoader.loadSqVersionEol(system2)));
}
}
-
private static int compareWithoutPatchVersion(Version v1, Version v2) {
return COMPARATOR.compare(v1, v2);
}
@@ -85,24 +85,4 @@ public class ActiveVersionEvaluator {
));
}
- private static Release findPreviousReleaseIgnoringPatch(SortedSet<Release> releases) {
- if (!releases.isEmpty()) {
- Release refRelease = releases.last();
- int patchesOfRefRelease = 0;
- List<Release> sublist = Lists.reverse(releases.stream().toList());
- for (Release release : sublist) {
- int versionComparison = compareWithoutPatchVersion(release.getVersion(), refRelease.getVersion());
- if (versionComparison < 0) {
- return release;
- } else if (versionComparison == 0) {
- patchesOfRefRelease++;
- }
- }
- // if all releases have the same version, return the last one
- if (patchesOfRefRelease == releases.size()) {
- return refRelease;
- }
- }
- throw new IllegalStateException("Unable to find previous release in releases");
- }
}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateConditionsUpdater.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateConditionsUpdater.java
index 1f603b7d63e..28c6d929f5d 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateConditionsUpdater.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateConditionsUpdater.java
@@ -61,6 +61,7 @@ import static org.sonar.server.measure.Rating.E;
import static org.sonar.server.qualitygate.Condition.Operator.GREATER_THAN;
import static org.sonar.server.qualitygate.Condition.Operator.LESS_THAN;
import static org.sonar.server.qualitygate.ValidRatingMetrics.isCoreRatingMetric;
+import static org.sonar.server.qualitygate.ValidRatingMetrics.isScaRatingMetric;
import static org.sonar.server.qualitygate.ValidRatingMetrics.isSoftwareQualityRatingMetric;
public class QualityGateConditionsUpdater {
@@ -228,7 +229,7 @@ public class QualityGateConditionsUpdater {
if (!metric.getValueType().equals(RATING.name())) {
return;
}
- if (!isCoreRatingMetric(metric.getKey()) && !isSoftwareQualityRatingMetric(metric.getKey())) {
+ if (!isCoreRatingMetric(metric.getKey()) && !isSoftwareQualityRatingMetric(metric.getKey()) && !isScaRatingMetric(metric.getKey())) {
errors.add(format("The metric '%s' cannot be used", metric.getShortName()));
}
if (!isValidRating(errorThreshold)) {
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ValidRatingMetrics.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ValidRatingMetrics.java
index f07efd46148..506f8e74b58 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ValidRatingMetrics.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ValidRatingMetrics.java
@@ -38,6 +38,16 @@ public class ValidRatingMetrics {
.map(org.sonar.api.measures.Metric::getKey)
.collect(Collectors.toSet());
+ // TODO: https://sonarsource.atlassian.net/browse/SONAR-25538 remove this hardcoding
+ private static final Set<String> SCA_RATING_METRICS = Set.of(
+ "sca_rating_licensing",
+ "new_sca_rating_licensing",
+ "sca_rating_vulnerability",
+ "new_sca_rating_vulnerability",
+ "sca_rating_any_issue",
+ "new_sca_rating_any_issue"
+ );
+
private ValidRatingMetrics() {
// only static methods
}
@@ -49,4 +59,8 @@ public class ValidRatingMetrics {
public static boolean isSoftwareQualityRatingMetric(String metricKey) {
return SOFTWARE_QUALITY_RATING_METRICS.contains(metricKey);
}
+
+ public static boolean isScaRatingMetric(String metricKey) {
+ return SCA_RATING_METRICS.contains(metricKey);
+ }
}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImpl.java
index dd20b9c78c8..048487d44ba 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImpl.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImpl.java
@@ -106,7 +106,7 @@ public class BuiltInQProfileRepositoryImpl implements BuiltInQProfileRepository
.collect(Collectors.toSet());
checkState(languagesWithoutBuiltInQProfiles.isEmpty(), "The following languages have no built-in quality profiles: %s",
- languagesWithoutBuiltInQProfiles.isEmpty() ? "" : String.join("", languagesWithoutBuiltInQProfiles));
+ languagesWithoutBuiltInQProfiles.isEmpty() ? "" : String.join(", ", languagesWithoutBuiltInQProfiles));
}
private Map<String, Map<String, BuiltInQualityProfile>> validateAndClean(BuiltInQualityProfilesDefinition.Context context) {
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/ActivateRulesAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/ActivateRulesAction.java
index 06ab823555c..deaf79ece99 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/ActivateRulesAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/ActivateRulesAction.java
@@ -109,7 +109,8 @@ public class ActivateRulesAction implements QProfileWsAction {
wsSupport.checkNotBuiltIn(profile);
RuleQuery ruleQuery = ruleQueryFactory.createRuleQuery(dbSession, request);
ruleQuery.setIncludeExternal(false);
- result = qProfileRules.bulkActivateAndCommit(dbSession, profile, ruleQuery, request.param(PARAM_TARGET_SEVERITY), request.paramAsBoolean(PARAM_PRIORITIZED_RULE));
+ result = qProfileRules.bulkActivateAndCommit(dbSession, profile, ruleQuery, request.param(PARAM_TARGET_SEVERITY),
+ request.paramAsBoolean(PARAM_PRIORITIZED_RULE));
}
writeResponse(result, response);
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RuleQueryFactory.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RuleQueryFactory.java
index 831558dcc11..90ff616544a 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RuleQueryFactory.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RuleQueryFactory.java
@@ -46,6 +46,7 @@ import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_INCLUDE_EXTERNAL;
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_INHERITANCE;
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_IS_TEMPLATE;
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_LANGUAGES;
+import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_OWASP_MOBILE_TOP_10_2024;
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_OWASP_TOP_10;
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_OWASP_TOP_10_2021;
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_PRIORITIZED_RULE;
@@ -108,6 +109,7 @@ public class RuleQueryFactory {
query.setCwe(request.paramAsStrings(PARAM_CWE));
query.setOwaspTop10(request.paramAsStrings(PARAM_OWASP_TOP_10));
query.setOwaspTop10For2021(request.paramAsStrings(PARAM_OWASP_TOP_10_2021));
+ query.setOwaspMobileTop10For2024(request.paramAsStrings(PARAM_OWASP_MOBILE_TOP_10_2024));
query.setSansTop25(request.paramAsStrings(PARAM_SANS_TOP_25));
query.setSonarsourceSecurity(request.paramAsStrings(PARAM_SONARSOURCE_SECURITY));
query.setCleanCodeAttributesCategories(request.paramAsStrings(PARAM_CLEAN_CODE_ATTRIBUTE_CATEGORIES));
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RuleWsSupport.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RuleWsSupport.java
index 2f1504032b4..4736710cdfd 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RuleWsSupport.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RuleWsSupport.java
@@ -63,6 +63,7 @@ import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_INCLUDE_EXTERNAL;
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_INHERITANCE;
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_IS_TEMPLATE;
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_LANGUAGES;
+import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_OWASP_MOBILE_TOP_10_2024;
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_OWASP_TOP_10;
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_OWASP_TOP_10_2021;
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_PRIORITIZED_RULE;
@@ -137,6 +138,11 @@ public class RuleWsSupport {
.setSince("9.4")
.setPossibleValues("a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "a10");
+ action.createParam(PARAM_OWASP_MOBILE_TOP_10_2024)
+ .setDescription("Comma-separated list of OWASP Mobile Top 10 2024 lowercase categories.")
+ .setSince("2025.4")
+ .setPossibleValues("m1", "m2", "m3", "m4", "m5", "m6", "m7", "m8", "m9", "m10");
+
action.createParam(PARAM_SANS_TOP_25)
.setDeprecatedSince("10.0")
.setDescription("Comma-separated list of SANS Top 25 categories.")
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RulesWsParameters.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RulesWsParameters.java
index 3ad32dd58f0..70595644f15 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RulesWsParameters.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RulesWsParameters.java
@@ -35,6 +35,7 @@ public class RulesWsParameters {
public static final String PARAM_CWE = "cwe";
public static final String PARAM_OWASP_TOP_10 = "owaspTop10";
public static final String PARAM_OWASP_TOP_10_2021 = "owaspTop10-2021";
+ public static final String PARAM_OWASP_MOBILE_TOP_10_2024 = "owaspMobileTop10-2024";
/**
* @deprecated SansTop25 report is outdated, it has been completely deprecated in version 10.0 and will be removed from version 11.0
*/
@@ -84,26 +85,26 @@ public class RulesWsParameters {
public static final String FIELD_CLEAN_CODE_ATTRIBUTE = "cleanCodeAttribute";
/**
- * Value for 'f' parameter which is used to return all the "defaultDebtRemFn" fields.
+ * Value for 'fields' parameter which is used to return all the "defaultDebtRemFn" fields.
*
* @deprecated since 10.0, replaced by {@link #FIELD_DEFAULT_REM_FUNCTION}
*/
@Deprecated(since = "10.0")
public static final String FIELD_DEFAULT_DEBT_REM_FUNCTION = "defaultDebtRemFn";
/**
- * Value for 'f' parameter which is used to return all the "defaultRemFn" fields.
+ * Value for 'fields' parameter which is used to return all the "defaultRemFn" fields.
*/
public static final String FIELD_DEFAULT_REM_FUNCTION = "defaultRemFn";
/**
- * Value for 'f' parameter which is used to return all the "debtRemFn" fields.
+ * Value for 'fields' parameter which is used to return all the "debtRemFn" fields.
*
* @deprecated since 10.0, replaced by {@link #FIELD_REM_FUNCTION}
*/
@Deprecated(since = "10.0")
public static final String FIELD_DEBT_REM_FUNCTION = "debtRemFn";
/**
- * Value for 'f' parameter which is used to return all the "remFn" fields.
+ * Value for 'fields' parameter which is used to return all the "remFn" fields.
*/
public static final String FIELD_REM_FUNCTION = "remFn";
public static final String FIELD_GAP_DESCRIPTION = "gapDescription";
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/SearchAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/SearchAction.java
index d0bfa57b855..808c822156a 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/SearchAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/SearchAction.java
@@ -33,16 +33,15 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
-import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.api.issue.impact.SoftwareQuality;
import org.sonar.api.rule.Severity;
import org.sonar.api.rules.CleanCodeAttributeCategory;
-import org.sonar.core.rule.RuleType;
import org.sonar.api.server.ws.Change;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
+import org.sonar.core.rule.RuleType;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.rule.RuleDto;
@@ -76,6 +75,7 @@ import static org.sonar.server.rule.index.RuleIndex.FACET_IMPACT_SEVERITY;
import static org.sonar.server.rule.index.RuleIndex.FACET_IMPACT_SOFTWARE_QUALITY;
import static org.sonar.server.rule.index.RuleIndex.FACET_LANGUAGES;
import static org.sonar.server.rule.index.RuleIndex.FACET_OLD_DEFAULT;
+import static org.sonar.server.rule.index.RuleIndex.FACET_OWASP_MOBILE_TOP_10_2024;
import static org.sonar.server.rule.index.RuleIndex.FACET_OWASP_TOP_10;
import static org.sonar.server.rule.index.RuleIndex.FACET_OWASP_TOP_10_2021;
import static org.sonar.server.rule.index.RuleIndex.FACET_REPOSITORIES;
@@ -93,6 +93,7 @@ import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_CWE;
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_IMPACT_SEVERITIES;
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_IMPACT_SOFTWARE_QUALITIES;
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_LANGUAGES;
+import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_OWASP_MOBILE_TOP_10_2024;
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_OWASP_TOP_10;
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_OWASP_TOP_10_2021;
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_PRIORITIZED_RULE;
@@ -121,6 +122,7 @@ public class SearchAction implements RulesWsAction {
FACET_CWE,
FACET_OWASP_TOP_10,
FACET_OWASP_TOP_10_2021,
+ FACET_OWASP_MOBILE_TOP_10_2024,
FACET_SANS_TOP_25,
FACET_SONARSOURCE_SECURITY,
FACET_CLEAN_CODE_ATTRIBUTE_CATEGORY,
@@ -154,16 +156,16 @@ public class SearchAction implements RulesWsAction {
new Change("5.5", "The field 'defaultDebtRemFnOffset' has been deprecated, use 'defaultRemFnBaseEffort' instead"),
new Change("5.5", "The field 'debtOverloaded' has been deprecated, use 'remFnOverloaded' instead"),
new Change("7.1", "The field 'scope' has been added to the response"),
- new Change("7.1", "The field 'scope' has been added to the 'f' parameter"),
+ new Change("7.1", "The field 'scope' has been added to the 'fields' parameter"),
new Change("7.2", "The field 'isExternal' has been added to the response"),
- new Change("7.2", "The field 'includeExternal' has been added to the 'f' parameter"),
- new Change("7.5", "The field 'updatedAt' has been added to the 'f' parameter"),
+ new Change("7.2", "The field 'includeExternal' has been added to the 'fields' parameter"),
+ new Change("7.5", "The field 'updatedAt' has been added to the 'fields' parameter"),
new Change("9.5", "The field 'htmlDesc' has been deprecated, use 'descriptionSections' instead"),
new Change("9.5", "The field 'descriptionSections' has been added to the payload"),
- new Change("9.5", "The field 'descriptionSections' has been added to the 'f' parameter"),
+ new Change("9.5", "The field 'descriptionSections' has been added to the 'fields' parameter"),
new Change("9.6", "'descriptionSections' can optionally embed a context field"),
- new Change("9.6", "The field 'educationPrinciples' has been added to the 'f' parameter"),
- new Change("9.8", "response fields 'total', 's', 'ps' have been deprecated, please use 'paging' object instead"),
+ new Change("9.6", "The field 'educationPrinciples' has been added to the 'fields' parameter"),
+ new Change("9.8", "response fields 'total', 's', 'pageSize' have been deprecated, please use 'paging' object instead"),
new Change("9.8", "The field 'paging' has been added to the response"),
new Change("10.0", "The deprecated field 'effortToFixDescription' has been removed, use 'gapDescription' instead."),
new Change("10.0", "The deprecated field 'debtRemFnCoeff' has been removed, use 'remFnGapMultiplier' instead."),
@@ -173,14 +175,14 @@ public class SearchAction implements RulesWsAction {
new Change("10.0", "The deprecated field 'debtOverloaded' has been removed, use 'remFnOverloaded' instead."),
new Change("10.0", "The field 'defaultDebtRemFnType' has been deprecated, use 'defaultRemFnType' instead"),
new Change("10.0", "The field 'debtRemFnType' has been deprecated, use 'remFnType' instead"),
- new Change("10.0", "The value 'debtRemFn' for the 'f' parameter has been deprecated, use 'remFn' instead"),
- new Change("10.0", "The value 'defaultDebtRemFn' for the 'f' parameter has been deprecated, use 'defaultRemFn' instead"),
+ new Change("10.0", "The value 'debtRemFn' for the 'fields' parameter has been deprecated, use 'remFn' instead"),
+ new Change("10.0", "The value 'defaultDebtRemFn' for the 'fields' parameter has been deprecated, use 'defaultRemFn' instead"),
new Change("10.0", "The value 'sansTop25' for the parameter 'facets' has been deprecated"),
new Change("10.0", "Parameter 'sansTop25' is deprecated"),
new Change("10.2", "Add 'impacts', 'cleanCodeAttribute', 'cleanCodeAttributeCategory' fields to the response"),
new Change("10.2", "The fields 'type' and 'severity' are deprecated in the response. Use 'impacts' instead."),
- new Change("10.2", "The field 'cleanCodeAttribute' has been added to the 'f' parameter."),
- new Change("10.2", "The value 'severity' for the 'f' parameter has been deprecated."),
+ new Change("10.2", "The field 'cleanCodeAttribute' has been added to the 'fields' parameter."),
+ new Change("10.2", "The value 'severity' for the 'fields' parameter has been deprecated."),
new Change("10.2",
format("The values '%s', '%s' and '%s' have been added to the 'facets' parameter.", FACET_CLEAN_CODE_ATTRIBUTE_CATEGORY, FACET_IMPACT_SOFTWARE_QUALITY,
FACET_IMPACT_SEVERITY)),
@@ -197,9 +199,9 @@ public class SearchAction implements RulesWsAction {
new Change("10.8", format("The parameters '%s','%s and '%s' are not deprecated anymore.", PARAM_SEVERITIES, PARAM_TYPES, PARAM_ACTIVE_SEVERITIES)),
new Change("10.8", "The values 'severity' and 'types' for the 'facets' parameter are not deprecated anymore."),
new Change("10.8", "The fields 'type' and 'severity' in the response are not deprecated anymore."),
- new Change("10.8", "The value 'severity' for the 'f' parameter is not deprecated anymore."),
+ new Change("10.8", "The value 'severity' for the 'fields' parameter is not deprecated anymore."),
new Change("2025.1", format("The facet '%s' has been added.", FACET_ACTIVE_IMPACT_SEVERITY)),
- new Change("2025.1", "The deprecated field 'htmlDesc' is not returned anymore, even if specified in the 'f' parameter."));
+ new Change("2025.1", "The deprecated field 'htmlDesc' is not returned anymore, even if specified in the 'fields' parameter."));
action.createParam(FACETS)
.setDescription("Comma-separated list of the facets to be computed. No facet is computed by default.")
@@ -273,15 +275,15 @@ public class SearchAction implements RulesWsAction {
}
private static SearchOptions loadCommonContext(SearchRequest request) {
- int pageSize = Integer.parseInt(request.getPs());
- SearchOptions context = new SearchOptions().addFields(request.getF());
- if (request.getFacets() != null) {
- context.addFacets(request.getFacets());
+ int pageSize = Integer.parseInt(request.pageSize());
+ SearchOptions context = new SearchOptions().addFields(request.fields());
+ if (request.facets() != null) {
+ context.addFacets(request.facets());
}
if (pageSize < 1) {
- context.setPage(Integer.parseInt(request.getP()), 0).setLimit(MAX_PAGE_SIZE);
+ context.setPage(Integer.parseInt(request.page()), 0).setLimit(MAX_PAGE_SIZE);
} else {
- context.setPage(Integer.parseInt(request.getP()), pageSize);
+ context.setPage(Integer.parseInt(request.page()), pageSize);
}
return context;
}
@@ -336,18 +338,19 @@ public class SearchAction implements RulesWsAction {
if (resultsFacets == null) {
return;
}
- addMandatoryFacetValues(results, FACET_LANGUAGES, request.getLanguages());
- addMandatoryFacetValues(results, FACET_REPOSITORIES, request.getRepositories());
+ addMandatoryFacetValues(results, FACET_LANGUAGES, request.languages);
+ addMandatoryFacetValues(results, FACET_REPOSITORIES, request.repositories);
addMandatoryFacetValues(results, FACET_STATUSES, ALL_STATUSES_EXCEPT_REMOVED);
addMandatoryFacetValues(results, FACET_SEVERITIES, Severity.ALL);
addMandatoryFacetValues(results, FACET_ACTIVE_SEVERITIES, Severity.ALL);
- addMandatoryFacetValues(results, FACET_TAGS, request.getTags());
+ addMandatoryFacetValues(results, FACET_TAGS, request.tags);
addMandatoryFacetValues(results, FACET_TYPES, RuleType.names());
- addMandatoryFacetValues(results, FACET_CWE, request.getCwe());
- addMandatoryFacetValues(results, FACET_OWASP_TOP_10, request.getOwaspTop10());
- addMandatoryFacetValues(results, FACET_OWASP_TOP_10_2021, request.getOwaspTop10For2021());
- addMandatoryFacetValues(results, FACET_SANS_TOP_25, request.getSansTop25());
- addMandatoryFacetValues(results, FACET_SONARSOURCE_SECURITY, request.getSonarsourceSecurity());
+ addMandatoryFacetValues(results, FACET_CWE, request.cwe);
+ addMandatoryFacetValues(results, FACET_OWASP_TOP_10, request.owaspTop10());
+ addMandatoryFacetValues(results, FACET_OWASP_TOP_10_2021, request.owaspTop10For2021());
+ addMandatoryFacetValues(results, FACET_OWASP_MOBILE_TOP_10_2024, request.owaspMobileTop10For2024());
+ addMandatoryFacetValues(results, FACET_SANS_TOP_25, request.sansTop25());
+ addMandatoryFacetValues(results, FACET_SONARSOURCE_SECURITY, request.sonarsourceSecurity());
addMandatoryFacetValues(results, PARAM_IMPACT_SOFTWARE_QUALITIES, enumToStringCollection(SoftwareQuality.values()));
addMandatoryFacetValues(results, PARAM_IMPACT_SEVERITIES, enumToStringCollection(org.sonar.api.issue.impact.Severity.values()));
addMandatoryFacetValues(results, PARAM_CLEAN_CODE_ATTRIBUTE_CATEGORIES, enumToStringCollection(CleanCodeAttributeCategory.values()));
@@ -355,22 +358,23 @@ public class SearchAction implements RulesWsAction {
Common.Facet.Builder facet = Common.Facet.newBuilder();
Common.FacetValue.Builder value = Common.FacetValue.newBuilder();
Map<String, List<String>> facetValuesByFacetKey = new HashMap<>();
- facetValuesByFacetKey.put(FACET_LANGUAGES, request.getLanguages());
- facetValuesByFacetKey.put(FACET_REPOSITORIES, request.getRepositories());
- facetValuesByFacetKey.put(FACET_STATUSES, request.getStatuses());
- facetValuesByFacetKey.put(FACET_SEVERITIES, request.getSeverities());
- facetValuesByFacetKey.put(FACET_ACTIVE_SEVERITIES, request.getActiveSeverities());
- facetValuesByFacetKey.put(FACET_TAGS, request.getTags());
- facetValuesByFacetKey.put(FACET_TYPES, request.getTypes());
- facetValuesByFacetKey.put(FACET_CWE, request.getCwe());
- facetValuesByFacetKey.put(FACET_OWASP_TOP_10, request.getOwaspTop10());
- facetValuesByFacetKey.put(FACET_OWASP_TOP_10_2021, request.getOwaspTop10For2021());
- facetValuesByFacetKey.put(FACET_SANS_TOP_25, request.getSansTop25());
- facetValuesByFacetKey.put(FACET_SONARSOURCE_SECURITY, request.getSonarsourceSecurity());
- facetValuesByFacetKey.put(FACET_CLEAN_CODE_ATTRIBUTE_CATEGORY, request.getCleanCodeAttributesCategories());
- facetValuesByFacetKey.put(FACET_IMPACT_SOFTWARE_QUALITY, request.getImpactSoftwareQualities());
- facetValuesByFacetKey.put(FACET_IMPACT_SEVERITY, request.getImpactSeverities());
- facetValuesByFacetKey.put(FACET_ACTIVE_IMPACT_SEVERITY, request.getActiveImpactSeverities());
+ facetValuesByFacetKey.put(FACET_LANGUAGES, request.languages());
+ facetValuesByFacetKey.put(FACET_REPOSITORIES, request.repositories());
+ facetValuesByFacetKey.put(FACET_STATUSES, request.statuses());
+ facetValuesByFacetKey.put(FACET_SEVERITIES, request.severities());
+ facetValuesByFacetKey.put(FACET_ACTIVE_SEVERITIES, request.activeSeverities());
+ facetValuesByFacetKey.put(FACET_TAGS, request.tags());
+ facetValuesByFacetKey.put(FACET_TYPES, request.types());
+ facetValuesByFacetKey.put(FACET_CWE, request.cwe());
+ facetValuesByFacetKey.put(FACET_OWASP_TOP_10, request.owaspTop10());
+ facetValuesByFacetKey.put(FACET_OWASP_TOP_10_2021, request.owaspTop10For2021());
+ facetValuesByFacetKey.put(FACET_OWASP_MOBILE_TOP_10_2024, request.owaspMobileTop10For2024());
+ facetValuesByFacetKey.put(FACET_SANS_TOP_25, request.sansTop25());
+ facetValuesByFacetKey.put(FACET_SONARSOURCE_SECURITY, request.sonarsourceSecurity());
+ facetValuesByFacetKey.put(FACET_CLEAN_CODE_ATTRIBUTE_CATEGORY, request.cleanCodeAttributesCategories());
+ facetValuesByFacetKey.put(FACET_IMPACT_SOFTWARE_QUALITY, request.impactSoftwareQualities());
+ facetValuesByFacetKey.put(FACET_IMPACT_SEVERITY, request.impactSeverities());
+ facetValuesByFacetKey.put(FACET_ACTIVE_IMPACT_SEVERITY, request.activeImpactSeverities());
for (String facetName : context.getFacets()) {
facet.clear().setProperty(facetName);
@@ -425,248 +429,55 @@ public class SearchAction implements RulesWsAction {
private static SearchRequest toSearchWsRequest(Request request) {
request.mandatoryParamAsBoolean(ASCENDING);
- return new SearchRequest()
- .setImpactSeverities(request.paramAsStrings(PARAM_IMPACT_SEVERITIES))
- .setImpactSoftwareQualities(request.paramAsStrings(PARAM_IMPACT_SOFTWARE_QUALITIES))
- .setCleanCodeAttributesCategories(request.paramAsStrings(PARAM_CLEAN_CODE_ATTRIBUTE_CATEGORIES))
- .setActiveSeverities(request.paramAsStrings(PARAM_ACTIVE_SEVERITIES))
- .setF(request.paramAsStrings(FIELDS))
- .setFacets(request.paramAsStrings(FACETS))
- .setLanguages(request.paramAsStrings(PARAM_LANGUAGES))
- .setP("" + request.mandatoryParamAsInt(PAGE))
- .setPs("" + request.mandatoryParamAsInt(PAGE_SIZE))
- .setRepositories(request.paramAsStrings(PARAM_REPOSITORIES))
- .setSeverities(request.paramAsStrings(PARAM_SEVERITIES))
- .setStatuses(request.paramAsStrings(PARAM_STATUSES))
- .setTags(request.paramAsStrings(PARAM_TAGS))
- .setTypes(request.paramAsStrings(PARAM_TYPES))
- .setCwe(request.paramAsStrings(PARAM_CWE))
- .setOwaspTop10(request.paramAsStrings(PARAM_OWASP_TOP_10))
- .setOwaspTop10For2021(request.paramAsStrings(PARAM_OWASP_TOP_10_2021))
- .setSansTop25(request.paramAsStrings(PARAM_SANS_TOP_25))
- .setSonarsourceSecurity(request.paramAsStrings(PARAM_SONARSOURCE_SECURITY))
- .setPrioritizedRule(request.paramAsBoolean(PARAM_PRIORITIZED_RULE))
- .setActiveImpactSeverities(request.paramAsStrings(PARAM_ACTIVE_IMPACT_SEVERITIES));
+ return new SearchRequest(
+ String.valueOf(request.mandatoryParamAsInt(PAGE)),
+ String.valueOf(request.mandatoryParamAsInt(PAGE_SIZE)),
+ request.paramAsStrings(FIELDS),
+ request.paramAsStrings(FACETS),
+ request.paramAsStrings(PARAM_LANGUAGES),
+ request.paramAsStrings(PARAM_REPOSITORIES),
+ request.paramAsStrings(PARAM_SEVERITIES),
+ request.paramAsStrings(PARAM_STATUSES),
+ request.paramAsStrings(PARAM_TAGS),
+ request.paramAsStrings(PARAM_TYPES),
+ request.paramAsStrings(PARAM_CWE),
+ request.paramAsStrings(PARAM_OWASP_TOP_10),
+ request.paramAsStrings(PARAM_OWASP_TOP_10_2021),
+ request.paramAsStrings(PARAM_OWASP_MOBILE_TOP_10_2024),
+ request.paramAsStrings(PARAM_SANS_TOP_25),
+ request.paramAsStrings(PARAM_SONARSOURCE_SECURITY),
+ request.paramAsStrings(PARAM_IMPACT_SEVERITIES),
+ request.paramAsStrings(PARAM_IMPACT_SOFTWARE_QUALITIES),
+ request.paramAsStrings(PARAM_CLEAN_CODE_ATTRIBUTE_CATEGORIES),
+ request.paramAsStrings(PARAM_ACTIVE_SEVERITIES),
+ request.paramAsStrings(PARAM_ACTIVE_IMPACT_SEVERITIES),
+ request.paramAsBoolean(PARAM_PRIORITIZED_RULE)
+ );
}
- private static class SearchRequest {
-
- private List<String> activeSeverities;
- private List<String> f;
- private List<String> facets;
- private List<String> languages;
- private String p;
- private String ps;
- private List<String> repositories;
- private List<String> severities;
- private List<String> statuses;
- private List<String> tags;
- private List<String> types;
- private List<String> cwe;
- private List<String> owaspTop10;
- private List<String> owaspTop10For2021;
- private List<String> sansTop25;
- private List<String> sonarsourceSecurity;
- private List<String> impactSeverities;
- private List<String> impactSoftwareQualities;
- private List<String> cleanCodeAttributesCategories;
- private List<String> activeImpactSeverities;
- private Boolean prioritizedRule;
-
- private SearchRequest setActiveSeverities(List<String> activeSeverities) {
- this.activeSeverities = activeSeverities;
- return this;
- }
-
- private List<String> getActiveSeverities() {
- return activeSeverities;
- }
-
- private SearchRequest setF(List<String> f) {
- this.f = f;
- return this;
- }
-
- private List<String> getF() {
- return f;
- }
-
- private SearchRequest setFacets(List<String> facets) {
- this.facets = facets;
- return this;
- }
-
- private List<String> getFacets() {
- return facets;
- }
-
- private SearchRequest setLanguages(List<String> languages) {
- this.languages = languages;
- return this;
- }
-
- private List<String> getLanguages() {
- return languages;
- }
-
- private SearchRequest setP(String p) {
- this.p = p;
- return this;
- }
-
- private String getP() {
- return p;
- }
-
- private SearchRequest setPs(String ps) {
- this.ps = ps;
- return this;
- }
-
- private String getPs() {
- return ps;
- }
-
- private SearchRequest setRepositories(List<String> repositories) {
- this.repositories = repositories;
- return this;
- }
-
- private List<String> getRepositories() {
- return repositories;
- }
-
- private SearchRequest setSeverities(List<String> severities) {
- this.severities = severities;
- return this;
- }
-
- private List<String> getSeverities() {
- return severities;
- }
-
- private SearchRequest setStatuses(List<String> statuses) {
- this.statuses = statuses;
- return this;
- }
-
- private List<String> getStatuses() {
- return statuses;
- }
-
- private SearchRequest setTags(List<String> tags) {
- this.tags = tags;
- return this;
- }
-
- private List<String> getTags() {
- return tags;
- }
-
- private SearchRequest setTypes(@Nullable List<String> types) {
- this.types = types;
- return this;
- }
-
- private List<String> getTypes() {
- return types;
- }
-
- public List<String> getCwe() {
- return cwe;
- }
-
- public SearchRequest setCwe(@Nullable List<String> cwe) {
- this.cwe = cwe;
- return this;
- }
-
- public List<String> getOwaspTop10() {
- return owaspTop10;
- }
-
- public SearchRequest setOwaspTop10(@Nullable List<String> owaspTop10) {
- this.owaspTop10 = owaspTop10;
- return this;
- }
-
- public List<String> getOwaspTop10For2021() {
- return owaspTop10For2021;
- }
-
- public SearchRequest setOwaspTop10For2021(@Nullable List<String> owaspTop10For2021) {
- this.owaspTop10For2021 = owaspTop10For2021;
- return this;
- }
-
- /**
- * @deprecated SansTop25 report is outdated, it has been completely deprecated in version 10.0 and will be removed from version 11.0
- */
- @Deprecated(since = "10.0", forRemoval = true)
- public List<String> getSansTop25() {
- return sansTop25;
- }
-
- @Deprecated(since = "10.0", forRemoval = true)
- public SearchRequest setSansTop25(@Nullable List<String> sansTop25) {
- this.sansTop25 = sansTop25;
- return this;
- }
-
- public List<String> getSonarsourceSecurity() {
- return sonarsourceSecurity;
- }
-
- public SearchRequest setSonarsourceSecurity(@Nullable List<String> sonarsourceSecurity) {
- this.sonarsourceSecurity = sonarsourceSecurity;
- return this;
- }
-
- public List<String> getImpactSeverities() {
- return impactSeverities;
- }
-
- public SearchRequest setImpactSeverities(@Nullable List<String> impactSeverities) {
- this.impactSeverities = impactSeverities;
- return this;
- }
-
- public List<String> getImpactSoftwareQualities() {
- return impactSoftwareQualities;
- }
-
- public SearchRequest setImpactSoftwareQualities(@Nullable List<String> impactSoftwareQualities) {
- this.impactSoftwareQualities = impactSoftwareQualities;
- return this;
- }
-
- public List<String> getCleanCodeAttributesCategories() {
- return cleanCodeAttributesCategories;
- }
-
- public SearchRequest setCleanCodeAttributesCategories(@Nullable List<String> cleanCodeAttributesCategories) {
- this.cleanCodeAttributesCategories = cleanCodeAttributesCategories;
- return this;
- }
-
- @CheckForNull
- public Boolean getPrioritizedRule() {
- return prioritizedRule;
- }
-
- public SearchRequest setPrioritizedRule(@Nullable Boolean prioritizedRule) {
- this.prioritizedRule = prioritizedRule;
- return this;
- }
-
- public SearchRequest setActiveImpactSeverities(@Nullable List<String> activeImpactSeverities) {
- this.activeImpactSeverities = activeImpactSeverities;
- return this;
- }
-
- @CheckForNull
- public List<String> getActiveImpactSeverities() {
- return activeImpactSeverities;
- }
+ private record SearchRequest(
+ String page,
+ String pageSize,
+ @Nullable List<String> fields,
+ @Nullable List<String> facets,
+ @Nullable List<String> languages,
+ @Nullable List<String> repositories,
+ @Nullable List<String> severities,
+ @Nullable List<String> statuses,
+ @Nullable List<String> tags,
+ @Nullable List<String> types,
+ @Nullable List<String> cwe,
+ @Nullable List<String> owaspTop10,
+ @Nullable List<String> owaspTop10For2021,
+ @Nullable List<String> owaspMobileTop10For2024,
+ @Nullable List<String> sansTop25,
+ @Nullable List<String> sonarsourceSecurity,
+ @Nullable List<String> impactSeverities,
+ @Nullable List<String> impactSoftwareQualities,
+ @Nullable List<String> cleanCodeAttributesCategories,
+ @Nullable List<String> activeSeverities,
+ @Nullable List<String> activeImpactSeverities,
+ @Nullable Boolean prioritizedRule
+ ) {
}
}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/ShowAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/ShowAction.java
index 2df21e2e12f..e1aea127817 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/ShowAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/ShowAction.java
@@ -89,7 +89,7 @@ public class ShowAction implements RulesWsAction {
new Change("10.2", "The field 'severity' and 'type' in the response have been deprecated, use 'impacts' instead."),
new Change("10.8", format("Possible values '%s' and '%s' for response field 'severity' of 'impacts' have been added.", INFO.name(), BLOCKER.name())),
new Change("10.8", "The field 'severity' and 'type' in the response are not deprecated anymore."),
- new Change("2025.1", "The deprecated field 'htmlDesc' is not returned anymore, even if specified in the 'f' parameter."));
+ new Change("2025.1", "The deprecated field 'htmlDesc' is not returned anymore, even if specified in the 'fields' parameter."));
action
.createParam(PARAM_KEY)
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DismissNoticeAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DismissNoticeAction.java
index 407ffcf2f36..5c5b2c8b36c 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DismissNoticeAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DismissNoticeAction.java
@@ -41,6 +41,7 @@ import static org.sonar.server.user.ws.DismissNoticeAction.DismissNotices.QUALIT
import static org.sonar.server.user.ws.DismissNoticeAction.DismissNotices.SHOW_DNA_BANNER;
import static org.sonar.server.user.ws.DismissNoticeAction.DismissNotices.SHOW_DNA_OPTIN_BANNER;
import static org.sonar.server.user.ws.DismissNoticeAction.DismissNotices.SHOW_DNA_TOUR;
+import static org.sonar.server.user.ws.DismissNoticeAction.DismissNotices.SHOW_ENABLE_SCA;
import static org.sonar.server.user.ws.DismissNoticeAction.DismissNotices.SHOW_NEW_MODES_BANNER;
import static org.sonar.server.user.ws.DismissNoticeAction.DismissNotices.SHOW_NEW_MODES_TOUR;
@@ -59,6 +60,7 @@ public class DismissNoticeAction implements UsersWsAction {
SHOW_DNA_OPTIN_BANNER("showDesignAndArchitectureOptInBanner"),
SHOW_DNA_BANNER("showDesignAndArchitectureBanner"),
SHOW_DNA_TOUR("showDesignAndArchitectureTour"),
+ SHOW_ENABLE_SCA("showEnableSca"),
;
private final String key;
@@ -99,12 +101,12 @@ public class DismissNoticeAction implements UsersWsAction {
return SUPPORT_FOR_NEW_NOTICE_MESSAGE.formatted(noticesList);
}
-
@Override
public void define(WebService.NewController context) {
WebService.NewAction action = context.createAction("dismiss_notice")
.setDescription("Dismiss a notice for the current user. Silently ignore if the notice is already dismissed.")
- .setChangelog(new Change("25.4", printNewNotice(SHOW_DNA_OPTIN_BANNER, SHOW_DNA_BANNER, SHOW_DNA_TOUR)))
+ .setChangelog(new Change("2025.3", printNewNotice(SHOW_ENABLE_SCA)))
+ .setChangelog(new Change("2025.3", printNewNotice(SHOW_DNA_OPTIN_BANNER, SHOW_DNA_BANNER, SHOW_DNA_TOUR)))
.setChangelog(new Change("10.8", printNewNotice(SHOW_NEW_MODES_TOUR)))
.setChangelog(new Change("10.8", printNewNotice(SHOW_NEW_MODES_BANNER)))
.setChangelog(new Change("10.6", printNewNotice(ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE)))
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/SearchAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/SearchAction.java
index 5b0d652caf6..811b8c10f54 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/SearchAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/SearchAction.java
@@ -73,7 +73,7 @@ public class SearchAction implements UserGroupsWsAction {
public void define(NewController context) {
WebService.NewAction action = context.createAction("search")
.setDescription("Search for user groups.<br>" +
- "Requires the following permission: 'Administer System'.")
+ "Requires the following permission: 'Administer System'.")
.setHandler(this)
.setResponseExample(getClass().getResource("search-example.json"))
.setSince("5.2")
@@ -107,7 +107,8 @@ public class SearchAction implements UserGroupsWsAction {
try (DbSession dbSession = dbClient.openSession(false)) {
userSession.checkLoggedIn().checkIsSystemAdministrator();
- GroupSearchRequest groupSearchRequest = new GroupSearchRequest(request.param(Param.TEXT_QUERY), request.paramAsBoolean(MANAGED_PARAM), page, pageSize);
+ GroupSearchRequest groupSearchRequest = new GroupSearchRequest(request.param(Param.TEXT_QUERY), request.paramAsBoolean(MANAGED_PARAM), null, null,
+ page, pageSize);
SearchResults<GroupInformation> searchResults = groupService.search(dbSession, groupSearchRequest);
Set<String> groupUuids = extractGroupUuids(searchResults.searchResults());
@@ -136,7 +137,7 @@ public class SearchAction implements UserGroupsWsAction {
}
private static SearchWsResponse buildResponse(List<GroupInformation> groups, Map<String, Integer> userCountByGroup,
- Set<String> fields, Paging paging) {
+ Set<String> fields, Paging paging) {
SearchWsResponse.Builder responseBuilder = SearchWsResponse.newBuilder();
groups.forEach(group -> responseBuilder
.addGroups(toWsGroup(group.groupDto(), userCountByGroup.get(group.groupDto().getName()), group.isManaged(), fields, group.isDefault())));
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/WebhookSupport.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/WebhookSupport.java
index 6160140239f..ebb49972b78 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/WebhookSupport.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/WebhookSupport.java
@@ -24,9 +24,10 @@ import java.net.SocketException;
import java.net.UnknownHostException;
import okhttp3.HttpUrl;
import org.sonar.api.config.Configuration;
-import org.sonar.db.permission.ProjectPermission;
import org.sonar.db.permission.GlobalPermission;
+import org.sonar.db.permission.ProjectPermission;
import org.sonar.db.project.ProjectDto;
+import org.sonar.server.network.NetworkInterfaceProvider;
import org.sonar.server.user.UserSession;
import static org.sonar.api.CoreProperties.SONAR_VALIDATE_WEBHOOKS_DEFAULT_VALUE;
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/WebhooksWsModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/WebhooksWsModule.java
index 2427e07f006..a5ef828720d 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/WebhooksWsModule.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/WebhooksWsModule.java
@@ -32,7 +32,6 @@ public class WebhooksWsModule extends Module {
UpdateAction.class,
DeleteAction.class,
WebhookDeliveryAction.class,
- WebhookDeliveriesAction.class,
- NetworkInterfaceProvider.class);
+ WebhookDeliveriesAction.class);
}
}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/batch/BatchIndexTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/batch/BatchIndexTest.java
index 902db3d5a81..39091a6dd9b 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/batch/BatchIndexTest.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/batch/BatchIndexTest.java
@@ -21,12 +21,14 @@ package org.sonar.server.batch;
import java.io.File;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.CharUtils;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.platform.ServerFileSystem;
@@ -36,28 +38,28 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
-public class BatchIndexTest {
+class BatchIndexTest {
- @Rule
- public TemporaryFolder temp = new TemporaryFolder();
+ @TempDir
+ Path temp;
private File jar;
- private ServerFileSystem fs = mock(ServerFileSystem.class);
+ private final ServerFileSystem fs = mock(ServerFileSystem.class);
- @Before
- public void prepare_fs() throws IOException {
- File homeDir = temp.newFolder();
- when(fs.getHomeDir()).thenReturn(homeDir);
+ @BeforeEach
+ void prepare_fs() throws IOException {
+ Path homeDir = Files.createTempDirectory(temp, "homeDir");
+ when(fs.getHomeDir()).thenReturn(homeDir.toFile());
- File batchDir = new File(homeDir, "lib/scanner");
- FileUtils.forceMkdir(batchDir);
- jar = new File(batchDir, "sonar-batch.jar");
- FileUtils.writeStringToFile(new File(batchDir, "sonar-batch.jar"), "foo");
+ Path batchDir = homeDir.resolve("lib/scanner");
+ Files.createDirectories(batchDir);
+ jar = batchDir.resolve("sonar-batch.jar").toFile();
+ FileUtils.writeByteArrayToFile(batchDir.resolve("sonar-batch.jar").toFile(), "foo".getBytes(StandardCharsets.UTF_8));
}
@Test
- public void get_index() {
+ void get_index() {
BatchIndex batchIndex = new BatchIndex(fs);
batchIndex.start();
@@ -68,7 +70,7 @@ public class BatchIndexTest {
}
@Test
- public void get_file() {
+ void get_file() {
BatchIndex batchIndex = new BatchIndex(fs);
batchIndex.start();
@@ -81,37 +83,35 @@ public class BatchIndexTest {
* /etc/passwd
*/
@Test
- public void check_location_of_file() {
- assertThatThrownBy(() -> {
- BatchIndex batchIndex = new BatchIndex(fs);
- batchIndex.start();
-
- batchIndex.getFile("../sonar-batch.jar");
- })
+ void check_location_of_file() {
+ BatchIndex batchIndex = new BatchIndex(fs);
+ batchIndex.start();
+ assertThatThrownBy(() -> batchIndex.getFile("../sonar-batch.jar"))
.isInstanceOf(NotFoundException.class)
.hasMessage("Bad filename: ../sonar-batch.jar");
}
@Test
- public void file_does_not_exist() {
- assertThatThrownBy(() -> {
- BatchIndex batchIndex = new BatchIndex(fs);
- batchIndex.start();
-
- batchIndex.getFile("other.jar");
- })
+ void file_does_not_exist() {
+ BatchIndex batchIndex = new BatchIndex(fs);
+ batchIndex.start();
+ assertThatThrownBy(() -> batchIndex.getFile("other.jar"))
.isInstanceOf(NotFoundException.class)
.hasMessage("Bad filename: other.jar");
}
@Test
- public void start_whenBatchDirDoesntExist_shouldThrow() throws IOException {
- File homeDir = temp.newFolder();
- when(fs.getHomeDir()).thenReturn(homeDir);
+ void start_whenBatchDirDoesntExist_shouldThrow() throws IOException {
+ Path homeDir = Files.createTempDirectory(temp, "homeDir");
+ when(fs.getHomeDir()).thenReturn(homeDir.toFile());
BatchIndex batchIndex = new BatchIndex(fs);
+
+ // Ensure that the file separator is correct based on the OS
+ String expectedMessage = format("%s%slib%sscanner folder not found",
+ homeDir.toFile().getAbsolutePath(), File.separator, File.separator);
assertThatThrownBy(batchIndex::start)
.isInstanceOf(IllegalStateException.class)
- .hasMessage(format("%s/lib/scanner folder not found", homeDir.getAbsolutePath()));
+ .hasMessage(expectedMessage);
}
}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionParserTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionParserTest.java
index 1872f4f3589..14bfe6dae92 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionParserTest.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionParserTest.java
@@ -20,26 +20,28 @@
package org.sonar.server.issue.ws.anticipatedtransition;
import java.io.IOException;
+import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.assertj.core.api.Assertions;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import org.sonar.api.rule.RuleKey;
import org.sonar.core.issue.AnticipatedTransition;
import static org.assertj.core.api.Assertions.assertThat;
-public class AnticipatedTransitionParserTest {
+class AnticipatedTransitionParserTest {
private static final String USER_UUID = "userUuid";
private static final String PROJECT_KEY = "projectKey";
+ public static final String REQUEST_WITH_TRANSITIONS_JSON = "request-with-transitions.json";
AnticipatedTransitionParser underTest = new AnticipatedTransitionParser();
@Test
- public void givenRequestBodyWithMultipleTransition_whenParse_thenAllTransitionsAreReturned() throws IOException {
+ void givenRequestBodyWithMultipleTransition_whenParse_thenAllTransitionsAreReturned() throws IOException, URISyntaxException {
// given
- String requestBody = readTestResourceFile("request-with-transitions.json");
+ String requestBody = readTestResourceFile();
// when
List<AnticipatedTransition> anticipatedTransitions = underTest.parse(requestBody, USER_UUID, PROJECT_KEY);
@@ -52,7 +54,7 @@ public class AnticipatedTransitionParserTest {
}
@Test
- public void givenRequestBodyWithNoTransitions_whenParse_ThenAnEmptyListIsReturned() {
+ void givenRequestBodyWithNoTransitions_whenParse_ThenAnEmptyListIsReturned() {
// given
String requestBody = "[]";
@@ -64,7 +66,7 @@ public class AnticipatedTransitionParserTest {
}
@Test
- public void givenRequestBodyWithInvalidJson_whenParse_thenExceptionIsThrown() {
+ void givenRequestBodyWithInvalidJson_whenParse_thenExceptionIsThrown() {
// given
String requestBody = "invalidJson";
@@ -75,7 +77,7 @@ public class AnticipatedTransitionParserTest {
}
@Test
- public void givenRequestBodyWithInvalidTransition_whenParse_thenExceptionIsThrown() throws IOException {
+ void givenRequestBodyWithInvalidTransition_whenParse_thenExceptionIsThrown() {
// given
String requestBodyWithInvalidTransition = """
[
@@ -124,8 +126,8 @@ public class AnticipatedTransitionParserTest {
"comment2"));
}
- private String readTestResourceFile(String fileName) throws IOException {
- return Files.readString(Path.of(getClass().getResource(fileName).getPath()));
+ private String readTestResourceFile() throws IOException, URISyntaxException {
+ return Files.readString(Path.of(getClass().getResource(AnticipatedTransitionParserTest.REQUEST_WITH_TRANSITIONS_JSON).toURI()));
}
}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/ActiveVersionEvaluatorTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/ActiveVersionEvaluatorTest.java
index 9e59be5b82b..eace45eb2a0 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/ActiveVersionEvaluatorTest.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/ActiveVersionEvaluatorTest.java
@@ -19,12 +19,14 @@
*/
package org.sonar.server.platform.ws;
+import java.time.LocalDate;
import java.util.Calendar;
-import java.util.Collections;
import java.util.SortedSet;
import java.util.TreeSet;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.sonar.api.internal.MetadataLoader;
import org.sonar.api.utils.DateUtils;
import org.sonar.api.utils.System2;
import org.sonar.core.platform.SonarQubeVersion;
@@ -107,69 +109,29 @@ class ActiveVersionEvaluatorTest {
}
@Test
- void evaluateIfActiveVersion_whenNoReleasesFound_shouldThrowIllegalStateException() {
+ void evaluateIfActiveVersion_whenEOLDateIsAfterToday_shouldReturnActiveVersion() {
+ LocalDate today = LocalDate.now();
+ LocalDate tomorrow = today.plusDays(1);
- when(sonarQubeVersion.get()).thenReturn(parse("10.8.0"));
+ when(sonarQubeVersion.get()).thenReturn(parse("2025.4.0.12345"));
- when(sonar.getAllReleases(any())).thenReturn(Collections.emptySortedSet());
-
- assertThatThrownBy(() -> underTest.evaluateIfActiveVersion(updateCenter))
- .isInstanceOf(IllegalStateException.class)
- .hasMessageContaining("Unable to find previous release in releases");
+ try (MockedStatic<MetadataLoader> mocked = org.mockito.Mockito.mockStatic(MetadataLoader.class)) {
+ mocked.when(() -> MetadataLoader.loadSqVersionEol(system2)).thenReturn(tomorrow.toString());
+ assertThat(underTest.evaluateIfActiveVersion(updateCenter)).isTrue();
+ }
}
@Test
- void evaluateIfActiveVersion_whenInstalledVersionIsLatestMinusOne_shouldReturnVersionIsActive() {
- when(sonarQubeVersion.get()).thenReturn(parse("10.9"));
- when(updateCenter.getSonar().getAllReleases(any())).thenReturn(getReleases());
+ void evaluateIfActiveVersion_whenEOLDateIsBeforeToday_shouldReturnInactiveVersion() {
+ LocalDate today = LocalDate.now();
+ LocalDate yesterday = today.minusDays(1);
- assertThat(underTest.evaluateIfActiveVersion(updateCenter)).isTrue();
- }
-
- @Test
- void evaluateIfActiveVersion_whenInstalledVersionIsSnapshot_shouldReturnVersionIsActive() {
- when(sonarQubeVersion.get()).thenReturn(parse("10.11-SNAPSHOT"));
- when(updateCenter.getSonar().getAllReleases(any())).thenReturn(getReleases());
+ when(sonarQubeVersion.get()).thenReturn(parse("2025.4.0.12345"));
- assertThat(underTest.evaluateIfActiveVersion(updateCenter)).isTrue();
- }
-
- @Test
- void evaluateIfActiveVersion_whenInstalledVersionIsTheOnlyAvailableVersion_shouldReturnVersionIsActive() {
- TreeSet<Release> releases = new TreeSet<>();
- releases.add(new Release(sonar, Version.create("10.8.0.12345")));
-
- when(sonarQubeVersion.get()).thenReturn(parse("10.8.0.12345"));
- when(updateCenter.getSonar().getAllReleases(any())).thenReturn(releases);
-
- assertThat(underTest.evaluateIfActiveVersion(updateCenter)).isTrue();
- }
-
- @Test
- void evaluateIfActiveVersion_whenAvailableVersionsAreAllPatchesOfInstalledVersion_shouldReturnVersionIsActive() {
- TreeSet<Release> releases = new TreeSet<>();
- releases.add(new Release(sonar, Version.create("10.8.0.12345")));
- releases.add(new Release(sonar, Version.create("10.8.1.12346")));
- when(sonar.getAllReleases(any())).thenReturn(releases);
-
- when(sonarQubeVersion.get()).thenReturn(parse("10.8.0.12345"));
- when(updateCenter.getSonar().getAllReleases(any())).thenReturn(releases);
-
- assertThat(underTest.evaluateIfActiveVersion(updateCenter)).isTrue();
- }
-
- @Test
- void evaluateIfActiveVersion_whenAvailableVersionsHaveDifferentNamingScheme_shouldReturnVersionIsActive() {
- TreeSet<Release> releases = new TreeSet<>();
- releases.add(new Release(sonar, Version.create("10.8.0.12345")));
- releases.add(new Release(sonar, Version.create("10.8.1.12346")));
- releases.add(new Release(sonar, Version.create("2025.1.0.12347")));
- when(sonar.getAllReleases(any())).thenReturn(releases);
-
- when(sonarQubeVersion.get()).thenReturn(parse("10.8.0.12345"));
- when(updateCenter.getSonar().getAllReleases(any())).thenReturn(releases);
-
- assertThat(underTest.evaluateIfActiveVersion(updateCenter)).isTrue();
+ try (MockedStatic<MetadataLoader> mocked = org.mockito.Mockito.mockStatic(MetadataLoader.class)) {
+ mocked.when(() -> MetadataLoader.loadSqVersionEol(system2)).thenReturn(yesterday.toString());
+ assertThat(underTest.evaluateIfActiveVersion(updateCenter)).isFalse();
+ }
}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImplTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImplTest.java
new file mode 100644
index 00000000000..2f76e5f9435
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImplTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.server.qualityprofile.builtin;
+
+import org.junit.jupiter.api.Test;
+import org.sonar.api.resources.Language;
+import org.sonar.api.resources.Languages;
+import org.sonar.db.DbClient;
+import org.sonar.server.rule.ServerRuleFinder;
+
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class BuiltInQProfileRepositoryImplTest {
+ @Test
+ void initializationWithoutQualityProfiles() {
+ DbClient dbClient = mock(DbClient.class);
+ ServerRuleFinder ruleFinder = mock(ServerRuleFinder.class);
+ Languages languages = mock(Languages.class);
+ Language java = mock(Language.class);
+ Language kotlin = mock(Language.class);
+
+ when(languages.all()).thenReturn(new Language[]{ java, kotlin });
+ when(java.getKey()).thenReturn("java");
+ when(kotlin.getKey()).thenReturn("kotlin");
+
+ BuiltInQProfileRepositoryImpl repository = new BuiltInQProfileRepositoryImpl(dbClient, ruleFinder, languages);
+
+ assertThatCode(repository::initialize).hasMessage("The following languages have no built-in quality profiles: java, kotlin");
+ }
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/WebhookSupportTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/WebhookSupportTest.java
index 57828a3a07a..31d98e3c0a1 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/WebhookSupportTest.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/WebhookSupportTest.java
@@ -31,6 +31,7 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.sonar.api.config.Configuration;
+import org.sonar.server.network.NetworkInterfaceProvider;
import org.sonar.server.user.UserSession;
import static java.util.Optional.of;
diff --git a/server/sonar-webserver/build.gradle b/server/sonar-webserver/build.gradle
index 1850493c986..b0c69ed86da 100644
--- a/server/sonar-webserver/build.gradle
+++ b/server/sonar-webserver/build.gradle
@@ -31,13 +31,12 @@ dependencies {
testImplementation 'org.apache.logging.log4j:log4j-api'
testImplementation 'org.apache.logging.log4j:log4j-core'
+ testImplementation 'com.squareup.okhttp3:mockwebserver'
testImplementation 'com.github.spotbugs:spotbugs-annotations'
testImplementation 'com.tngtech.java:junit-dataprovider'
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testImplementation 'org.junit.jupiter:junit-jupiter-params'
testImplementation 'org.mockito:mockito-core'
- testImplementation 'org.eclipse.jetty:jetty-server'
- testImplementation 'org.eclipse.jetty:jetty-servlet'
testImplementation 'org.sonarsource.api.plugin:sonar-plugin-api-test-fixtures'
testImplementation testFixtures(project(':server:sonar-server-common'))
testImplementation testFixtures(project(':server:sonar-webserver-auth'))
diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
index cb5cb05bc90..461f6763db2 100644
--- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
+++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
@@ -58,6 +58,7 @@ import org.sonar.core.language.LanguagesProvider;
import org.sonar.core.metric.SoftwareQualitiesMetrics;
import org.sonar.core.platform.PlatformEditionProvider;
import org.sonar.core.platform.SpringComponentContainer;
+import org.sonar.core.scadata.DefaultScaDataSourceImpl;
import org.sonar.server.ai.code.assurance.AiCodeAssuranceEntitlement;
import org.sonar.server.ai.code.assurance.NoOpAiCodeAssuranceVerifier;
import org.sonar.server.almintegration.ws.AlmIntegrationsWSModule;
@@ -70,6 +71,7 @@ import org.sonar.server.authentication.AuthenticationModule;
import org.sonar.server.authentication.DefaultAdminCredentialsVerifierImpl;
import org.sonar.server.authentication.DefaultAdminCredentialsVerifierNotificationHandler;
import org.sonar.server.authentication.DefaultAdminCredentialsVerifierNotificationTemplate;
+import org.sonar.server.authentication.HardcodedActiveTimeoutProvider;
import org.sonar.server.authentication.LogOAuthWarning;
import org.sonar.server.authentication.ws.AuthenticationWsModule;
import org.sonar.server.badge.ws.ProjectBadgesWsModule;
@@ -93,6 +95,7 @@ import org.sonar.server.common.github.config.GithubConfigurationService;
import org.sonar.server.common.gitlab.config.GitlabConfigurationService;
import org.sonar.server.common.group.service.GroupMembershipService;
import org.sonar.server.common.group.service.GroupService;
+import org.sonar.server.network.NetworkInterfaceProvider;
import org.sonar.server.common.newcodeperiod.NewCodeDefinitionResolver;
import org.sonar.server.common.permission.DefaultTemplatesResolverImpl;
import org.sonar.server.common.permission.GroupPermissionChanger;
@@ -207,6 +210,7 @@ import org.sonar.server.platform.telemetry.TelemetrySubportfolioSelectionModePro
import org.sonar.server.platform.telemetry.TelemetryUserEnabledProvider;
import org.sonar.server.platform.telemetry.TelemetryVersionProvider;
import org.sonar.server.platform.web.ActionDeprecationLoggerInterceptor;
+import org.sonar.server.platform.web.NoCacheFilter;
import org.sonar.server.platform.web.SonarQubeIdeConnectionFilter;
import org.sonar.server.platform.web.WebServiceFilter;
import org.sonar.server.platform.web.WebServiceReroutingFilter;
@@ -352,6 +356,7 @@ public class PlatformLevel4 extends PlatformLevel {
DefaultBranchNameResolver.class,
DelegatingManagedServices.class,
DelegatingDevOpsProjectCreatorFactory.class,
+ NetworkInterfaceProvider.class,
// ai code assurance
NoOpAiCodeAssuranceVerifier.class,
@@ -423,6 +428,7 @@ public class PlatformLevel4 extends PlatformLevel {
new WebServicesWsModule(),
SonarQubeIdeConnectionFilter.class,
WebServiceFilter.class,
+ NoCacheFilter.class,
WebServiceReroutingFilter.class,
// localization
@@ -446,6 +452,7 @@ public class PlatformLevel4 extends PlatformLevel {
DefaultAdminCredentialsVerifierImpl.class,
DefaultAdminCredentialsVerifierNotificationTemplate.class,
DefaultAdminCredentialsVerifierNotificationHandler.class,
+ HardcodedActiveTimeoutProvider.class,
// users
UserSessionFactoryImpl.class,
@@ -740,7 +747,10 @@ public class PlatformLevel4 extends PlatformLevel {
MainCollector.class,
- PluginsRiskConsentFilter.class);
+ PluginsRiskConsentFilter.class,
+
+ // sca-provided capabilities
+ DefaultScaDataSourceImpl.class);
// system info
add(new SystemInfoWriterModule(getWebServer()));
diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java
index d81006a91e3..4ef6d79166e 100644
--- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java
+++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java
@@ -26,6 +26,7 @@ import org.sonar.server.platform.db.migration.AutoDbMigration;
import org.sonar.server.platform.db.migration.DatabaseMigrationImpl;
import org.sonar.server.platform.db.migration.MigrationEngineModule;
import org.sonar.server.platform.db.migration.NoopDatabaseMigrationImpl;
+import org.sonar.server.platform.web.NoCacheFilter;
import org.sonar.server.platform.web.WebServiceFilter;
import org.sonar.server.platform.ws.IndexAction;
import org.sonar.server.platform.ws.L10nWs;
@@ -59,6 +60,7 @@ public class PlatformLevelSafeMode extends PlatformLevel {
SafeModeUserSession.class,
WebServiceEngine.class,
WebServiceFilter.class,
+ NoCacheFilter.class,
// Monitoring
ServerMonitoringMetrics.class);
diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/CspFilter.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/CspFilter.java
index f0678eb02b7..195aa01c6bf 100644
--- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/CspFilter.java
+++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/CspFilter.java
@@ -35,7 +35,6 @@ import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletResponse;
public class CspFilter implements Filter {
-
private final List<String> cspHeaders = new ArrayList<>();
private String policies = null;
@@ -48,12 +47,13 @@ public class CspFilter implements Filter {
cspPolicies.add("base-uri 'none'");
cspPolicies.add("connect-src 'self' http: https:");
cspPolicies.add("font-src 'self' data:");
+ cspPolicies.add("frame-src");
cspPolicies.add("img-src * data: blob:");
cspPolicies.add("object-src 'none'");
// the hash below corresponds to the window.__assetsPath script in index.html
cspPolicies.add("script-src 'self' " + getAssetsPathScriptCSPHash(filterConfig.getServletContext().getContextPath()));
cspPolicies.add("style-src 'self' 'unsafe-inline'");
- cspPolicies.add("worker-src 'none'");
+ cspPolicies.add("worker-src 'self'");
this.policies = String.join("; ", cspPolicies).trim();
}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/roots/UnsetRootRequest.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/NoCacheFilter.java
index 0d4ebef768b..268bafd3aed 100644
--- a/sonar-ws/src/main/java/org/sonarqube/ws/client/roots/UnsetRootRequest.java
+++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/NoCacheFilter.java
@@ -17,31 +17,30 @@
* 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.sonarqube.ws.client.roots;
+package org.sonar.server.platform.web;
-import jakarta.annotation.Generated;
+import org.sonar.api.server.http.HttpRequest;
+import org.sonar.api.server.http.HttpResponse;
+import org.sonar.api.web.FilterChain;
+import org.sonar.api.web.HttpFilter;
+import java.io.IOException;
+import org.sonar.api.web.UrlPattern;
-/**
- * This is part of the internal API.
- * This is a POST request.
- * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/roots/unset_root">Further information about this action online (including a response example)</a>
- * @since 6.2
- */
-@Generated("sonar-ws-generator")
-public class UnsetRootRequest {
+public class NoCacheFilter extends HttpFilter {
- private String login;
+ @Override
+ public void doFilter(HttpRequest httpRequest, HttpResponse httpResponse, FilterChain filterChain) throws IOException {
+ httpResponse.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ filterChain.doFilter(httpRequest, httpResponse);
+ }
/**
- * This is a mandatory parameter.
- * Example value: "admin"
+ * The Cache-Control for API v1 is handled in the org.sonar.server.ws.ServletResponse
*/
- public UnsetRootRequest setLogin(String login) {
- this.login = login;
- return this;
- }
-
- public String getLogin() {
- return login;
+ @Override
+ public UrlPattern doGetPattern() {
+ return UrlPattern.builder()
+ .includes("/api/v2/*")
+ .build();
}
}
diff --git a/server/sonar-webserver/src/test/java/org/sonar/server/app/EmbeddedTomcatTest.java b/server/sonar-webserver/src/test/java/org/sonar/server/app/EmbeddedTomcatTest.java
index 8974db2b3a7..8989be8ccd9 100644
--- a/server/sonar-webserver/src/test/java/org/sonar/server/app/EmbeddedTomcatTest.java
+++ b/server/sonar-webserver/src/test/java/org/sonar/server/app/EmbeddedTomcatTest.java
@@ -23,13 +23,16 @@ import java.io.File;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetAddress;
+import java.net.URI;
import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.Properties;
import org.apache.catalina.connector.Connector;
import org.apache.commons.io.FileUtils;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
import org.sonar.process.NetworkUtilsImpl;
import org.sonar.process.Props;
@@ -39,13 +42,17 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
-public class EmbeddedTomcatTest {
+class EmbeddedTomcatTest {
- @Rule
- public TemporaryFolder temp = new TemporaryFolder();
+ private Path tempDir;
+
+ @BeforeEach
+ void setUp() throws IOException {
+ tempDir = Files.createTempDirectory("temp-folder");
+ }
@Test
- public void start_shouldStartTomcatAndAcceptConnections() throws Exception {
+ void start_shouldStartTomcatAndAcceptConnections() throws Exception {
InetAddress address = InetAddress.getLoopbackAddress();
int httpPort = NetworkUtilsImpl.INSTANCE.getNextLoopbackAvailablePort();
Props props = getProps(address, httpPort);
@@ -55,13 +62,13 @@ public class EmbeddedTomcatTest {
tomcat.start();
assertThat(tomcat.getStatus()).isEqualTo(EmbeddedTomcat.Status.UP);
- URL url = new URL("http://" + address.getHostAddress() + ":" + httpPort);
+ URL url = new URI("http://" + address.getHostAddress() + ":" + httpPort).toURL();
assertThatCode(() -> url.openConnection().connect())
.doesNotThrowAnyException();
}
@Test
- public void start_whenWrongScheme_shouldThrow() throws IOException {
+ void start_whenWrongScheme_shouldThrow() throws IOException {
InetAddress address = InetAddress.getLoopbackAddress();
int httpPort = NetworkUtilsImpl.INSTANCE.getNextLoopbackAvailablePort();
Props props = getProps(address, httpPort);
@@ -84,34 +91,34 @@ public class EmbeddedTomcatTest {
}
@Test
- public void terminate_shouldTerminateTomcatAndStopAcceptingConnections() throws IOException {
+ void terminate_shouldTerminateTomcatAndStopAcceptingConnections() throws Exception {
InetAddress address = InetAddress.getLoopbackAddress();
int httpPort = NetworkUtilsImpl.INSTANCE.getNextLoopbackAvailablePort();
Props props = getProps(address, httpPort);
EmbeddedTomcat tomcat = new EmbeddedTomcat(props, new TomcatHttpConnectorFactory());
tomcat.start();
- URL url = new URL("http://" + address.getHostAddress() + ":" + httpPort);
+ URL url = new URI("http://" + address.getHostAddress() + ":" + httpPort).toURL();
tomcat.terminate();
assertThatThrownBy(() -> url.openConnection().connect())
.isInstanceOf(ConnectException.class)
- .hasMessage("Connection refused");
+ .hasMessageContaining("Connection refused");
}
private Props getProps(InetAddress address, int httpPort) throws IOException {
Props props = new Props(new Properties());
- File home = temp.newFolder();
- File data = temp.newFolder();
+ File home = new File(tempDir.toFile(), "homeDir");
+ File data = new File(tempDir.toFile(), "dataDir");
File webDir = new File(home, "web");
- FileUtils.write(new File(home, "web/WEB-INF/web.xml"), "<web-app/>");
+ FileUtils.writeByteArrayToFile(new File(home, "web/WEB-INF/web.xml"), "<web-app/>".getBytes(StandardCharsets.UTF_8));
props.set("sonar.path.home", home.getAbsolutePath());
props.set("sonar.path.data", data.getAbsolutePath());
props.set("sonar.path.web", webDir.getAbsolutePath());
- props.set("sonar.path.logs", temp.newFolder().getAbsolutePath());
+ props.set("sonar.path.logs", new File(tempDir.toFile(), "logsDir").getAbsolutePath());
// start server on a random port
props.set("sonar.web.host", address.getHostAddress());
diff --git a/server/sonar-webserver/src/test/java/org/sonar/server/platform/web/CspFilterTest.java b/server/sonar-webserver/src/test/java/org/sonar/server/platform/web/CspFilterTest.java
index d1cd89eae2b..c4e0b3cfcd4 100644
--- a/server/sonar-webserver/src/test/java/org/sonar/server/platform/web/CspFilterTest.java
+++ b/server/sonar-webserver/src/test/java/org/sonar/server/platform/web/CspFilterTest.java
@@ -42,11 +42,12 @@ public class CspFilterTest {
"base-uri 'none'; " +
"connect-src 'self' http: https:; " +
"font-src 'self' data:; " +
+ "frame-src; " +
"img-src * data: blob:; " +
"object-src 'none'; " +
"script-src 'self' 'sha256-hK8SVWFNHY0UhP61DBzX/3fvT74EI8u6/jRQvUKeZoU='; " +
"style-src 'self' 'unsafe-inline'; " +
- "worker-src 'none'";
+ "worker-src 'self'";
private final ServletContext servletContext = mock(ServletContext.class, RETURNS_MOCKS);
private final HttpServletResponse response = mock(HttpServletResponse.class);
private final FilterChain chain = mock(FilterChain.class);
diff --git a/server/sonar-webserver/src/test/java/org/sonar/server/platform/web/NoCacheFilterTest.java b/server/sonar-webserver/src/test/java/org/sonar/server/platform/web/NoCacheFilterTest.java
new file mode 100644
index 00000000000..18b54291215
--- /dev/null
+++ b/server/sonar-webserver/src/test/java/org/sonar/server/platform/web/NoCacheFilterTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.server.platform.web;
+
+import org.sonar.api.server.http.HttpRequest;
+import org.sonar.api.server.http.HttpResponse;
+import org.sonar.api.web.FilterChain;
+import org.junit.Test;
+import org.sonar.api.web.UrlPattern;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class NoCacheFilterTest {
+
+ private final NoCacheFilter filter = new NoCacheFilter();
+
+ @Test
+ public void doGetPattern_whenAPIv2_patternMatches() {
+ UrlPattern urlPattern = filter.doGetPattern();
+
+ assertThat(urlPattern.matches("/api/v2/whatever")).isTrue();
+ }
+
+ @Test
+ public void doGetPattern_whenAPIv1_patternDoesNotMatch() {
+ UrlPattern urlPattern = filter.doGetPattern();
+
+ assertThat(urlPattern.matches("/api/whatever")).isFalse();
+ }
+
+ @Test
+ public void doFilter_setResponseHeader() throws Exception{
+ HttpResponse response = mock();
+ HttpRequest request = mock();
+ FilterChain chain = mock();
+
+ filter.doFilter(request, response, chain);
+ verify(response).setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ }
+}
diff --git a/server/sonar-webserver/src/test/java/org/sonar/server/platform/web/StaticResourcesServletTest.java b/server/sonar-webserver/src/test/java/org/sonar/server/platform/web/StaticResourcesServletTest.java
index b9f772b70bb..0603e324a2b 100644
--- a/server/sonar-webserver/src/test/java/org/sonar/server/platform/web/StaticResourcesServletTest.java
+++ b/server/sonar-webserver/src/test/java/org/sonar/server/platform/web/StaticResourcesServletTest.java
@@ -19,10 +19,11 @@
*/
package org.sonar.server.platform.web;
+import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletResponse;
+import java.io.File;
import java.io.IOException;
import java.io.InputStream;
-import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
@@ -31,11 +32,10 @@ import java.nio.charset.Charset;
import java.util.Optional;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
+import org.apache.catalina.Context;
import org.apache.catalina.connector.ClientAbortException;
+import org.apache.catalina.startup.Tomcat;
import org.apache.commons.io.IOUtils;
-import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.servlet.ServletContextHandler;
-import org.eclipse.jetty.servlet.ServletHolder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -56,7 +56,8 @@ class StaticResourcesServletTest {
@RegisterExtension
LogTesterJUnit5 logTester = new LogTesterJUnit5();
- private Server jetty;
+ private Tomcat tomcat;
+ private int port;
private final PluginRepository pluginRepository = mock(PluginRepository.class);
private final CoreExtensionRepository coreExtensionRepository = mock(CoreExtensionRepository.class);
@@ -65,31 +66,39 @@ class StaticResourcesServletTest {
@BeforeEach
void setUp() throws Exception {
logTester.setLevel(Level.TRACE);
- jetty = new Server(InetSocketAddress.createUnresolved("localhost", 0));
- ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
- context.setContextPath("/");
- ServletHolder servletHolder = new ServletHolder(new StaticResourcesServlet(system));
- context.addServlet(servletHolder, "/static/*");
- jetty.setHandler(context);
- jetty.start();
+ tomcat = new Tomcat();
+ tomcat.setPort(0); // Use an ephemeral port
+
+ String contextPath = "/";
+ String docBase = new File(".").getAbsolutePath();
+
+ Context context = tomcat.addContext(contextPath, docBase);
+
+ HttpServlet servlet = new StaticResourcesServlet(system);
+ String servletName = "staticResourcesServlet";
+ Tomcat.addServlet(context, servletName, servlet);
+ context.addServletMappingDecoded("/static/*", servletName);
+
+ tomcat.start();
+ port = tomcat.getConnector().getLocalPort();
}
@AfterEach
- public void tearDown() throws Exception {
- if (jetty != null) {
- jetty.stop();
+ void tearDown() throws Exception {
+ if (tomcat != null) {
+ tomcat.stop();
}
}
private HttpResponse<String> callAndStop(String path) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
- .uri(jetty.getURI().resolve(URI.create(path)))
+ .uri(URI.create("http://localhost:" + port + path))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
- jetty.stop();
+ tomcat.stop();
return response;
}
diff --git a/settings.gradle b/settings.gradle
index 1c3627384d1..aaa2a94a961 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -19,7 +19,8 @@ pluginManagement {
}
}
plugins {
- id("com.gradle.develocity") version("3.18.2")
+ id("com.gradle.develocity") version("4.0.2")
+ id("com.gradle.common-custom-user-data-gradle-plugin") version "2.2.1"
}
rootProject.name = 'sonarqube'
@@ -43,6 +44,7 @@ include 'server:sonar-db-migration'
include 'server:sonar-main'
include 'server:sonar-process'
include 'server:sonar-server-common'
+include 'server:sonar-statemachine'
include 'server:sonar-telemetry'
include 'server:sonar-telemetry-core'
include 'server:sonar-webserver'
@@ -76,10 +78,15 @@ include 'sonar-ws-generator'
ext.isCiServer = System.getenv().containsKey("CIRRUS_CI")
develocity {
+ projectId = "sonar-enterprise"
server = "https://develocity.sonar.build"
buildScan {
uploadInBackground.set(!isCiServer)
publishing.onlyIf { isCiServer && it.authenticated }
+ if (isCiServer) {
+ buildScan.value 'Branch', System.getenv()["CIRRUS_BRANCH"]
+ buildScan.value 'Cirrus Link', "https://cirrus-ci.com/task/" + System.getenv()["CIRRUS_TASK_ID"]
+ }
}
}
@@ -93,7 +100,6 @@ buildCache {
}
-
// use Settings.getRootDir() so that it doesn't matter which directory you are executing from
File extraSettings = new File(rootDir, 'private/private-settings.gradle')
if (extraSettings.exists()) {
diff --git a/sonar-application/build.gradle b/sonar-application/build.gradle
index fea954c8601..499f4e34d71 100644
--- a/sonar-application/build.gradle
+++ b/sonar-application/build.gradle
@@ -151,6 +151,14 @@ task zip(type: Zip, dependsOn: [configurations.compileClasspath]) {
exclude 'bin/windows-x86-64/StartSonar.bat'
exclude 'bin/linux-x86-64/sonar.sh'
exclude 'bin/macosx-universal-64/sonar.sh'
+ rename { fileName ->
+ if (fileName == 'CVE-review-and-treatment-status-sqcb.csv') {
+ return "CVE-review-and-treatment-status-sqcb-${version}.csv"
+ } else if (fileName == 'CVE-review-and-treatment-status-sqcb.json') {
+ return "CVE-review-and-treatment-status-sqcb-${version}.json"
+ }
+ return fileName
+ }
}
}
@@ -353,14 +361,14 @@ zip {
//When the archive size increases due to dependencies, the expected size should be updated as well.
//Bump the expected size by at least 10 more megabytes than what is strictly needed, this in conjunction with the
//tolerance will allow for some growth in the archive size.
- def expectedSize = 830_000_000
+ def expectedSize = 850_000_000
//We set a tolerance of 15MB to avoid failing the build for small differences in the archive size.
def tolerance = 15_000_000
def minArchiveSize = expectedSize - tolerance
def maxArchiveSize = expectedSize + tolerance
def archiveSize = archiveFile.get().asFile.length()
- if (archiveSize < minArchiveSize)
+ if (archiveSize < minArchiveSize && System.getenv("CI") == "true")
throw new GradleException("${archiveFileName.get()} size ($archiveSize) too small. Min is $minArchiveSize")
if (archiveSize > maxArchiveSize)
throw new GradleException("${destinationDirectory.get()}/${archiveFileName.get()} size ($archiveSize) too large. Max is $maxArchiveSize")
diff --git a/sonar-application/bundled_plugins.gradle b/sonar-application/bundled_plugins.gradle
index cbbfc1737df..2e060e4d108 100644
--- a/sonar-application/bundled_plugins.gradle
+++ b/sonar-application/bundled_plugins.gradle
@@ -13,6 +13,7 @@ dependencies {
bundledPlugin 'org.sonarsource.python:sonar-python-plugin'
bundledPlugin "org.sonarsource.kotlin:sonar-kotlin-plugin"
bundledPlugin "org.sonarsource.slang:sonar-ruby-plugin"
+ bundledPlugin 'org.sonarsource.rust:sonar-rust-plugin'
bundledPlugin "org.sonarsource.slang:sonar-scala-plugin"
bundledPlugin 'org.sonarsource.xml:sonar-xml-plugin'
bundledPlugin 'org.sonarsource.iac:sonar-iac-plugin'
diff --git a/sonar-application/src/main/assembly/conf/sonar.properties b/sonar-application/src/main/assembly/conf/sonar.properties
index aa768dd1183..a3385f0f913 100644
--- a/sonar-application/src/main/assembly/conf/sonar.properties
+++ b/sonar-application/src/main/assembly/conf/sonar.properties
@@ -87,7 +87,7 @@
#
# Startup can be long if entropy source is short of entropy. Adding
# -Djava.security.egd=file:/dev/./urandom is an option to resolve the problem.
-# See https://wiki.apache.org/tomcat/HowTo/FasterStartUp#Entropy_Source
+# See https://cwiki.apache.org/confluence/display/TOMCAT/HowTo+FasterStartUp#HowToFasterStartUp-EntropySource
#
#sonar.web.javaOpts=@webJavaOpts@
@@ -137,10 +137,20 @@
# echo -n "type_what_you_want" | openssl dgst -sha256 -hmac "key" -binary | base64
#sonar.auth.jwtBase64Hs256Secret=
-# The inactivity timeout duration of user sessions, in minutes. After the configured
-# period of time, the user is logged out.
+# Active Session Timeout
+# Enterprise+ edition only feature
+# The maximum time a user can remain logged in, regardless of activity.
+# After this time, the session ends automatically even if the user is actively using the system.
+# The default value is set to 90 days (129 600 minutes).
+# It must be set between 15 minutes and 90 days (129 600 minutes).
+# Value must be strictly positive.
+#sonar.web.activeSessionTimeoutInMinutes=129600
+
+# Inactive Session Timeout
+# The maximum time a user can remain idle (no activity) before the session ends.
+# If the user does not interact with the system within this time, they are logged out.
# The default value is set to 3 days (4320 minutes).
-# It must be set between 6 minutes and 3 months (129600 minutes).
+# It must be set between 6 minutes and 90 days (129 600 minutes).
# Value must be strictly positive.
#sonar.web.sessionTimeoutInMinutes=4320
diff --git a/sonar-application/src/main/assembly/security/CVE-review-and-treatment-status-sqcb.csv b/sonar-application/src/main/assembly/security/CVE-review-and-treatment-status-sqcb.csv
new file mode 100644
index 00000000000..76321e10cfc
--- /dev/null
+++ b/sonar-application/src/main/assembly/security/CVE-review-and-treatment-status-sqcb.csv
@@ -0,0 +1,61 @@
+Vulnerability ID,Library,Severity,CVSS,CVSS Type,Status,Library Type,Comment
+CVE-2020-36843,eddsa-0.3.0.jar,MEDIUM,4.3,CVSS_3,Ignored,Java,The transitive dependency has been removed.
+CVE-2025-49146,postgresql-42.7.6.jar,HIGH,8.2,CVSS_3,Ignored,Java,SonarQube is not vulnerable as it doesn't use channel binding set to required.
+CVE-2025-41234,spring-web-6.2.7.jar,MEDIUM,6.5,CVSS_3,Ignored,Java,"SonarQube is not vulnerable as it does not use ContentDisposition.Builder#filename(String, Charset)"
+CVE-2021-22570,google.protobuf.3.6.1.nupkg,MEDIUM,6.5,CVSS_3,Ignored,Nuget,The protobuf payload is both generated and consumed by the user of SonarQube . An external attacker would need already access to the machine to exploit this.
+CVE-2024-38081,microsoft.io.redist.6.0.0.nupkg,HIGH,7.3,CVSS_3,Ignored,Nuget,"This dependency is only used for product unit testing and it's not included in the product package. The CVE is registered as ""unproven""."
+CVE-2025-26646,microsoft.build.tasks.core.17.10.4.nupkg,HIGH,8,CVSS_3,Ignored,Nuget,This dependency is only used for product unit testing and it's not included in the product package.
+CVE-2025-26646,microsoft.build.tasks.core.17.7.2.nupkg,HIGH,8,CVSS_3,Ignored,Nuget,This dependency is only used for product unit testing and it's not included in the product package.
+CVE-2024-38095,system.formats.asn1.7.0.0.nupkg,HIGH,7.5,CVSS_3,Ignored,Nuget,"This dependency is only used for product unit testing and it's not included in the product package. The CVE is registered as ""unproven""."
+CVE-2024-43485,microsoft.codeanalysis.workspaces.msbuild.4.12.0-1.final.nupkg,HIGH,7.5,CVSS_3,Ignored,Nuget,"This library is used by the TestFramework and it's not included in the product package. The CVE is registered as ""unproven"". The risk is a DDoS on the test system."
+CVE-2023-0833,okhttp-4.5.0.jar,MEDIUM,4.7,CVSS_3,Ignored,Java,This transitive test dependency is not shipped with the analyzers
+CVE-2024-7254,protobuf-java-3.21.12.jar,HIGH,7.5,CVSS_3,Ignored,Java,This transitive test dependency is not shipped with the analyzers
+CVE-2022-24329,kotlin-stdlib-1.3.70.jar,MEDIUM,5.3,CVSS_3,Ignored,Java,This transitive test dependency is not shipped with the analyzers
+CVE-2020-29582,kotlin-stdlib-1.3.70.jar,MEDIUM,5.3,CVSS_3,Ignored,Java,This transitive test dependency is not shipped with the analyzers
+CVE-2023-3635,okio-2.5.0.jar,MEDIUM,5.9,CVSS_3,Ignored,Java,This transitive test dependency is not shipped with the analyzers
+CVE-2023-3635,okio-jvm-3.0.0.jar,MEDIUM,5.9,CVSS_3,Ignored,Java,This transitive test dependency is not shipped with the analyzers
+CVE-2020-36518,jackson-databind-2.13.2.jar,HIGH,7.5,CVSS_3,Ignored,Java,Library jackson-databind-2.13.2.jar is a transitive dependency of Orchestrator only used to run the integration tests of plugins
+CVE-2022-40152,woodstox-core-6.2.7.jar,MEDIUM,6.5,CVSS_3,Ignored,Java,Library woodstox-core-6.2.7.jar is a transitive dependency of Orchestrator only used to run the integration tests of plugins
+CVE-2022-42003,jackson-databind-2.13.2.jar,HIGH,7.5,CVSS_3,Ignored,Java,Library jackson-databind-2.13.2.jar is a transitive dependency of Orchestrator only used to run the integration tests of plugins
+CVE-2024-47554,commons-io-2.7.jar,MEDIUM,4.3,CVSS_3,Ignored,Java,"This is a transitive dependency over the sonar-orchestrator library, which is only used for testing and is not shipped with the product."
+CVE-2022-42004,jackson-databind-2.13.2.jar,HIGH,7.5,CVSS_3,Ignored,Java,Library jackson-databind-2.13.2.jar is a transitive dependency of Orchestrator only used to run the integration tests of plugins
+CVE-2024-12801,logback-core-1.2.13.jar,MEDIUM,4.4,CVSS_3,Ignored,Java,This transitive test dependency is not shipped with the analyzers
+CVE-2024-12801,logback-core-1.3.12.jar,MEDIUM,4.4,CVSS_3,Ignored,Java,This transitive test dependency is not shipped with the analyzers
+CVE-2024-12798,logback-core-1.3.12.jar,MEDIUM,6.6,CVSS_3,Ignored,Java,This transitive test dependency is not shipped with the analyzers
+CVE-2024-12798,logback-core-1.2.13.jar,MEDIUM,6.6,CVSS_3,Ignored,Java,This transitive test dependency is not shipped with the analyzers
+CVE-2024-12798,logback-classic-1.3.12.jar,MEDIUM,6.6,CVSS_3,Ignored,Java,This transitive test dependency is not shipped with the analyzers
+CVE-2024-12798,logback-classic-1.2.13.jar,MEDIUM,6.6,CVSS_3,Ignored,Java,This transitive test dependency is not shipped with the analyzers
+WS-2022-0468,jackson-core-2.13.2.jar,HIGH,7.5,CVSS_3,Ignored,Java,Library jackson-core-2.13.2.jar is a transitive dependency of Orchestrator only used to run the integration tests of plugins
+CVE-2025-52999,jackson-core-2.13.2.jar,HIGH,7.5,CVSS_3,Ignored,Java,The jackson-core-2.13.2.jar library is a transitive dependency of Orchestrator and is used only during compile and test time and is not included in the final Ruby Analyzer.
+CVE-2025-48734,commons-beanutils-1.9.4.jar,HIGH,8.8,CVSS_3,Ignored,Java,commons-beanutils:commons-beanutils:1.9.4 is used only within integration tests and is not shipped in the final product
+WS-2022-0468,jackson-core-2.13.2.jar,HIGH,7.5,CVSS_3,Ignored,Java,Library jackson-core-2.13.2.jar is a transitive dependency of Orchestrator only used to run the integration tests of plugins
+CVE-2022-40152,woodstox-core-6.2.7.jar,MEDIUM,6.5,CVSS_3,Ignored,Java,Library woodstox-core-6.2.7.jar is a transitive dependency of Orchestrator only used to run the integration tests of plugins
+CVE-2020-36518,jackson-databind-2.13.2.jar,HIGH,7.5,CVSS_3,Ignored,Java,Library jackson-databind-2.13.2.jar is a transitive dependency of Orchestrator only used to run the integration tests of plugins
+CVE-2023-0833,okhttp-4.5.0.jar,MEDIUM,4.7,CVSS_3,Ignored,Java,Library okhttp-4.5.0.jar is a transitive dependency of Orchestrator only used to run the integration tests of plugins and is not included in either python plugins
+CVE-2022-24329,kotlin-stdlib-1.3.70.jar,MEDIUM,5.3,CVSS_3,Ignored,Java,Library kotlin-stdlib-1.3.70.jar is a transitive dependency of Orchestrator only used to run the integration tests of plugins and is not included in either python plugins
+CVE-2022-42003,jackson-databind-2.13.2.jar,HIGH,7.5,CVSS_3,Ignored,Java,Library jackson-databind-2.13.2.jar is a transitive dependency of Orchestrator only used to run the integration tests of plugins
+CVE-2022-42004,jackson-databind-2.13.2.jar,HIGH,7.5,CVSS_3,Ignored,Java,Library jackson-databind-2.13.2.jar is a transitive dependency of Orchestrator only used to run the integration tests of plugins
+CVE-2023-3635,okio-2.5.0.jar,MEDIUM,5.9,CVSS_3,Ignored,Java,Library okio-2.5.0.jar is a transitive dependency of Orchestrator only used to run the integration tests of plugins and is not included in either plugins
+CVE-2020-29582,kotlin-stdlib-1.3.70.jar,MEDIUM,5.3,CVSS_3,Ignored,Java,Library kotlin-stdlib-1.3.70.jar is a transitive dependency of Orchestrator only used to run the integration tests of plugins and is not included in either python plugins
+CVE-2024-7254,protobuf-java-3.21.12.jar,HIGH,7.5,CVSS_3,Ignored,Java,This transitive test dependency is not shipped with the analyzers
+CVE-2023-46122,io_2.13-1.6.0.jar,LOW,3.9,CVSS_3,Ignored,Java,"This dependency is used by zinc that is used to build the analyzer, but it is not shipped with the product."
+CVE-2023-0833,okhttp-4.5.0.jar,MEDIUM,4.7,CVSS_3,Ignored,Java,This transitive test dependency is not shipped with the analyzers
+CVE-2022-36944,scala-library-2.13.6.jar,CRITICAL,9.8,CVSS_3,Ignored,Java,"This dependency is used by zinc that is used to build the analyzer, but it is not shipped with the product."
+CVE-2023-3635,okio-jvm-3.0.0.jar,MEDIUM,5.9,CVSS_3,Ignored,Java,This transitive test dependency is not shipped with the analyzers
+CVE-2023-50572,jline-3.19.0.jar,MEDIUM,5.5,CVSS_3,Ignored,Java,"This dependency is used by zinc that is used to build the analyzer, but it is not shipped with the product."
+CVE-2020-29582,kotlin-stdlib-1.3.70.jar,MEDIUM,5.3,CVSS_3,Ignored,Java,This transitive test dependency is not shipped with the analyzers
+CVE-2022-24329,kotlin-stdlib-1.3.70.jar,MEDIUM,5.3,CVSS_3,Ignored,Java,This transitive test dependency is not shipped with the analyzers
+CVE-2023-3635,okio-2.5.0.jar,MEDIUM,5.9,CVSS_3,Ignored,Java,This transitive test dependency is not shipped with the analyzers
+CVE-2022-42003,jackson-databind-2.13.2.jar,HIGH,7.5,CVSS_3,Ignored,Java,Library jackson-databind-2.13.2.jar is a transitive dependency of Orchestrator only used to run the integration tests of plugins
+CVE-2022-42004,jackson-databind-2.13.2.jar,HIGH,7.5,CVSS_3,Ignored,Java,Library jackson-databind-2.13.2.jar is a transitive dependency of Orchestrator only used to run the integration tests of plugins
+CVE-2024-47554,commons-io-2.7.jar,MEDIUM,4.3,CVSS_3,Ignored,Java,"This is a transitive dependency used by the sonar-orchestrator library, which is only used for testing and is not shipped with the product."
+CVE-2020-36518,jackson-databind-2.13.2.jar,HIGH,7.5,CVSS_3,Ignored,Java,Library jackson-databind-2.13.2.jar is a transitive dependency of Orchestrator only used to run the integration tests of plugins
+CVE-2022-40152,woodstox-core-6.2.7.jar,MEDIUM,6.5,CVSS_3,Ignored,Java,Library woodstox-core-6.2.7.jar is a transitive dependency of Orchestrator only used to run the integration tests of plugins
+CVE-2024-12801,logback-core-1.3.12.jar,MEDIUM,4.4,CVSS_3,Ignored,Java,This transitive test dependency is not shipped with the analyzers
+CVE-2024-12798,logback-core-1.2.13.jar,MEDIUM,6.6,CVSS_3,Ignored,Java,This transitive test dependency is not shipped with the analyzers
+CVE-2024-12801,logback-core-1.2.13.jar,MEDIUM,4.4,CVSS_3,Ignored,Java,This transitive test dependency is not shipped with the analyzers
+CVE-2024-12798,logback-core-1.3.12.jar,MEDIUM,6.6,CVSS_3,Ignored,Java,This transitive test dependency is not shipped with the analyzers
+CVE-2024-12798,logback-classic-1.2.13.jar,MEDIUM,6.6,CVSS_3,Ignored,Java,This transitive test dependency is not shipped with the analyzers
+CVE-2024-12798,logback-classic-1.3.12.jar,MEDIUM,6.6,CVSS_3,Ignored,Java,This transitive test dependency is not shipped with the analyzers
+WS-2022-0468,jackson-core-2.13.2.jar,HIGH,7.5,CVSS_3,Ignored,Java,Library jackson-core-2.13.2.jar is a transitive dependency of Orchestrator only and is used to run the integration tests of plugins
+CVE-2025-52999,jackson-core-2.13.2.jar,HIGH,7.5,CVSS_3,Ignored,Java,The jackson-core-2.13.2.jar library is a transitive dependency of Orchestrator. This dependency is used only during compile and test time and is not included in the final scanner for Gradle product. \ No newline at end of file
diff --git a/sonar-core/src/main/java/org/sonar/core/config/AiCodefixPropertyDefinitions.java b/sonar-core/src/main/java/org/sonar/core/config/AiCodefixPropertyDefinitions.java
new file mode 100644
index 00000000000..328cfb94813
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/config/AiCodefixPropertyDefinitions.java
@@ -0,0 +1,47 @@
+/*
+ * 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.core.config;
+
+import java.util.List;
+import org.sonar.api.PropertyType;
+import org.sonar.api.config.PropertyDefinition;
+
+import static org.sonar.api.config.PropertyDefinition.builder;
+
+public class AiCodefixPropertyDefinitions {
+ public static final String PROP_AI_CODEFIX_HIDDEN = "sonar.ai.codefix.hidden";
+ public static final String AI_CODE_CATEGORY = "ai_codefix";
+
+ private AiCodefixPropertyDefinitions() {
+ // only static stuff
+ }
+
+ public static List<PropertyDefinition> all() {
+ return List.of(
+ builder(PROP_AI_CODEFIX_HIDDEN)
+ .name("AI Codefix feature hidden")
+ .description("Defines if the AI Codefix feature should be hidden across the product, including its marketing content.")
+ .type(PropertyType.BOOLEAN)
+ .hidden()
+ .category(AI_CODE_CATEGORY)
+ .defaultValue(Boolean.toString(false))
+ .build());
+ }
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java b/sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java
index e4055f7c1b1..beb314cae1e 100644
--- a/sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java
+++ b/sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java
@@ -66,6 +66,7 @@ public class CorePropertyDefinitions {
defs.addAll(PurgeProperties.all());
defs.addAll(ScannerProperties.all());
defs.addAll(MQRModeProperties.all());
+ defs.addAll(AiCodefixPropertyDefinitions.all());
defs.addAll(asList(
PropertyDefinition.builder(CoreProperties.MODULE_LEVEL_ARCHIVED_SETTINGS)
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java
index c26edf6709e..c8e1a7fa4a8 100644
--- a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java
+++ b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java
@@ -28,7 +28,6 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
-import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -518,25 +517,6 @@ public class DefaultIssue implements Issue, Trackable {
return this;
}
- /**
- * @deprecated since 9.4, attribute was already not returning any element since 5.2
- */
- @Deprecated
- @Override
- @CheckForNull
- public String attribute(String key) {
- return null;
- }
-
- /**
- * @deprecated since 9.4, attribute was already not returning any element since 5.2
- */
- @Deprecated
- @Override
- public Map<String, String> attributes() {
- return new HashMap<>();
- }
-
@Override
@CheckForNull
public String authorLogin() {
diff --git a/sonar-core/src/main/java/org/sonar/core/scadata/DefaultScaDataSourceImpl.java b/sonar-core/src/main/java/org/sonar/core/scadata/DefaultScaDataSourceImpl.java
new file mode 100644
index 00000000000..24276ae7a53
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/scadata/DefaultScaDataSourceImpl.java
@@ -0,0 +1,41 @@
+/*
+ * 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.core.scadata;
+
+import jakarta.annotation.Priority;
+import org.sonar.api.server.ServerSide;
+
+import java.util.OptionalInt;
+
+/**
+ * Default implementation of {@link ScaDataSource} that provides default, no-op values
+ * when SCA extension is not available.
+ */
+@ServerSide
+@Priority(2)
+public class DefaultScaDataSourceImpl implements ScaDataSource {
+ public int getVulnerabilityCount(String componentUuid) {
+ return 0;
+ }
+
+ public OptionalInt getVulnerabilityRating(String componentUuid) {
+ return OptionalInt.empty();
+ }
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/scadata/ScaDataSource.java b/sonar-core/src/main/java/org/sonar/core/scadata/ScaDataSource.java
new file mode 100644
index 00000000000..10b56d79b9c
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/scadata/ScaDataSource.java
@@ -0,0 +1,33 @@
+/*
+ * 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.core.scadata;
+
+import java.util.OptionalInt;
+
+public interface ScaDataSource {
+ /**
+ * The component UUID could be a regular project, or it could be an application.
+ * It is not yet resolved to a list of real branches.
+ * @param componentUuid the component UUID
+ * @return count of how many vulnerabilities
+ */
+ int getVulnerabilityCount(String componentUuid);
+ OptionalInt getVulnerabilityRating(String componentUuid);
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/scadata/package-info.java b/sonar-core/src/main/java/org/sonar/core/scadata/package-info.java
new file mode 100644
index 00000000000..f0dba2ec833
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/scadata/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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.core.scadata;
diff --git a/sonar-core/src/main/java/org/sonar/core/util/ProcessWrapperFactory.java b/sonar-core/src/main/java/org/sonar/core/util/ProcessWrapperFactory.java
index 465008931ad..d68ebbc01f1 100644
--- a/sonar-core/src/main/java/org/sonar/core/util/ProcessWrapperFactory.java
+++ b/sonar-core/src/main/java/org/sonar/core/util/ProcessWrapperFactory.java
@@ -23,6 +23,7 @@ import java.io.IOException;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import javax.annotation.Nullable;
import org.apache.commons.exec.CommandLine;
@@ -46,12 +47,8 @@ public class ProcessWrapperFactory {
// nothing to do
}
- public ProcessWrapper create(@Nullable Path baseDir, Consumer<String> stdOutLineConsumer, String... command) {
- return new ProcessWrapper(baseDir, stdOutLineConsumer, LOG::debug, Map.of(), command);
- }
-
- public ProcessWrapper create(@Nullable Path baseDir, Consumer<String> stdOutLineConsumer, Map<String, String> envVariablesOverrides, String... command) {
- return new ProcessWrapper(baseDir, stdOutLineConsumer, LOG::debug, envVariablesOverrides, command);
+ public ProcessWrapper create(@Nullable Path baseDir, Consumer<String> stdOutLineConsumer, Consumer<String> stdErrLineConsumer, String... command) {
+ return new ProcessWrapper(baseDir, stdOutLineConsumer, stdErrLineConsumer, Map.of(), command);
}
public ProcessWrapper create(@Nullable Path baseDir, Consumer<String> stdOutLineConsumer, Consumer<String> stdErrLineConsumer, Map<String, String> envVariablesOverrides,
@@ -66,6 +63,7 @@ public class ProcessWrapperFactory {
private final Consumer<String> stdErrLineConsumer;
private final String[] command;
private final Map<String, String> envVariables = new HashMap<>();
+ private final AtomicReference<Exception> exceptionWhileProcessingStream = new AtomicReference<>();
private ExecuteWatchdog watchdog = null;
ProcessWrapper(@Nullable Path baseDir, Consumer<String> stdOutLineConsumer, Consumer<String> stdErrLineConsumer, Map<String, String> envVariablesOverrides, String... command) {
@@ -97,6 +95,9 @@ public class ProcessWrapperFactory {
if (exitValue != 0 && !watchdog.killedProcess()) {
throw new IllegalStateException(format("Command execution exited with code: %d", exitValue), resultHandler.getException());
}
+ if (exceptionWhileProcessingStream.get() != null) {
+ throw new IllegalStateException("Error while processing stream for command", exceptionWhileProcessingStream.get());
+ }
} catch (InterruptedException e) {
LOG.warn("Command [{}] interrupted", join(" ", command), e);
Thread.currentThread().interrupt();
@@ -109,17 +110,9 @@ public class ProcessWrapperFactory {
builder.setWorkingDirectory(baseDir.toFile());
}
- PumpStreamHandler psh = new PumpStreamHandler(new LogOutputStream() {
- @Override
- protected void processLine(String line, int logLevel) {
- stdOutLineConsumer.accept(line);
- }
- }, new LogOutputStream() {
- @Override
- protected void processLine(String line, int logLevel) {
- stdErrLineConsumer.accept("[stderr] %s".formatted(line));
- }
- });
+ PumpStreamHandler psh = new PumpStreamHandler(
+ new ExceptionCatchingLogOutputStream(stdOutLineConsumer),
+ new ExceptionCatchingLogOutputStream(stdErrLineConsumer));
builder.setExecuteStreamHandler(psh);
var executor = builder.get();
@@ -131,5 +124,24 @@ public class ProcessWrapperFactory {
public void destroy() {
watchdog.destroyProcess();
}
+
+ private class ExceptionCatchingLogOutputStream extends LogOutputStream {
+
+ private final Consumer<String> lineConsumer;
+
+ public ExceptionCatchingLogOutputStream(Consumer<String> lineConsumer) {
+ this.lineConsumer = lineConsumer;
+ }
+
+ @Override
+ protected void processLine(String line, int logLevel) {
+ try {
+ lineConsumer.accept(line);
+ } catch (Exception e) {
+ exceptionWhileProcessingStream.compareAndSet(null, e);
+ watchdog.destroyProcess();
+ }
+ }
+ }
}
}
diff --git a/sonar-core/src/test/java/org/sonar/core/scadata/DefaultScaDataSourceImplTest.java b/sonar-core/src/test/java/org/sonar/core/scadata/DefaultScaDataSourceImplTest.java
new file mode 100644
index 00000000000..08c6a38f324
--- /dev/null
+++ b/sonar-core/src/test/java/org/sonar/core/scadata/DefaultScaDataSourceImplTest.java
@@ -0,0 +1,48 @@
+/*
+ * 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.core.scadata;
+
+import org.junit.Test;
+import java.util.OptionalInt;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.mock;
+
+public class DefaultScaDataSourceImplTest {
+ private final DefaultScaDataSourceImpl dataSource = mock(DefaultScaDataSourceImpl.class);
+
+ @Test
+ public void getVulnerabilityCount_defaultsToZero() {
+ String componentUuid = "component-uuid";
+
+ int vulnerabilityCount = dataSource.getVulnerabilityCount(componentUuid);
+
+ assertEquals(0, vulnerabilityCount);
+ }
+
+ @Test
+ public void getVulnerabilityRating_defaultsToEmpty() {
+ String componentUuid = "component-uuid";
+
+ OptionalInt vulnerabilityRating = dataSource.getVulnerabilityRating(componentUuid);
+
+ assertEquals(OptionalInt.empty(), vulnerabilityRating);
+ }
+}
diff --git a/sonar-core/src/test/java/org/sonar/core/util/ProcessWrapperFactoryTest.java b/sonar-core/src/test/java/org/sonar/core/util/ProcessWrapperFactoryTest.java
index 6dffe58aa91..04627cd808c 100644
--- a/sonar-core/src/test/java/org/sonar/core/util/ProcessWrapperFactoryTest.java
+++ b/sonar-core/src/test/java/org/sonar/core/util/ProcessWrapperFactoryTest.java
@@ -33,6 +33,7 @@ import org.apache.commons.lang3.SystemUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
import org.sonar.api.testfixtures.log.LogTesterJUnit5;
@@ -49,12 +50,15 @@ class ProcessWrapperFactoryTest {
@Test
void should_log_error_output_in_debug_mode(@TempDir Path root) {
logTester.setLevel(Level.DEBUG);
- var processWrapper = underTest.create(root, v -> {
- }, Map.of("LANG", "en_US"), "git", "blame");
+
+ Consumer<String> stdoutConsumer = s -> {
+ };
+ Consumer<String> stderrConsumer = LoggerFactory.getLogger(ProcessWrapperFactoryTest.class)::debug;
+ var processWrapper = underTest.create(root, stdoutConsumer, stderrConsumer, Map.of("LANG", "en_US"), "git", "blame");
assertThatThrownBy(processWrapper::execute)
.isInstanceOf(IllegalStateException.class);
- assertThat(logTester.logs(Level.DEBUG).get(0)).startsWith("[stderr] fatal:");
+ assertThat(logTester.logs(Level.DEBUG).get(0)).startsWith("fatal:");
}
// SONAR-24376
@@ -67,8 +71,9 @@ class ProcessWrapperFactoryTest {
}
var stdoutHandler = new DestroyProcessAfter10Lines();
+ var stderrHandler = new DestroyProcessAfter10Lines();
- var processWrapper = underTest.create(temp, stdoutHandler::process,
+ var processWrapper = underTest.create(temp, stdoutHandler::process, stderrHandler::process,
SystemUtils.IS_OS_WINDOWS ? new String[] {"cmd.exe", "/c", "type stdout.txt"} : new String[] {"cat", "stdout.txt"});
stdoutHandler.wrapper = processWrapper;
@@ -78,21 +83,44 @@ class ProcessWrapperFactoryTest {
}
@Test
+ void should_not_deadlock_when_stream_handler_throw_exception(@TempDir Path temp) throws IOException {
+ var bigFile = temp.resolve("stdout.txt");
+ for (int i = 0; i < 1024; i++) {
+ Files.writeString(bigFile, StringUtils.repeat("a", 1024), StandardCharsets.UTF_8, StandardOpenOption.APPEND, StandardOpenOption.CREATE);
+ Files.writeString(bigFile, "\n", StandardCharsets.UTF_8, StandardOpenOption.APPEND);
+ }
+
+ var stdoutHandler = new ThrowExceptionForEveryLine();
+
+ var processWrapper = underTest.create(temp, stdoutHandler::process, l -> {
+ },
+ SystemUtils.IS_OS_WINDOWS ? new String[] {"cmd.exe", "/c", "type stdout.txt"} : new String[] {"cat", "stdout.txt"});
+
+ assertThatThrownBy(processWrapper::execute)
+ .hasMessage("Error while processing stream for command")
+ .hasCauseInstanceOf(IllegalStateException.class)
+ .hasStackTraceContaining("Some error");
+ }
+
+ @Test
void should_apply_env_overrides_on_top_of_parent_env(@TempDir Path temp) throws IOException {
ConcurrentLinkedDeque<String> logs = new ConcurrentLinkedDeque<>();
Consumer<String> stdoutHandler = logs::add;
+ Consumer<String> stderrHandler = logs::add;
- var processWrapper = underTest.create(temp, stdoutHandler, Map.of("FOO", "BAR"),
+ var processWrapper = underTest.create(temp, stdoutHandler, stderrHandler, Map.of("FOO", "BAR"),
SystemUtils.IS_OS_WINDOWS ? new String[] {"cmd.exe", "/c", "echo %PATH% & echo %FOO%"} : new String[] {"/bin/bash", "-c", "echo $PATH; echo $FOO;"});
processWrapper.execute();
- assertThat(logs).containsExactly(System.getenv("PATH"), "BAR");
+ // Trim all output lines to avoid issues with trailing spaces on Windows environment variables
+ var trimmedLogs = logs.stream().map(String::trim).toList();
+ assertThat(trimmedLogs).containsExactly(System.getenv("PATH").trim(), "BAR");
}
private static class DestroyProcessAfter10Lines {
- private ProcessWrapperFactory.ProcessWrapper wrapper;
private final AtomicInteger lineCounter = new AtomicInteger();
+ private ProcessWrapperFactory.ProcessWrapper wrapper;
void process(String line) {
if (lineCounter.incrementAndGet() == 10) {
@@ -101,4 +129,11 @@ class ProcessWrapperFactoryTest {
}
}
+ private static class ThrowExceptionForEveryLine {
+
+ void process(String line) {
+ throw new IllegalStateException("Some error");
+ }
+ }
+
}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/DefaultIndexedFile.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/DefaultIndexedFile.java
index c170fccd935..686d37d8b5d 100644
--- a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/DefaultIndexedFile.java
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/DefaultIndexedFile.java
@@ -50,22 +50,24 @@ public class DefaultIndexedFile extends DefaultInputComponent implements Indexed
private final Path absolutePath;
private final SensorStrategy sensorStrategy;
private final String oldRelativeFilePath;
+ private final boolean hidden;
+ private final URI uri;
/**
* Testing purposes only!
*/
public DefaultIndexedFile(String projectKey, Path baseDir, String relativePath, @Nullable String language) {
this(baseDir.resolve(relativePath), projectKey, relativePath, relativePath, Type.MAIN, language, intGenerator.getAndIncrement(),
- new SensorStrategy(), null);
+ new SensorStrategy(), null, false);
}
public DefaultIndexedFile(Path absolutePath, String projectKey, String projectRelativePath, String moduleRelativePath, Type type, @Nullable String language, int batchId,
- SensorStrategy sensorStrategy) {
- this(absolutePath, projectKey, projectRelativePath, moduleRelativePath, type, language, batchId, sensorStrategy, null);
+ SensorStrategy sensorStrategy, boolean hidden) {
+ this(absolutePath, projectKey, projectRelativePath, moduleRelativePath, type, language, batchId, sensorStrategy, null, hidden);
}
public DefaultIndexedFile(Path absolutePath, String projectKey, String projectRelativePath, String moduleRelativePath, Type type, @Nullable String language, int batchId,
- SensorStrategy sensorStrategy, @Nullable String oldRelativeFilePath) {
+ SensorStrategy sensorStrategy, @Nullable String oldRelativeFilePath, boolean hidden) {
super(batchId);
this.projectKey = projectKey;
this.projectRelativePath = checkSanitize(projectRelativePath);
@@ -74,13 +76,15 @@ public class DefaultIndexedFile extends DefaultInputComponent implements Indexed
this.language = language;
this.sensorStrategy = sensorStrategy;
this.absolutePath = absolutePath;
+ this.uri = absolutePath.toUri();
this.oldRelativeFilePath = oldRelativeFilePath;
+ this.hidden = hidden;
validateKeyLength();
}
static String checkSanitize(String relativePath) {
String sanitized = PathUtils.sanitize(relativePath);
- if(sanitized == null) {
+ if (sanitized == null) {
throw new IllegalArgumentException(String.format("The path '%s' must sanitize to a non-null value", relativePath));
}
return sanitized;
@@ -185,7 +189,12 @@ public class DefaultIndexedFile extends DefaultInputComponent implements Indexed
}
@Override
+ public boolean isHidden() {
+ return hidden;
+ }
+
+ @Override
public URI uri() {
- return path().toUri();
+ return uri;
}
}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/DefaultInputFile.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/DefaultInputFile.java
index 79917fcf37c..e1d0cbbb0fd 100644
--- a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/DefaultInputFile.java
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/DefaultInputFile.java
@@ -73,7 +73,6 @@ public class DefaultInputFile extends DefaultInputComponent implements InputFile
private BitSet executableLines;
private boolean markedAsUnchanged;
-
public DefaultInputFile(DefaultIndexedFile indexedFile, Consumer<DefaultInputFile> metadataGenerator, Consumer<DefaultInputFile> scmStatusGenerator) {
this(indexedFile, metadataGenerator, null, scmStatusGenerator);
}
@@ -99,7 +98,7 @@ public class DefaultInputFile extends DefaultInputComponent implements InputFile
}
private void checkScmStatus() {
- if(status == null) {
+ if (status == null) {
scmStatusGenerator.accept(this);
}
}
@@ -108,7 +107,7 @@ public class DefaultInputFile extends DefaultInputComponent implements InputFile
public InputStream inputStream() throws IOException {
return contents != null ? new ByteArrayInputStream(contents.getBytes(charset()))
: new BOMInputStream(Files.newInputStream(path()),
- ByteOrderMark.UTF_8, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE, ByteOrderMark.UTF_32LE, ByteOrderMark.UTF_32BE);
+ ByteOrderMark.UTF_8, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE, ByteOrderMark.UTF_32LE, ByteOrderMark.UTF_32BE);
}
public boolean isMarkedAsUnchanged() {
@@ -212,6 +211,11 @@ public class DefaultInputFile extends DefaultInputComponent implements InputFile
return indexedFile.type();
}
+ @Override
+ public boolean isHidden() {
+ return indexedFile.isHidden();
+ }
+
/**
* Component key (without branch).
*/
@@ -236,7 +240,7 @@ public class DefaultInputFile extends DefaultInputComponent implements InputFile
@Override
public Status status() {
checkScmStatus();
- if(status == null) {
+ if (status == null) {
// scm might not be available, fallback to using hashes in the metadata
checkMetadata();
}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/TestInputFileBuilder.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/TestInputFileBuilder.java
index d2f303e1a01..de7a194d9ff 100644
--- a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/TestInputFileBuilder.java
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/TestInputFileBuilder.java
@@ -74,6 +74,7 @@ public class TestInputFileBuilder {
private int lastValidOffset = -1;
private boolean publish = true;
private String contents;
+ private boolean hidden = false;
/**
* Create a InputFile identified by the given project key and relative path.
@@ -206,6 +207,11 @@ public class TestInputFileBuilder {
return this;
}
+ public TestInputFileBuilder setHidden(boolean isHiddenFile) {
+ this.hidden = isHiddenFile;
+ return this;
+ }
+
public TestInputFileBuilder setMetadata(Metadata metadata) {
this.setLines(metadata.lines());
this.setLastValidOffset(metadata.lastValidOffset());
@@ -227,7 +233,8 @@ public class TestInputFileBuilder {
projectBaseDir = moduleBaseDir;
}
String projectRelativePath = projectBaseDir.relativize(absolutePath).toString();
- DefaultIndexedFile indexedFile = new DefaultIndexedFile(absolutePath, projectKey, projectRelativePath, relativePath, type, language, id, new SensorStrategy(), oldRelativePath);
+ DefaultIndexedFile indexedFile = new DefaultIndexedFile(absolutePath, projectKey, projectRelativePath, relativePath, type, language, id, new SensorStrategy(), oldRelativePath,
+ hidden);
DefaultInputFile inputFile = new DefaultInputFile(indexedFile,
f -> f.setMetadata(new Metadata(lines, nonBlankLines, hash, originalLineStartOffsets, originalLineEndOffsets, lastValidOffset)),
contents, f -> {});
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/AbsolutePathPredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/AbsolutePathPredicate.java
index ffa4aa448a7..a0cdddf697a 100644
--- a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/AbsolutePathPredicate.java
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/AbsolutePathPredicate.java
@@ -21,8 +21,10 @@ package org.sonar.api.batch.fs.internal.predicates;
import java.io.File;
import java.nio.file.Path;
-import java.util.Arrays;
import java.util.Collections;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.sonar.api.batch.fs.FileSystem.Index;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.scan.filesystem.PathResolver;
@@ -33,27 +35,36 @@ import org.sonar.api.utils.PathUtils;
*/
class AbsolutePathPredicate extends AbstractFilePredicate {
+ private static final Logger LOG = LoggerFactory.getLogger(AbsolutePathPredicate.class);
+
private final String path;
private final Path baseDir;
+ private final String sanitizedPath;
AbsolutePathPredicate(String path, Path baseDir) {
this.baseDir = baseDir;
- this.path = PathUtils.sanitize(path);
+ this.path = path;
+ this.sanitizedPath = PathUtils.sanitize(path);
}
@Override
public boolean apply(InputFile f) {
- return path.equals(f.absolutePath());
+ return sanitizedPath.equals(f.absolutePath());
}
@Override
public Iterable<InputFile> get(Index index) {
- String relative = PathUtils.sanitize(new PathResolver().relativePath(baseDir.toFile(), new File(path)));
+ if (sanitizedPath == null) {
+ LOG.debug("Cannot resolve absolute path '{}' as it is not a valid path", path);
+ return Collections.emptyList();
+ }
+
+ String relative = PathUtils.sanitize(new PathResolver().relativePath(baseDir.toFile(), new File(sanitizedPath)));
if (relative == null) {
return Collections.emptyList();
}
InputFile f = index.inputFile(relative);
- return f != null ? Arrays.asList(f) : Collections.emptyList();
+ return f != null ? List.of(f) : Collections.emptyList();
}
@Override
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/ChangedFilePredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/ChangedFilePredicate.java
index 7e981e82f77..c3c38969ce4 100644
--- a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/ChangedFilePredicate.java
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/ChangedFilePredicate.java
@@ -24,15 +24,9 @@ import org.sonar.api.batch.fs.InputFile;
public class ChangedFilePredicate implements FilePredicate {
- private final FilePredicate originalPredicate;
-
- public ChangedFilePredicate(FilePredicate originalPredicate) {
- this.originalPredicate = originalPredicate;
- }
-
@Override
public boolean apply(InputFile inputFile) {
- return originalPredicate.apply(inputFile) && InputFile.Status.SAME != inputFile.status();
+ return InputFile.Status.SAME != inputFile.status();
}
}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/DefaultFilePredicates.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/DefaultFilePredicates.java
index c0c2b734cce..34dd0c69ca7 100644
--- a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/DefaultFilePredicates.java
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/DefaultFilePredicates.java
@@ -204,11 +204,11 @@ public class DefaultFilePredicates implements FilePredicates {
@Override
public FilePredicate hasStatus(Status status) {
- return new StatusPredicate(status);
+ return inputFile -> status == inputFile.status();
}
@Override
public FilePredicate hasAnyStatus() {
- return new StatusPredicate(null);
+ return TruePredicate.TRUE;
}
}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/FileExtensionPredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/FileExtensionPredicate.java
index 29e387b97b2..91ca9258621 100644
--- a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/FileExtensionPredicate.java
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/FileExtensionPredicate.java
@@ -59,4 +59,9 @@ public class FileExtensionPredicate extends AbstractFilePredicate {
private static String lowercase(String extension) {
return extension.toLowerCase(Locale.ENGLISH);
}
+
+ @Override
+ public int priority() {
+ return USE_INDEX;
+ }
}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/FilenamePredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/FilenamePredicate.java
index 39856a3583e..a32f3c88cb9 100644
--- a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/FilenamePredicate.java
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/FilenamePredicate.java
@@ -42,4 +42,8 @@ public class FilenamePredicate extends AbstractFilePredicate {
return index.getFilesByName(filename);
}
+ @Override
+ public int priority() {
+ return USE_INDEX;
+ }
}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/StatusPredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/NonHiddenFilesPredicate.java
index bdaa6ef20c7..4b467e6dba5 100644
--- a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/StatusPredicate.java
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/predicates/NonHiddenFilesPredicate.java
@@ -19,24 +19,14 @@
*/
package org.sonar.api.batch.fs.internal.predicates;
-import javax.annotation.Nullable;
+import org.sonar.api.batch.fs.FilePredicate;
import org.sonar.api.batch.fs.InputFile;
-/**
- * @deprecated since 7.8
- */
-@Deprecated
-public class StatusPredicate extends AbstractFilePredicate {
-
- private final InputFile.Status status;
-
- StatusPredicate(@Nullable InputFile.Status status) {
- this.status = status;
- }
+public class NonHiddenFilesPredicate implements FilePredicate {
@Override
- public boolean apply(InputFile f) {
- return status == null || status == f.status();
+ public boolean apply(InputFile inputFile) {
+ return !inputFile.isHidden();
}
}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/DefaultSensorDescriptor.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/DefaultSensorDescriptor.java
index de03ccc6d83..b4cf659ac06 100644
--- a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/DefaultSensorDescriptor.java
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/DefaultSensorDescriptor.java
@@ -27,18 +27,16 @@ import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nullable;
-
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.sensor.SensorDescriptor;
import org.sonar.api.config.Configuration;
public class DefaultSensorDescriptor implements SensorDescriptor {
public static final Set<String> HARDCODED_INDEPENDENT_FILE_SENSORS = Collections.unmodifiableSet(Stream.of(
- "CSS Metrics",
- "CSS Rules",
- "HTML",
- "XML Sensor"
- ).collect(Collectors.toSet()));
+ "CSS Metrics",
+ "CSS Rules",
+ "HTML",
+ "XML Sensor").collect(Collectors.toSet()));
private String name;
private String[] languages = new String[0];
@@ -47,6 +45,7 @@ public class DefaultSensorDescriptor implements SensorDescriptor {
private boolean global = false;
private Predicate<Configuration> configurationPredicate;
private boolean processesFilesIndependently = false;
+ private boolean processesHiddenFiles = false;
public String name() {
return name;
@@ -77,6 +76,10 @@ public class DefaultSensorDescriptor implements SensorDescriptor {
return processesFilesIndependently;
}
+ public boolean isProcessesHiddenFiles() {
+ return processesHiddenFiles;
+ }
+
@Override
public DefaultSensorDescriptor name(String name) {
// TODO: Remove this hardcoded list once all plugins will implement the new API "processFilesIndependently"
@@ -132,4 +135,10 @@ public class DefaultSensorDescriptor implements SensorDescriptor {
this.processesFilesIndependently = true;
return this;
}
+
+ @Override
+ public SensorDescriptor processesHiddenFiles() {
+ this.processesHiddenFiles = true;
+ return this;
+ }
}
diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/DefaultIndexedFileTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/DefaultIndexedFileTest.java
index 1c52a520e54..ecc98c43d9c 100644
--- a/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/DefaultIndexedFileTest.java
+++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/DefaultIndexedFileTest.java
@@ -43,4 +43,14 @@ public class DefaultIndexedFileTest {
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining(invalidPath);
}
+
+ @Test
+ public void uri_should_be_cached() {
+ String projectKey = "12345";
+ Path baseDir = Paths.get("");
+ String path = "foo/bar";
+
+ DefaultIndexedFile file = new DefaultIndexedFile(projectKey, baseDir, path, null);
+ Assertions.assertThat(file.uri()).isSameAs(file.uri());
+ }
}
diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/fs/DefaultInputFileTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/fs/DefaultInputFileTest.java
index c6d21de0ede..51a34b2087e 100644
--- a/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/fs/DefaultInputFileTest.java
+++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/fs/DefaultInputFileTest.java
@@ -73,7 +73,7 @@ public class DefaultInputFileTest {
baseDir = temp.newFolder().toPath();
sensorStrategy = new SensorStrategy();
indexedFile = new DefaultIndexedFile(baseDir.resolve(PROJECT_RELATIVE_PATH), "ABCDE", PROJECT_RELATIVE_PATH, MODULE_RELATIVE_PATH, InputFile.Type.TEST, "php", 0,
- sensorStrategy);
+ sensorStrategy, false);
}
@Test
@@ -111,6 +111,7 @@ public class DefaultInputFileTest {
assertThat(new File(inputFile.absolutePath())).isAbsolute();
assertThat(inputFile.language()).isEqualTo("php");
assertThat(inputFile.status()).isEqualTo(InputFile.Status.ADDED);
+ assertThat(inputFile.isHidden()).isFalse();
assertThat(inputFile.type()).isEqualTo(InputFile.Type.TEST);
assertThat(inputFile.lines()).isEqualTo(42);
assertThat(inputFile.charset()).isEqualTo(StandardCharsets.ISO_8859_1);
@@ -130,7 +131,7 @@ public class DefaultInputFileTest {
public void test_moved_file() {
DefaultIndexedFile indexedFileForMovedFile = new DefaultIndexedFile(baseDir.resolve(PROJECT_RELATIVE_PATH), "ABCDE", PROJECT_RELATIVE_PATH, MODULE_RELATIVE_PATH,
InputFile.Type.TEST, "php", 0,
- sensorStrategy, OLD_RELATIVE_PATH);
+ sensorStrategy, OLD_RELATIVE_PATH, false);
Metadata metadata = new Metadata(42, 42, "", new int[0], new int[0], 10);
DefaultInputFile inputFile = new DefaultInputFile(indexedFileForMovedFile, f -> f.setMetadata(metadata), NO_OP)
.setStatus(InputFile.Status.ADDED)
@@ -255,7 +256,7 @@ public class DefaultInputFileTest {
Metadata metadata = new Metadata(2, 2, "", new int[] {0, 10}, new int[] {8, 15}, 16);
DefaultInputFile file = new DefaultInputFile(new DefaultIndexedFile("ABCDE", Paths.get("module"), MODULE_RELATIVE_PATH, null),
f -> f.setMetadata(metadata), f -> {
- });
+ });
assertThat(file.newPointer(0).line()).isOne();
assertThat(file.newPointer(0).lineOffset()).isZero();
@@ -366,4 +367,14 @@ public class DefaultInputFileTest {
private static final Consumer<DefaultInputFile> NO_OP = f -> {
};
+
+ @Test
+ public void test_hidden() {
+ DefaultIndexedFile hiddenIndexedFile = new DefaultIndexedFile(baseDir.resolve(PROJECT_RELATIVE_PATH), "ABCDE", PROJECT_RELATIVE_PATH, MODULE_RELATIVE_PATH, InputFile.Type.TEST,
+ "php", 0,
+ sensorStrategy, true);
+ DefaultInputFile inputFile = new DefaultInputFile(hiddenIndexedFile, NO_OP, NO_OP);
+
+ assertThat(inputFile.isHidden()).isTrue();
+ }
}
diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/fs/TestInputFileBuilderTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/fs/TestInputFileBuilderTest.java
index a211903be85..970456eb99f 100644
--- a/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/fs/TestInputFileBuilderTest.java
+++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/fs/TestInputFileBuilderTest.java
@@ -55,12 +55,14 @@ public class TestInputFileBuilderTest {
DefaultInputFile file = TestInputFileBuilder.create("module", new File("baseDir"), new File("baseDir", "path"))
.setStatus(Status.SAME)
.setType(Type.MAIN)
+ .setHidden(true)
.build();
assertThat(file.type()).isEqualTo(Type.MAIN);
assertThat(file.status()).isEqualTo(Status.SAME);
assertThat(file.isPublished()).isTrue();
assertThat(file.type()).isEqualTo(Type.MAIN);
+ assertThat(file.isHidden()).isTrue();
assertThat(file.relativePath()).isEqualTo("path");
assertThat(file.absolutePath()).isEqualTo("baseDir/path");
diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/predicates/AbsolutePathPredicateTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/predicates/AbsolutePathPredicateTest.java
new file mode 100644
index 00000000000..f32838fa477
--- /dev/null
+++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/predicates/AbsolutePathPredicateTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.api.batch.fs.internal.predicates;
+
+import java.nio.file.Path;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.io.TempDir;
+import org.sonar.api.batch.fs.FileSystem.Index;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.testfixtures.log.LogTesterJUnit5;
+
+import static java.lang.String.format;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.slf4j.event.Level.DEBUG;
+
+class AbsolutePathPredicateTest {
+
+ @RegisterExtension
+ public LogTesterJUnit5 logTester = new LogTesterJUnit5().setLevel(DEBUG);
+
+ @Test
+ void get_shouldReturnEmptyAndLogError_whenPathIsInvalid(@TempDir Path baseDir) {
+ String path = "../../some/path/file.txt";
+ AbsolutePathPredicate predicate = new AbsolutePathPredicate(path, baseDir);
+
+ Iterable<InputFile> inputFiles = predicate.get(mock(Index.class));
+
+ assertThat(inputFiles).isEmpty();
+ assertThat(logTester.logs(DEBUG)).contains(format("Cannot resolve absolute path '%s' as it is not a valid path", path));
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/predicates/ChangedFilePredicateTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/predicates/ChangedFilePredicateTest.java
index 33da3b1755a..33f085344d2 100644
--- a/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/predicates/ChangedFilePredicateTest.java
+++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/predicates/ChangedFilePredicateTest.java
@@ -21,74 +21,42 @@ package org.sonar.api.batch.fs.internal.predicates;
import org.assertj.core.api.Assertions;
import org.junit.Test;
-import org.sonar.api.batch.fs.FilePredicate;
import org.sonar.api.batch.fs.InputFile;
-import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ChangedFilePredicateTest {
- private final FilePredicate predicate = mock(FilePredicate.class);
private final InputFile inputFile = mock(InputFile.class);
- private final ChangedFilePredicate underTest = new ChangedFilePredicate(predicate);
+ private final ChangedFilePredicate underTest = new ChangedFilePredicate();
@Test
- public void apply_when_file_is_changed_and_predicate_is_true() {
+ public void apply_when_file_is_changed() {
when(inputFile.status()).thenReturn(InputFile.Status.CHANGED);
- when(predicate.apply(inputFile)).thenReturn(true);
Assertions.assertThat(underTest.apply(inputFile)).isTrue();
- verify(predicate, times(1)).apply(any());
- verify(inputFile, times(1)).status();
+ verify(inputFile).status();
}
@Test
- public void apply_when_file_is_added_and_predicate_is_true() {
+ public void apply_when_file_is_added() {
when(inputFile.status()).thenReturn(InputFile.Status.ADDED);
- when(predicate.apply(inputFile)).thenReturn(true);
Assertions.assertThat(underTest.apply(inputFile)).isTrue();
- verify(predicate, times(1)).apply(any());
- verify(inputFile, times(1)).status();
+ verify(inputFile).status();
}
@Test
- public void do_not_apply_when_file_is_same_and_predicate_is_true() {
+ public void do_not_apply_when_file_is_same() {
when(inputFile.status()).thenReturn(InputFile.Status.SAME);
- when(predicate.apply(inputFile)).thenReturn(true);
Assertions.assertThat(underTest.apply(inputFile)).isFalse();
- verify(predicate, times(1)).apply(any());
- verify(inputFile, times(1)).status();
+ verify(inputFile).status();
}
-
- @Test
- public void predicate_is_evaluated_before_file_status() {
- when(predicate.apply(inputFile)).thenReturn(false);
-
- Assertions.assertThat(underTest.apply(inputFile)).isFalse();
-
- verify(inputFile, never()).status();
- }
-
- @Test
- public void do_not_apply_when_file_is_same_and_predicate_is_false() {
- when(inputFile.status()).thenReturn(InputFile.Status.SAME);
- when(predicate.apply(inputFile)).thenReturn(true);
-
- Assertions.assertThat(underTest.apply(inputFile)).isFalse();
-
- verify(predicate, times(1)).apply(any());
- verify(inputFile, times(1)).status();
- }
-
}
diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/predicates/FileExtensionPredicateTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/predicates/FileExtensionPredicateTest.java
index 83ccb9da164..392d823e541 100644
--- a/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/predicates/FileExtensionPredicateTest.java
+++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/predicates/FileExtensionPredicateTest.java
@@ -19,7 +19,6 @@
*/
package org.sonar.api.batch.fs.internal.predicates;
-import java.io.IOException;
import org.junit.Test;
import org.sonar.api.batch.fs.InputFile;
@@ -31,14 +30,14 @@ import static org.sonar.api.batch.fs.internal.predicates.FileExtensionPredicate.
public class FileExtensionPredicateTest {
@Test
- public void should_match_correct_extension() throws IOException {
+ public void should_match_correct_extension() {
FileExtensionPredicate predicate = new FileExtensionPredicate("bat");
assertThat(predicate.apply(mockWithName("prog.bat"))).isTrue();
assertThat(predicate.apply(mockWithName("prog.bat.bat"))).isTrue();
}
@Test
- public void should_not_match_incorrect_extension() throws IOException {
+ public void should_not_match_incorrect_extension() {
FileExtensionPredicate predicate = new FileExtensionPredicate("bat");
assertThat(predicate.apply(mockWithName("prog.batt"))).isFalse();
assertThat(predicate.apply(mockWithName("prog.abat"))).isFalse();
@@ -49,7 +48,7 @@ public class FileExtensionPredicateTest {
}
@Test
- public void should_match_correct_extension_case_insensitively() throws IOException {
+ public void should_match_correct_extension_case_insensitively() {
FileExtensionPredicate predicate = new FileExtensionPredicate("jAVa");
assertThat(predicate.apply(mockWithName("Program.java"))).isTrue();
assertThat(predicate.apply(mockWithName("Program.JAVA"))).isTrue();
@@ -64,7 +63,12 @@ public class FileExtensionPredicateTest {
assertThat(getExtension(".")).isEmpty();
}
- private InputFile mockWithName(String filename) throws IOException {
+ @Test
+ public void should_have_use_index_priority() {
+ assertThat(new FileExtensionPredicate("bat").priority()).isEqualTo(AbstractFilePredicate.USE_INDEX);
+ }
+
+ private InputFile mockWithName(String filename) {
InputFile inputFile = mock(InputFile.class);
when(inputFile.filename()).thenReturn(filename);
return inputFile;
diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/predicates/FilenamePredicateTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/predicates/FilenamePredicateTest.java
index 2393d9cfa9e..30bc1e5f61e 100644
--- a/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/predicates/FilenamePredicateTest.java
+++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/predicates/FilenamePredicateTest.java
@@ -59,4 +59,12 @@ public class FilenamePredicateTest {
assertThat(new FilenamePredicate(filename).get(index)).containsOnly(inputFile);
}
+ @Test
+ public void should_have_use_index_priority() {
+ String filename = "some name";
+ InputFile inputFile = mock(InputFile.class);
+ when(inputFile.filename()).thenReturn(filename);
+
+ assertThat(new FilenamePredicate(filename).priority()).isEqualTo(AbstractFilePredicate.USE_INDEX);
+ }
}
diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/predicates/NonHiddenFilesPredicateTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/predicates/NonHiddenFilesPredicateTest.java
new file mode 100644
index 00000000000..a5a09a97567
--- /dev/null
+++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/fs/internal/predicates/NonHiddenFilesPredicateTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.api.batch.fs.internal.predicates;
+
+import org.assertj.core.api.Assertions;
+import org.junit.Test;
+import org.sonar.api.batch.fs.InputFile;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class NonHiddenFilesPredicateTest {
+
+ private final InputFile inputFile = mock(InputFile.class);
+
+ private final NonHiddenFilesPredicate underTest = new NonHiddenFilesPredicate();
+
+ @Test
+ public void apply_when_file_is_not_hidden() {
+ when(inputFile.isHidden()).thenReturn(false);
+
+ Assertions.assertThat(underTest.apply(inputFile)).isTrue();
+
+ verify(inputFile).isHidden();
+ }
+
+ @Test
+ public void do_not_apply_when_file_is_hidden() {
+ when(inputFile.isHidden()).thenReturn(true);
+
+ Assertions.assertThat(underTest.apply(inputFile)).isFalse();
+
+ verify(inputFile).isHidden();
+ }
+}
diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/sensor/internal/DefaultSensorDescriptorTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/sensor/internal/DefaultSensorDescriptorTest.java
index 07064a1f30f..1d35506deb9 100644
--- a/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/sensor/internal/DefaultSensorDescriptorTest.java
+++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/sensor/internal/DefaultSensorDescriptorTest.java
@@ -36,13 +36,14 @@ public class DefaultSensorDescriptorTest {
public void describe_defaults() {
DefaultSensorDescriptor descriptor = new DefaultSensorDescriptor();
descriptor
- .name("Foo");
+ .name("Foo");
assertThat(descriptor.name()).isEqualTo("Foo");
assertThat(descriptor.languages()).isEmpty();
assertThat(descriptor.type()).isNull();
assertThat(descriptor.ruleRepositories()).isEmpty();
assertThat(descriptor.isProcessesFilesIndependently()).isFalse();
+ assertThat(descriptor.isProcessesHiddenFiles()).isFalse();
}
@Test
@@ -54,7 +55,8 @@ public class DefaultSensorDescriptorTest {
.onlyOnFileType(InputFile.Type.MAIN)
.onlyWhenConfiguration(c -> c.hasKey("sonar.foo.reportPath2") && c.hasKey("sonar.foo.reportPath"))
.createIssuesForRuleRepository("java-java")
- .processesFilesIndependently();
+ .processesFilesIndependently()
+ .processesHiddenFiles();
assertThat(descriptor.name()).isEqualTo("Foo");
assertThat(descriptor.languages()).containsOnly("java");
@@ -66,6 +68,7 @@ public class DefaultSensorDescriptorTest {
assertThat(descriptor.configurationPredicate().test(settings.asConfig())).isTrue();
assertThat(descriptor.ruleRepositories()).containsOnly("java-java");
assertThat(descriptor.isProcessesFilesIndependently()).isTrue();
+ assertThat(descriptor.isProcessesHiddenFiles()).isTrue();
}
@Test
diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/analysisdata/AnalysisDataIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/analysisdata/AnalysisDataIT.java
index 7f56cb87cce..94ccedfa946 100644
--- a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/analysisdata/AnalysisDataIT.java
+++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/analysisdata/AnalysisDataIT.java
@@ -63,6 +63,6 @@ public class AnalysisDataIT {
// then
List<ScannerReport.AnalysisData> analysisData = result.analysisData();
assertThat(analysisData)
- .anyMatch(data -> data.getKey().equals("architecture.file_graph." + Xoo.KEY));
+ .anyMatch(data -> data.getKey().startsWith("architecture.graph."));
}
}
diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/bootstrap/BootstrapMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/bootstrap/BootstrapMediumIT.java
index 63317e362b9..6cfd6ea5f94 100644
--- a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/bootstrap/BootstrapMediumIT.java
+++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/bootstrap/BootstrapMediumIT.java
@@ -117,7 +117,7 @@ class BootstrapMediumIT {
void should_fail_if_invalid_json_input() {
var in = new ByteArrayInputStream("}".getBytes());
- var exitCode = ScannerMain.run(in);
+ var exitCode = ScannerMain.run(in, System.out);
assertThat(exitCode).isEqualTo(1);
assertThat(logTester.getLogs(Level.ERROR)).hasSize(1);
@@ -128,7 +128,7 @@ class BootstrapMediumIT {
@Test
void should_warn_if_null_property_key() {
ScannerMain.run(new ByteArrayInputStream("""
- {"scannerProperties": [{"value": "aValueWithoutKey"}]}""".getBytes()));
+ {"scannerProperties": [{"value": "aValueWithoutKey"}]}""".getBytes()), System.out);
assertThat(logTester.logs(Level.WARN)).contains("Ignoring property with null key. Value='aValueWithoutKey'");
}
@@ -136,7 +136,7 @@ class BootstrapMediumIT {
@Test
void should_warn_if_null_property_value() {
ScannerMain.run(new ByteArrayInputStream("""
- {"scannerProperties": [{"key": "aKey", "value": null}]}""".getBytes()));
+ {"scannerProperties": [{"key": "aKey", "value": null}]}""".getBytes()), System.out);
assertThat(logTester.logs(Level.WARN)).contains("Ignoring property with null value. Key='aKey'");
}
@@ -144,7 +144,7 @@ class BootstrapMediumIT {
@Test
void should_warn_if_not_provided_property_value() {
ScannerMain.run(new ByteArrayInputStream("""
- {"scannerProperties": [{"key": "aKey"}]}""".getBytes()));
+ {"scannerProperties": [{"key": "aKey"}]}""".getBytes()), System.out);
assertThat(logTester.logs(Level.WARN)).contains("Ignoring property with null value. Key='aKey'");
}
@@ -152,7 +152,7 @@ class BootstrapMediumIT {
@Test
void should_warn_if_duplicate_property_keys() {
ScannerMain.run(new ByteArrayInputStream("""
- {"scannerProperties": [{"key": "aKey", "value": "aValue"}, {"key": "aKey", "value": "aValue"}]}""".getBytes()));
+ {"scannerProperties": [{"key": "aKey", "value": "aValue"}, {"key": "aKey", "value": "aValue"}]}""".getBytes()), System.out);
assertThat(logTester.logs(Level.WARN)).contains("Duplicated properties. Key='aKey'");
}
@@ -160,7 +160,7 @@ class BootstrapMediumIT {
@Test
void should_warn_if_null_property() {
ScannerMain.run(new ByteArrayInputStream("""
- {"scannerProperties": [{"key": "aKey", "value": "aValue"},]}""".getBytes()));
+ {"scannerProperties": [{"key": "aKey", "value": "aValue"},]}""".getBytes()), System.out);
assertThat(logTester.logs(Level.WARN)).contains("Ignoring null or empty property");
}
@@ -168,7 +168,7 @@ class BootstrapMediumIT {
@Test
void should_warn_if_empty_property() {
ScannerMain.run(new ByteArrayInputStream("""
- {"scannerProperties": [{}]}""".getBytes()));
+ {"scannerProperties": [{}]}""".getBytes()), System.out);
assertThat(logTester.logs(Level.WARN)).contains("Ignoring null or empty property");
}
@@ -229,7 +229,7 @@ class BootstrapMediumIT {
}
private int runScannerEngine(ScannerProperties scannerProperties) {
- return ScannerMain.run(new ByteArrayInputStream(scannerProperties.toJson().getBytes()));
+ return ScannerMain.run(new ByteArrayInputStream(scannerProperties.toJson().getBytes()), System.out);
}
static class ScannerProperties {
diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/fs/FileSystemMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/fs/FileSystemMediumIT.java
index 0a1ecff943b..babeadf518e 100644
--- a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/fs/FileSystemMediumIT.java
+++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/fs/FileSystemMediumIT.java
@@ -28,14 +28,21 @@ import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
import java.util.Random;
+import java.util.Set;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.slf4j.event.Level;
import org.sonar.api.CoreProperties;
import org.sonar.api.SonarEdition;
@@ -49,6 +56,7 @@ import org.sonar.api.utils.System2;
import org.sonar.scanner.mediumtest.AnalysisResult;
import org.sonar.scanner.mediumtest.ScannerMediumTester;
import org.sonar.scanner.mediumtest.ScannerMediumTester.AnalysisBuilder;
+import org.sonar.scanner.protocol.output.ScannerReport;
import org.sonar.xoo.XooPlugin;
import org.sonar.xoo.global.DeprecatedGlobalSensor;
import org.sonar.xoo.global.GlobalProjectSensor;
@@ -59,7 +67,6 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.tuple;
import static org.assertj.core.api.AssertionsForClassTypes.catchThrowableOfType;
-import static org.junit.jupiter.api.Assumptions.assumeFalse;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.slf4j.event.Level.DEBUG;
@@ -150,7 +157,7 @@ class FileSystemMediumIT {
.build())
.execute();
- assertThat(logTester.logs()).contains("2 files indexed");
+ assertThat(logTester.logs()).anyMatch(log -> log.startsWith("2 files indexed (done) | time="));
assertThat(logTester.logs()).contains("'src/sample.xoo' generated metadata with charset 'UTF-8'");
assertThat(String.join("\n", logTester.logs())).doesNotContain("'src/sample.java' generated metadata");
}
@@ -173,7 +180,7 @@ class FileSystemMediumIT {
.build())
.execute();
- assertThat(logTester.logs()).contains("2 files indexed");
+ assertThat(logTester.logs()).anyMatch(log -> log.startsWith("2 files indexed (done) | time="));
assertThat(logTester.logs()).contains("'src/sample.xoo' generated metadata with charset 'UTF-8'");
assertThat(logTester.logs()).contains("'src/sample.java' generated metadata with charset 'UTF-8'");
}
@@ -200,7 +207,7 @@ class FileSystemMediumIT {
.execute();
assertThat(logTester.logs()).containsAnyOf("'src/main/sample.java' indexed with no language", "'src\\main\\sample.java' indexed with no language");
- assertThat(logTester.logs()).contains("3 files indexed");
+ assertThat(logTester.logs()).anyMatch(log -> log.startsWith("3 files indexed (done) | time="));
assertThat(logTester.logs()).contains("'src/main/sample.xoo' generated metadata with charset 'UTF-8'");
assertThat(logTester.logs()).doesNotContain("'src/main/sample.java' generated metadata", "'src\\main\\sample.java' generated metadata");
assertThat(logTester.logs()).doesNotContain("'src/test/sample.java' generated metadata", "'src\\test\\sample.java' generated metadata");
@@ -231,7 +238,7 @@ class FileSystemMediumIT {
.build())
.execute();
- assertThat(logTester.logs()).contains("1 file indexed");
+ assertThat(logTester.logs()).anyMatch(log -> log.startsWith("1 file indexed (done) | time="));
assertThat(logTester.logs()).contains("'src/sample.unknown' indexed with no language");
assertThat(logTester.logs()).contains("'src/sample.unknown' generated metadata with charset 'UTF-8'");
DefaultInputFile inputFile = (DefaultInputFile) result.inputFile("src/sample.unknown");
@@ -456,6 +463,7 @@ class FileSystemMediumIT {
}
@Test
+ @DisabledOnOs(OS.WINDOWS)
void analysisDoesNotFailOnBrokenSymlink() throws IOException {
prepareBrokenSymlinkTestScenario();
@@ -465,6 +473,7 @@ class FileSystemMediumIT {
}
@Test
+ @DisabledOnOs(OS.WINDOWS)
void analysisWarnsAndIgnoresBrokenSymlink() throws IOException {
Path link = prepareBrokenSymlinkTestScenario();
@@ -477,8 +486,8 @@ class FileSystemMediumIT {
}
@Test
+ @DisabledOnOs(OS.WINDOWS)
void analysisIgnoresSymbolicLinkWithTargetOutsideBaseDir() throws IOException {
- assumeFalse(SystemUtils.IS_OS_WINDOWS);
File srcDir = new File(baseDir, "src");
assertThat(srcDir.mkdir()).isTrue();
@@ -497,23 +506,34 @@ class FileSystemMediumIT {
}
@Test
+ @DisabledOnOs(OS.WINDOWS)
+ void analysisIgnoresSymbolicLinkWithRelativeTargetOutsideBaseDir() throws IOException {
+ File srcDir = new File(baseDir, "src");
+ assertThat(srcDir.mkdir()).isTrue();
+
+ File otherDir = createDirectory(temp.toPath().resolve("other_dir")).toFile();
+ writeFile(otherDir, "target_outside.xoo");
+
+ Path linkPath = srcDir.toPath().resolve("target_link");
+ Path link = Files.createSymbolicLink(linkPath, Paths.get("../../other_dir/target_outside.xoo"));
+
+ tester.newAnalysis().properties(builder.build()).execute();
+
+ String logMessage = String.format("File '%s' is ignored. It is a symbolic link targeting a file not located in project basedir.", link.toRealPath(LinkOption.NOFOLLOW_LINKS));
+ assertThat(logTester.logs(Level.WARN)).contains(logMessage);
+ }
+
+ @Test
+ @DisabledOnOs(OS.WINDOWS)
void analysisIgnoresSymbolicLinkWithTargetOutsideModule() throws IOException {
- assumeFalse(SystemUtils.IS_OS_WINDOWS);
- File baseDirModuleA = new File(baseDir, "module_a");
- File baseDirModuleB = new File(baseDir, "module_b");
- File srcDirA = new File(baseDirModuleA, "src");
- assertThat(srcDirA.mkdirs()).isTrue();
- File srcDirB = new File(baseDirModuleB, "src");
- assertThat(srcDirB.mkdirs()).isTrue();
+ File srcDirA = createModuleWithSubdirectory("module_a", "src");
+ File srcDirB = createModuleWithSubdirectory("module_b", "src");
File target = writeFile(srcDirA, "target.xoo", "Sample xoo\ncontent");
Path link = Paths.get(srcDirB.getPath(), "target_link.xoo");
Files.createSymbolicLink(link, target.toPath());
- builder = ImmutableMap.<String, String>builder()
- .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
- .put("sonar.projectKey", "com.foo.project")
- .put("sonar.modules", "module_a,module_b");
+ builder.put("sonar.modules", "module_a,module_b");
AnalysisResult result = tester.newAnalysis().properties(builder.build()).execute();
@@ -524,13 +544,49 @@ class FileSystemMediumIT {
}
@Test
+ @DisabledOnOs(OS.WINDOWS)
+ void analysisIgnoresSymbolicLinkWithRelativeTargetOutsideModule() throws IOException {
+ File srcA = createModuleWithSubdirectory("module_a", "src");
+ File srcB = createModuleWithSubdirectory("module_b", "src");
+
+ Path target = srcB.toPath().resolve("target.xoo");
+ FileUtils.write(target.toFile(), "Sample xoo\ncontent", StandardCharsets.UTF_8);
+ Path link = srcA.toPath().resolve("target_link");
+ Files.createSymbolicLink(link, Paths.get("../../module_b/src/target.xoo"));
+
+ builder.put("sonar.modules", "module_a,module_b");
+
+ AnalysisResult result = tester.newAnalysis().properties(builder.build()).execute();
+
+ String logMessage = String.format("File '%s' is ignored. It is a symbolic link targeting a file not located in module basedir.", link.toRealPath(LinkOption.NOFOLLOW_LINKS));
+ assertThat(logTester.logs(Level.INFO)).contains(logMessage);
+ InputFile fileA = result.inputFile("module_b/src/target.xoo");
+ assertThat(fileA).isNotNull();
+ }
+
+ @Test
+ @DisabledOnOs(OS.WINDOWS)
+ void analysisDoesNotIgnoreSymbolicLinkWithRelativePath() throws IOException {
+ File src = createModuleWithSubdirectory("module_a", "src");
+ Path target = src.toPath().resolve("target.xoo");
+ FileUtils.write(target.toFile(), "Sample xoo\ncontent", StandardCharsets.UTF_8);
+ Path link = src.toPath().resolve("target_link");
+ Files.createSymbolicLink(link, Paths.get("target.xoo"));
+
+ builder.put("sonar.modules", "module_a");
+
+ AnalysisResult result = tester.newAnalysis().properties(builder.build()).execute();
+
+ InputFile targetFile = result.inputFile("module_a/src/target.xoo");
+ assertThat(targetFile).isNotNull();
+ String logMessage = String.format("File '%s' is ignored. It is a symbolic link targeting a file that does not exist.", link.toRealPath(LinkOption.NOFOLLOW_LINKS));
+ assertThat(logTester.logs(Level.WARN)).doesNotContain(logMessage);
+ }
+
+ @Test
void test_inclusions_on_multi_modules() throws IOException {
- File baseDirModuleA = new File(baseDir, "moduleA");
- File baseDirModuleB = new File(baseDir, "moduleB");
- File srcDirA = new File(baseDirModuleA, "tests");
- assertThat(srcDirA.mkdirs()).isTrue();
- File srcDirB = new File(baseDirModuleB, "tests");
- assertThat(srcDirB.mkdirs()).isTrue();
+ File srcDirA = createModuleWithSubdirectory("moduleA", "tests");
+ File srcDirB = createModuleWithSubdirectory("moduleB", "tests");
writeFile(srcDirA, "sampleTestA.xoo", "Sample xoo\ncontent");
writeFile(srcDirB, "sampleTestB.xoo", "Sample xoo\ncontent");
@@ -571,12 +627,8 @@ class FileSystemMediumIT {
@Test
void test_module_level_inclusions_override_parent_on_multi_modules() throws IOException {
- File baseDirModuleA = new File(baseDir, "moduleA");
- File baseDirModuleB = new File(baseDir, "moduleB");
- File srcDirA = new File(baseDirModuleA, "src");
- assertThat(srcDirA.mkdirs()).isTrue();
- File srcDirB = new File(baseDirModuleB, "src");
- assertThat(srcDirB.mkdirs()).isTrue();
+ File srcDirA = createModuleWithSubdirectory("moduleA", "src");
+ File srcDirB = createModuleWithSubdirectory("moduleB", "src");
writeFile(srcDirA, "sampleA.xoo", "Sample xoo\ncontent");
writeFile(srcDirB, "sampleB.xoo", "Sample xoo\ncontent");
@@ -608,12 +660,8 @@ class FileSystemMediumIT {
@Test
void warn_user_for_outdated_scanner_side_inherited_exclusions_for_multi_module_project() throws IOException {
- File baseDirModuleA = new File(baseDir, "moduleA");
- File baseDirModuleB = new File(baseDir, "moduleB");
- File srcDirA = new File(baseDirModuleA, "src");
- assertThat(srcDirA.mkdirs()).isTrue();
- File srcDirB = new File(baseDirModuleB, "src");
- assertThat(srcDirB.mkdirs()).isTrue();
+ File srcDirA = createModuleWithSubdirectory("moduleA", "src");
+ File srcDirB = createModuleWithSubdirectory("moduleB", "src");
writeFile(srcDirA, "sample.xoo", "Sample xoo\ncontent");
writeFile(srcDirB, "sample.xoo", "Sample xoo\ncontent");
@@ -641,12 +689,8 @@ class FileSystemMediumIT {
@Test
void support_global_server_side_exclusions_for_multi_module_project() throws IOException {
- File baseDirModuleA = new File(baseDir, "moduleA");
- File baseDirModuleB = new File(baseDir, "moduleB");
- File srcDirA = new File(baseDirModuleA, "src");
- assertThat(srcDirA.mkdirs()).isTrue();
- File srcDirB = new File(baseDirModuleB, "src");
- assertThat(srcDirB.mkdirs()).isTrue();
+ File srcDirA = createModuleWithSubdirectory("moduleA", "src");
+ File srcDirB = createModuleWithSubdirectory("moduleB", "src");
writeFile(srcDirA, "sample.xoo", "Sample xoo\ncontent");
writeFile(srcDirB, "sample.xoo", "Sample xoo\ncontent");
@@ -671,12 +715,8 @@ class FileSystemMediumIT {
@Test
void support_global_server_side_global_exclusions_for_multi_module_project() throws IOException {
- File baseDirModuleA = new File(baseDir, "moduleA");
- File baseDirModuleB = new File(baseDir, "moduleB");
- File srcDirA = new File(baseDirModuleA, "src");
- assertThat(srcDirA.mkdirs()).isTrue();
- File srcDirB = new File(baseDirModuleB, "src");
- assertThat(srcDirB.mkdirs()).isTrue();
+ File srcDirA = createModuleWithSubdirectory("moduleA", "src");
+ File srcDirB = createModuleWithSubdirectory("moduleB", "src");
writeFile(srcDirA, "sample.xoo", "Sample xoo\ncontent");
writeFile(srcDirB, "sample.xoo", "Sample xoo\ncontent");
@@ -701,12 +741,8 @@ class FileSystemMediumIT {
@Test
void warn_user_for_outdated_server_side_inherited_exclusions_for_multi_module_project() throws IOException {
- File baseDirModuleA = new File(baseDir, "moduleA");
- File baseDirModuleB = new File(baseDir, "moduleB");
- File srcDirA = new File(baseDirModuleA, "src");
- assertThat(srcDirA.mkdirs()).isTrue();
- File srcDirB = new File(baseDirModuleB, "src");
- assertThat(srcDirB.mkdirs()).isTrue();
+ File srcDirA = createModuleWithSubdirectory("moduleA", "src");
+ File srcDirB = createModuleWithSubdirectory("moduleB", "src");
writeFile(srcDirA, "sample.xoo", "Sample xoo\ncontent");
writeFile(srcDirB, "sample.xoo", "Sample xoo\ncontent");
@@ -979,12 +1015,8 @@ class FileSystemMediumIT {
@Test
void log_all_exclusions_properties_per_modules() throws IOException {
- File baseDirModuleA = new File(baseDir, "moduleA");
- File baseDirModuleB = new File(baseDir, "moduleB");
- File srcDirA = new File(baseDirModuleA, "src");
- assertThat(srcDirA.mkdirs()).isTrue();
- File srcDirB = new File(baseDirModuleB, "src");
- assertThat(srcDirB.mkdirs()).isTrue();
+ File srcDirA = createModuleWithSubdirectory("moduleA", "src");
+ File srcDirB = createModuleWithSubdirectory("moduleB", "src");
writeFile(srcDirA, "sample.xoo", "Sample xoo\ncontent");
writeFile(srcDirB, "sample.xoo", "Sample xoo\ncontent");
@@ -1013,7 +1045,7 @@ class FileSystemMediumIT {
" Excluded sources for coverage: **/coverage.exclusions",
" Excluded sources for duplication: **/cpd.exclusions",
"Indexing files of module 'moduleA'",
- " Base dir: " + baseDirModuleA.toPath().toRealPath(LinkOption.NOFOLLOW_LINKS),
+ " Base dir: " + srcDirA.toPath().getParent().toRealPath(LinkOption.NOFOLLOW_LINKS),
" Included sources: **/global.inclusions",
" Excluded sources: **/global.exclusions, **/global.test.inclusions",
" Included tests: **/global.test.inclusions",
@@ -1021,7 +1053,7 @@ class FileSystemMediumIT {
" Excluded sources for coverage: **/coverage.exclusions",
" Excluded sources for duplication: **/cpd.exclusions",
"Indexing files of module 'moduleB'",
- " Base dir: " + baseDirModuleB.toPath().toRealPath(LinkOption.NOFOLLOW_LINKS),
+ " Base dir: " + srcDirB.toPath().getParent().toRealPath(LinkOption.NOFOLLOW_LINKS),
" Included sources: **/global.inclusions",
" Excluded sources: **/global.exclusions, **/global.test.inclusions",
" Included tests: **/global.test.inclusions",
@@ -1147,7 +1179,7 @@ class FileSystemMediumIT {
.build())
.execute();
- assertThat(logTester.logs()).contains("1 file indexed");
+ assertThat(logTester.logs()).anyMatch(log -> log.startsWith("1 file indexed (done) | time="));
assertThat(result.inputFile("sample.xoo")).isNotNull();
}
@@ -1314,6 +1346,200 @@ class FileSystemMediumIT {
.hasMessageEndingWith("Failed to preprocess files");
}
+ @ParameterizedTest
+ @ValueSource(booleans = {
+ true,
+ false
+ })
+ void shouldScanAndAnalyzeAllHiddenFiles(boolean setHiddenFileScanningExplicitly) throws IOException {
+ prepareHiddenFileProject();
+ File projectDir = new File("test-resources/mediumtest/xoo/sample-with-hidden-files");
+ AnalysisBuilder analysisBuilder = tester
+ .addRules(new XooRulesDefinition())
+ .addActiveRule("xoo", "OneIssuePerFile", null, "Issue Per File", "MAJOR", null, "xoo")
+ .newAnalysis(new File(projectDir, "sonar-project.properties"))
+ .property("sonar.exclusions", "**/*.ignore")
+ .property("sonar.oneIssuePerFile.enableHiddenFileProcessing", "true");
+
+ if (setHiddenFileScanningExplicitly) {
+ // default is assumed to be false, here we set it explicitly
+ analysisBuilder.property("sonar.scanner.excludeHiddenFiles", "false");
+ }
+
+ AnalysisResult result = analysisBuilder.execute();
+
+ for (Map.Entry<String, Boolean> pathToHiddenStatus : hiddenFileProjectExpectedHiddenStatus().entrySet()) {
+ String filePath = pathToHiddenStatus.getKey();
+ boolean expectedIsHidden = pathToHiddenStatus.getValue();
+ assertHiddenFileScan(result, filePath, expectedIsHidden, true);
+ // we expect the sensor to process all files, regardless of visibility
+ assertFileIssue(result, filePath, true);
+ }
+ assertThat(result.inputFiles()).hasSize(10);
+ }
+
+ @Test
+ void shouldScanAllFilesAndOnlyAnalyzeNonHiddenFiles() throws IOException {
+ prepareHiddenFileProject();
+ File projectDir = new File("test-resources/mediumtest/xoo/sample-with-hidden-files");
+ AnalysisResult result = tester
+ .addRules(new XooRulesDefinition())
+ .addActiveRule("xoo", "OneIssuePerFile", null, "Issue Per File", "MAJOR", null, "xoo")
+ .newAnalysis(new File(projectDir, "sonar-project.properties"))
+ .property("sonar.exclusions", "**/*.ignore")
+ .property("sonar.oneIssuePerFile.enableHiddenFileProcessing", "false")
+ .execute();
+
+ for (Map.Entry<String, Boolean> pathToHiddenStatus : hiddenFileProjectExpectedHiddenStatus().entrySet()) {
+ String filePath = pathToHiddenStatus.getKey();
+ boolean expectedHiddenStatus = pathToHiddenStatus.getValue();
+ assertHiddenFileScan(result, filePath, expectedHiddenStatus, true);
+ // sensor should not process hidden files, we only expect issues on non-hidden files
+ assertFileIssue(result, filePath, !expectedHiddenStatus);
+ }
+ assertThat(result.inputFiles()).hasSize(10);
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {
+ true,
+ false
+ })
+ void shouldNotScanAndAnalyzeHiddenFilesWhenHiddenFileScanningIsDisabled(boolean sensorHiddenFileProcessingEnabled) throws IOException {
+ prepareHiddenFileProject();
+ File projectDir = new File("test-resources/mediumtest/xoo/sample-with-hidden-files");
+ AnalysisResult result = tester
+ .addRules(new XooRulesDefinition())
+ .addActiveRule("xoo", "OneIssuePerFile", null, "Issue Per File", "MAJOR", null, "xoo")
+ .newAnalysis(new File(projectDir, "sonar-project.properties"))
+ .property("sonar.exclusions", "**/*.ignore")
+ .property("sonar.scanner.excludeHiddenFiles", "true")
+ // hidden files are not scanned, so issues can't be raised on them regardless if the sensor wants to process them
+ .property("sonar.oneIssuePerFile.enableHiddenFileProcessing", String.valueOf(sensorHiddenFileProcessingEnabled))
+ .execute();
+
+ for (Map.Entry<String, Boolean> pathToHiddenStatus : hiddenFileProjectExpectedHiddenStatus().entrySet()) {
+ String filePath = pathToHiddenStatus.getKey();
+ boolean expectedHiddenStatus = pathToHiddenStatus.getValue();
+ assertHiddenFileScan(result, filePath, expectedHiddenStatus, false);
+ if (!expectedHiddenStatus) {
+ assertFileIssue(result, filePath, true);
+ }
+ }
+ assertThat(result.inputFiles()).hasSize(1);
+ }
+
+ @Test
+ void hiddenFilesAssignedToALanguageShouldNotBePublishedByDefault() throws IOException {
+ tester
+ .addRules(new XooRulesDefinition());
+
+ File srcDir = new File(baseDir, "src");
+ assertThat(srcDir.mkdir()).isTrue();
+
+ File hiddenFile = writeFile(srcDir, ".xoo", "Sample xoo\ncontent");
+ setFileAsHiddenOnWindows(hiddenFile.toPath());
+ File hiddenFileWithoutLanguage = writeFile(srcDir, ".bar", "Sample bar\ncontent");
+ setFileAsHiddenOnWindows(hiddenFileWithoutLanguage.toPath());
+ writeFile(srcDir, "file.xoo", "Sample xoo\ncontent");
+
+ AnalysisResult result = tester.newAnalysis()
+ .properties(builder
+ .put("sonar.sources", "src")
+ .build())
+ .execute();
+
+ DefaultInputFile hiddenInputFile = (DefaultInputFile) result.inputFile("src/.xoo");
+
+ assertThat(hiddenInputFile).isNotNull();
+ assertThat(hiddenInputFile.isPublished()).isFalse();
+ assertThatThrownBy(() -> result.getReportComponent(hiddenInputFile))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("Unable to find report for component");
+
+ DefaultInputFile hiddenInputFileWithoutLanguage = (DefaultInputFile) result.inputFile("src/.bar");
+ assertThat(hiddenInputFileWithoutLanguage).isNotNull();
+ assertThat(hiddenInputFileWithoutLanguage.isPublished()).isFalse();
+ assertThatThrownBy(() -> result.getReportComponent(hiddenInputFileWithoutLanguage))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("Unable to find report for component");
+
+ DefaultInputFile visibleInputFile = (DefaultInputFile) result.inputFile("src/file.xoo");
+ assertThat(visibleInputFile).isNotNull();
+ assertThat(visibleInputFile.isPublished()).isTrue();
+ assertThat(result.getReportComponent(visibleInputFile)).isNotNull();
+ }
+
+ @Test
+ void shouldDetectHiddenFilesFromMultipleModules() throws IOException {
+ File srcDirA = createModuleWithSubdirectory("moduleA", "src");
+ File srcDirB = createModuleWithSubdirectory("moduleB", "src");
+
+ File fileModuleA = writeFile(srcDirA, ".xoo", "Sample xoo\ncontent");
+ setFileAsHiddenOnWindows(fileModuleA.toPath());
+ File fileModuleB = writeFile(srcDirB, ".xoo", "Sample xoo\ncontent");
+ setFileAsHiddenOnWindows(fileModuleB.toPath());
+
+ AnalysisResult result = tester.newAnalysis()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.sources", "src")
+ .put("sonar.modules", "moduleA,moduleB")
+ .build())
+ .execute();
+
+ assertHiddenFileScan(result, "moduleA/src/.xoo", true, true);
+ assertHiddenFileScan(result, "moduleB/src/.xoo", true, true);
+ }
+
+ @Test
+ void shouldScanAndAnalyzeAllHiddenFilesWithRespectToExclusions() throws IOException {
+ prepareHiddenFileProject();
+ File projectDir = new File("test-resources/mediumtest/xoo/sample-with-hidden-files");
+
+
+ AnalysisResult result = tester
+ .addRules(new XooRulesDefinition())
+ .addActiveRule("xoo", "OneIssuePerFile", null, "Issue Per File", "MAJOR", null, "xoo")
+ .newAnalysis(new File(projectDir, "sonar-project.properties"))
+ .property("sonar.scm.provider", "xoo")
+ .property("sonar.oneIssuePerFile.enableHiddenFileProcessing", "true")
+ .property("sonar.exclusions", "**/.nestedHidden/**,**/*.ignore")
+ .execute();
+
+ Set<String> excludedFiles = Set.of(
+ // sonar.exclusions
+ "xources/.hidden/.nestedHidden/.xoo",
+ "xources/.hidden/.nestedHidden/Class.xoo",
+ "xources/.hidden/.nestedHidden/visibleInHiddenFolder/.xoo",
+ "xources/.hidden/.nestedHidden/visibleInHiddenFolder/.xoo.ignore",
+ "xources/.hidden/.nestedHidden/visibleInHiddenFolder/Class.xoo",
+ // scm ignore
+ "xources/nonHidden/.hiddenInVisibleFolder/.xoo");
+
+ for (Map.Entry<String, Boolean> pathToHiddenStatus : hiddenFileProjectExpectedHiddenStatus().entrySet()) {
+ String filePath = pathToHiddenStatus.getKey();
+ boolean expectedIsHidden = pathToHiddenStatus.getValue();
+
+ if (excludedFiles.contains(filePath)) {
+ assertThat(result.inputFile(filePath)).isNull();
+ } else {
+ assertHiddenFileScan(result, filePath, expectedIsHidden, true);
+ // we expect the sensor to process all non-excluded files, regardless of visibility
+ assertFileIssue(result, filePath, true);
+ }
+ }
+ assertThat(result.inputFiles()).hasSize(5);
+ }
+
+ private File createModuleWithSubdirectory(String moduleName, String subDirName) {
+ File moduleBaseDir = new File(baseDir, moduleName);
+ File srcDir = moduleBaseDir.toPath().resolve(subDirName).toFile();
+ assertThat(srcDir.mkdirs()).isTrue();
+ return srcDir;
+ }
+
private static void assertAnalysedFiles(AnalysisResult result, String... files) {
assertThat(result.inputFiles().stream().map(InputFile::toString).toList()).contains(files);
}
@@ -1341,7 +1567,6 @@ class FileSystemMediumIT {
}
private Path prepareBrokenSymlinkTestScenario() throws IOException {
- assumeFalse(SystemUtils.IS_OS_WINDOWS);
File srcDir = new File(baseDir, "src");
assertThat(srcDir.mkdir()).isTrue();
@@ -1353,4 +1578,69 @@ class FileSystemMediumIT {
return link;
}
+ private Map<String, Boolean> hiddenFileProjectExpectedHiddenStatus() {
+ return Map.of(
+ "xources/.hidden/.xoo", true,
+ "xources/.hidden/Class.xoo", true,
+ "xources/.hidden/.nestedHidden/.xoo", true,
+ "xources/.hidden/.nestedHidden/Class.xoo", true,
+ "xources/.hidden/.nestedHidden/visibleInHiddenFolder/.xoo", true,
+ "xources/.hidden/.nestedHidden/visibleInHiddenFolder/Class.xoo", true,
+ "xources/nonHidden/.xoo", true,
+ "xources/nonHidden/Class.xoo", false,
+ "xources/nonHidden/.hiddenInVisibleFolder/.xoo", true,
+ "xources/nonHidden/.hiddenInVisibleFolder/Class.xoo", true);
+ }
+
+ private static void prepareHiddenFileProject() throws IOException {
+ if (!SystemUtils.IS_OS_WINDOWS) {
+ return;
+ }
+
+ // On Windows, we need to set the hidden attribute on the file system
+ Set<String> dirAndFilesToHideOnWindows = Set.of(
+ "xources/.hidden",
+ "xources/.hidden/.xoo",
+ "xources/.hidden/.nestedHidden",
+ "xources/.hidden/.nestedHidden/.xoo",
+ "xources/.hidden/.nestedHidden/visibleInHiddenFolder/.xoo",
+ "xources/nonHidden/.xoo",
+ "xources/nonHidden/.hiddenInVisibleFolder",
+ "xources/nonHidden/.hiddenInVisibleFolder/.xoo");
+
+ for (String path : dirAndFilesToHideOnWindows) {
+ Path pathFromResources = Path.of("test-resources/mediumtest/xoo/sample-with-hidden-files", path);
+ setFileAsHiddenOnWindows(pathFromResources);
+ }
+ }
+
+ private static void setFileAsHiddenOnWindows(Path path) throws IOException {
+ if (SystemUtils.IS_OS_WINDOWS) {
+ Files.setAttribute(path, "dos:hidden", true, LinkOption.NOFOLLOW_LINKS);
+ }
+ }
+
+ private static void assertHiddenFileScan(AnalysisResult result, String filePath, boolean expectedHiddenStatus, boolean hiddenFilesShouldBeScanned) {
+ InputFile file = result.inputFile(filePath);
+
+ if (!hiddenFilesShouldBeScanned && expectedHiddenStatus) {
+ assertThat(file).isNull();
+ } else {
+ assertThat(file).withFailMessage(String.format("File \"%s\" was not analyzed", filePath)).isNotNull();
+ assertThat(file.isHidden())
+ .withFailMessage(String.format("Expected file \"%s\" hidden status to be \"%s\", however was \"%s\"", filePath, expectedHiddenStatus, file.isHidden()))
+ .isEqualTo(expectedHiddenStatus);
+ }
+ }
+
+ private static void assertFileIssue(AnalysisResult result, String filePath, boolean expectToHaveIssue) {
+ InputFile file = result.inputFile(filePath);
+ assertThat(file).isNotNull();
+ List<ScannerReport.Issue> issues = result.issuesFor(file);
+ if (expectToHaveIssue) {
+ assertThat(issues).hasSize(1);
+ } else {
+ assertThat(issues).isEmpty();
+ }
+ }
}
diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/scm/ScmMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/scm/ScmMediumIT.java
index e1a158bfac9..904e09cc5c5 100644
--- a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/scm/ScmMediumIT.java
+++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/scm/ScmMediumIT.java
@@ -241,7 +241,7 @@ public class ScmMediumIT {
assertThat(getChangesets(baseDir, NO_BLAME_SCM_ON_SERVER_XOO)).isNull();
// 5 .xoo files + 3 .scm files, but only 4 marked for publishing. 1 file is SAME so not included in the total
- assertThat(logTester.logs()).containsSubsequence("8 files indexed");
+ assertThat(logTester.logs()).anyMatch(s -> s.startsWith("8 files indexed (done) | time="));
assertThat(logTester.logs()).containsSubsequence("SCM Publisher 4 source files to be analyzed");
assertThat(logTester.logs().stream().anyMatch(s -> Pattern.matches("SCM Publisher 3/4 source files have been analyzed \\(done\\) \\| time=[0-9]+ms", s))).isTrue();
assertThat(logTester.logs()).containsSubsequence(MISSING_BLAME_INFORMATION_FOR_THE_FOLLOWING_FILES, " * src/no_blame_scm_on_server.xoo");
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/JGitCleanupService.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/JGitCleanupService.java
new file mode 100644
index 00000000000..28e052cfa4e
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/JGitCleanupService.java
@@ -0,0 +1,47 @@
+/*
+ * 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.scanner.bootstrap;
+
+import java.lang.reflect.Method;
+import org.eclipse.jgit.internal.util.CleanupService;
+
+/**
+ * Normally, JGit terminates with a shutdown hook. Since we also want to support running the Scanner Engine in the same JVM, this allows triggering shutdown manually.
+ */
+class JGitCleanupService implements AutoCloseable {
+
+ private final Method shutDownMethod;
+ private final CleanupService cleanupService;
+
+ public JGitCleanupService() {
+ cleanupService = new CleanupService();
+ try {
+ shutDownMethod = CleanupService.class.getDeclaredMethod("shutDown");
+ } catch (NoSuchMethodException e) {
+ throw new IllegalStateException("Unable to find method 'shutDown' on JGit CleanupService", e);
+ }
+ shutDownMethod.setAccessible(true);
+ }
+
+ @Override
+ public void close() throws Exception {
+ shutDownMethod.invoke(cleanupService);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerMain.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerMain.java
index 0fe2c3ad479..bd8d5b9b99c 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerMain.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerMain.java
@@ -21,17 +21,21 @@ package org.sonar.scanner.bootstrap;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.OutputStreamAppender;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
+import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
import org.jetbrains.annotations.NotNull;
import org.slf4j.LoggerFactory;
import org.sonar.api.utils.MessageException;
@@ -49,11 +53,13 @@ public class ScannerMain {
private static final String SCANNER_APP_VERSION_KEY = "sonar.scanner.appVersion";
public static void main(String... args) {
- System.exit(run(System.in));
+ System.exit(run(System.in, System.out));
}
- public static int run(InputStream in) {
- try {
+ public static int run(InputStream in, OutputStream out) {
+ try (var ignored = new JGitCleanupService()) {
+ configureLogOutput(out);
+
LOG.info("Starting SonarScanner Engine...");
LOG.atInfo().log(ScannerMain::java);
@@ -67,9 +73,11 @@ public class ScannerMain {
LOG.info("SonarScanner Engine completed successfully");
return 0;
- } catch (Exception e) {
- handleException(e);
+ } catch (Throwable throwable) {
+ handleException(throwable);
return 1;
+ } finally {
+ stopLogback();
}
}
@@ -87,30 +95,28 @@ public class ScannerMain {
return sb.toString();
}
- private static void handleException(Exception e) {
- var messageException = unwrapMessageException(e);
+ private static void handleException(Throwable throwable) {
+ var messageException = unwrapMessageException(throwable);
if (messageException.isPresent()) {
// Don't show the stacktrace for a message exception to not pollute the logs
if (LoggerFactory.getLogger(ScannerMain.class).isDebugEnabled()) {
- LOG.error(messageException.get(), e);
+ LOG.error(messageException.get(), throwable);
} else {
LOG.error(messageException.get());
}
} else {
- LOG.error("Error during SonarScanner Engine execution", e);
+ LOG.error("Error during SonarScanner Engine execution", throwable);
}
}
- private static Optional<String> unwrapMessageException(Exception t) {
- Throwable y = t;
- do {
- if (y instanceof MessageException messageException) {
- return Optional.of(messageException.getMessage());
- }
- y = y.getCause();
- } while (y != null);
-
- return Optional.empty();
+ private static Optional<String> unwrapMessageException(@Nullable Throwable throwable) {
+ if (throwable == null) {
+ return Optional.empty();
+ } else if (throwable instanceof MessageException messageException) {
+ return Optional.of(messageException.getMessage());
+ } else {
+ return unwrapMessageException(throwable.getCause());
+ }
}
private static @NotNull Map<String, String> parseInputProperties(InputStream in) {
@@ -157,6 +163,28 @@ public class ScannerMain {
rootLogger.setLevel(Level.toLevel(verbose ? LEVEL_ROOT_VERBOSE : LEVEL_ROOT_DEFAULT));
}
+ private static void configureLogOutput(OutputStream out) {
+ var loggerContext = (ch.qos.logback.classic.LoggerContext) LoggerFactory.getILoggerFactory();
+ var encoder = new ScannerLogbackEncoder();
+ encoder.setContext(loggerContext);
+ encoder.start();
+
+ var appender = new OutputStreamAppender<ILoggingEvent>();
+ appender.setEncoder(encoder);
+ appender.setContext(loggerContext);
+ appender.setOutputStream(out);
+ appender.start();
+
+ var rootLogger = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
+ rootLogger.addAppender(appender);
+ rootLogger.setLevel(Level.toLevel(LEVEL_ROOT_DEFAULT));
+ }
+
+ private static void stopLogback() {
+ var loggerContext = (ch.qos.logback.classic.LoggerContext) LoggerFactory.getILoggerFactory();
+ loggerContext.stop();
+ }
+
private static class Input {
@SerializedName("scannerProperties")
private List<ScannerProperty> scannerProperties;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SpringScannerContainer.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SpringScannerContainer.java
index 9c41891dbab..133f4387856 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SpringScannerContainer.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SpringScannerContainer.java
@@ -99,7 +99,6 @@ import org.sonar.scanner.scan.InputModuleHierarchyProvider;
import org.sonar.scanner.scan.InputProjectProvider;
import org.sonar.scanner.scan.ModuleIndexer;
import org.sonar.scanner.scan.MutableProjectReactorProvider;
-import org.sonar.scanner.scan.MutableProjectSettings;
import org.sonar.scanner.scan.ProjectBuildersExecutor;
import org.sonar.scanner.scan.ProjectConfigurationProvider;
import org.sonar.scanner.scan.ProjectLock;
@@ -116,6 +115,7 @@ import org.sonar.scanner.scan.branch.BranchType;
import org.sonar.scanner.scan.branch.ProjectBranchesProvider;
import org.sonar.scanner.scan.filesystem.DefaultProjectFileSystem;
import org.sonar.scanner.scan.filesystem.FilePreprocessor;
+import org.sonar.scanner.scan.filesystem.HiddenFilesProjectData;
import org.sonar.scanner.scan.filesystem.InputComponentStore;
import org.sonar.scanner.scan.filesystem.LanguageDetection;
import org.sonar.scanner.scan.filesystem.MetadataGenerator;
@@ -200,6 +200,7 @@ public class SpringScannerContainer extends SpringComponentContainer {
FilePreprocessor.class,
ProjectFilePreprocessor.class,
ProjectExclusionFilters.class,
+ HiddenFilesProjectData.class,
// rules
new ActiveRulesProvider(),
@@ -226,7 +227,6 @@ public class SpringScannerContainer extends SpringComponentContainer {
ContextPropertiesCache.class,
TelemetryCache.class,
- MutableProjectSettings.class,
SonarGlobalPropertiesFilter.class,
ProjectConfigurationProvider.class,
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/RulesSeverityDetector.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/RulesSeverityDetector.java
index 703cd038fd0..d4f22ad9704 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/RulesSeverityDetector.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/RulesSeverityDetector.java
@@ -97,8 +97,11 @@ public class RulesSeverityDetector {
}
private static Map<String, Result.Level> getDriverDefinedRuleSeverities(Run run) {
- return run.getTool().getDriver().getRules()
- .stream()
+ Set<ReportingDescriptor> rules = run.getTool().getDriver().getRules();
+ if (rules == null) {
+ return emptyMap();
+ }
+ return rules.stream()
.filter(RulesSeverityDetector::hasRuleDefinedLevel)
.collect(toMap(ReportingDescriptor::getId, x -> Result.Level.valueOf(x.getDefaultConfiguration().getLevel().name())));
}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/RunMapper.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/RunMapper.java
index 5b9abf383cf..bdf5a9a1114 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/RunMapper.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/RunMapper.java
@@ -83,7 +83,7 @@ public class RunMapper {
private List<NewAdHocRule> toNewAdHocRules(Run run, String driverName,
Map<String, Result.Level> ruleSeveritiesByRuleId, Map<String, Result.Level> ruleSeveritiesByRuleIdForNewCCT) {
- Set<ReportingDescriptor> driverRules = run.getTool().getDriver().getRules();
+ Set<ReportingDescriptor> driverRules = Optional.ofNullable(run.getTool().getDriver().getRules()).orElse(Set.of());
Set<ReportingDescriptor> extensionRules = hasExtensions(run.getTool())
? run.getTool().getExtensions().stream().filter(RunMapper::hasRules).flatMap(extension -> extension.getRules().stream()).collect(toSet())
: Set.of();
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ScannerWsClientProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ScannerWsClientProvider.java
index 088ebfb0052..d9eed8bedc8 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ScannerWsClientProvider.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ScannerWsClientProvider.java
@@ -87,7 +87,7 @@ public class ScannerWsClientProvider {
String responseTimeout = defaultIfBlank(scannerProps.property(SONAR_SCANNER_RESPONSE_TIMEOUT), valueOf(DEFAULT_RESPONSE_TIMEOUT));
String envVarToken = defaultIfBlank(system.envVariable(TOKEN_ENV_VARIABLE), null);
String token = defaultIfBlank(scannerProps.property(TOKEN_PROPERTY), envVarToken);
- String login = defaultIfBlank(scannerProps.property(CoreProperties.LOGIN), token);
+ String login = defaultIfBlank(token, scannerProps.property(CoreProperties.LOGIN));
boolean skipSystemTrustMaterial = Boolean.parseBoolean(defaultIfBlank(scannerProps.property(SKIP_SYSTEM_TRUST_MATERIAL), "false"));
var sslContext = configureSsl(parseSslConfig(scannerProps, sonarUserHome), system, skipSystemTrustMaterial);
connectorBuilder
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliCacheService.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliCacheService.java
index 51bb79c9d23..24db0ddec64 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliCacheService.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliCacheService.java
@@ -25,6 +25,7 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
+import java.io.UncheckedIOException;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -139,7 +140,7 @@ public class CliCacheService {
String checksum = metadataResponse.sha256();
// If we have a matching checksum dir with the existing CLI file, then we are up to date.
if (!cachedCliFile(checksum).exists()) {
- LOG.debug("CLI checksum mismatch");
+ LOG.debug("SCA CLI update detected");
downloadCli(metadataResponse.id(), checksum);
telemetryCache.put("scanner.sca.get.cli.cache.hit", "false");
} else {
@@ -175,8 +176,8 @@ public class CliCacheService {
}.getType();
return new Gson().fromJson(reader, listOfMetadata);
}
- } catch (Exception e) {
- throw new IllegalStateException("Unable to load CLI metadata", e);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
}
}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliService.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliService.java
index 8a8e90cce25..6b3418a8f6a 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliService.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliService.java
@@ -29,20 +29,22 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import java.util.function.Consumer;
+import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
+import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.fs.internal.DefaultInputModule;
import org.sonar.api.platform.Server;
import org.sonar.api.utils.System2;
import org.sonar.core.util.ProcessWrapperFactory;
import org.sonar.scanner.config.DefaultConfiguration;
import org.sonar.scanner.repository.TelemetryCache;
+import org.sonar.scanner.scan.filesystem.ProjectExclusionFilters;
import org.sonar.scanner.scm.ScmConfiguration;
import org.sonar.scm.git.JGitUtils;
@@ -54,37 +56,42 @@ import org.sonar.scm.git.JGitUtils;
*/
public class CliService {
private static final Logger LOG = LoggerFactory.getLogger(CliService.class);
- public static final String EXCLUDED_MANIFESTS_PROP_KEY = "sonar.sca.excludedManifests";
+ public static final String SCA_EXCLUSIONS_KEY = "sonar.sca.exclusions";
+ public static final String LEGACY_SCA_EXCLUSIONS_KEY = "sonar.sca.excludedManifests";
private final ProcessWrapperFactory processWrapperFactory;
private final TelemetryCache telemetryCache;
private final System2 system2;
private final Server server;
private final ScmConfiguration scmConfiguration;
+ private final ProjectExclusionFilters projectExclusionFilters;
- public CliService(ProcessWrapperFactory processWrapperFactory, TelemetryCache telemetryCache, System2 system2, Server server, ScmConfiguration scmConfiguration) {
+ public CliService(ProcessWrapperFactory processWrapperFactory, TelemetryCache telemetryCache, System2 system2, Server server, ScmConfiguration scmConfiguration,
+ ProjectExclusionFilters projectExclusionFilters) {
this.processWrapperFactory = processWrapperFactory;
this.telemetryCache = telemetryCache;
this.system2 = system2;
this.server = server;
this.scmConfiguration = scmConfiguration;
+ this.projectExclusionFilters = projectExclusionFilters;
}
- public File generateManifestsZip(DefaultInputModule module, File cliExecutable, DefaultConfiguration configuration) throws IOException, IllegalStateException {
+ public File generateManifestsArchive(DefaultInputModule module, File cliExecutable, DefaultConfiguration configuration) throws IOException, IllegalStateException {
long startTime = system2.now();
boolean success = false;
try {
- String zipName = "dependency-files.zip";
- Path zipPath = module.getWorkDir().resolve(zipName);
+ String archiveName = "dependency-files.tar.xz";
+ Path archivePath = module.getWorkDir().resolve(archiveName);
List<String> args = new ArrayList<>();
args.add(cliExecutable.getAbsolutePath());
args.add("projects");
args.add("save-lockfiles");
- args.add("--zip");
- args.add("--zip-filename");
- args.add(zipPath.toAbsolutePath().toString());
+ args.add("--xz");
+ args.add("--xz-filename");
+ args.add(archivePath.toAbsolutePath().toString());
args.add("--directory");
args.add(module.getBaseDir().toString());
+ args.add("--recursive");
String excludeFlag = getExcludeFlag(module, configuration);
if (excludeFlag != null) {
@@ -92,8 +99,7 @@ public class CliService {
args.add(excludeFlag);
}
- boolean scaDebug = configuration.getBoolean("sonar.sca.debug").orElse(false);
- if (LOG.isDebugEnabled() || scaDebug) {
+ if (LOG.isDebugEnabled()) {
LOG.info("Setting CLI to debug mode");
args.add("--debug");
}
@@ -104,18 +110,16 @@ public class CliService {
envProperties.put("TIDELIFT_ALLOW_MANIFEST_FAILURES", "1");
envProperties.put("TIDELIFT_CLI_INSIDE_SCANNER_ENGINE", "1");
envProperties.put("TIDELIFT_CLI_SQ_SERVER_VERSION", server.getVersion());
- // EXCLUDED_MANIFESTS_PROP_KEY is a special case which we handle via --args, not environment variables
- Set<String> ignoredProperties = Set.of(EXCLUDED_MANIFESTS_PROP_KEY);
- envProperties.putAll(ScaProperties.buildFromScannerProperties(configuration, ignoredProperties));
+ envProperties.putAll(ScaProperties.buildFromScannerProperties(configuration));
LOG.info("Running command: {}", args);
LOG.info("Environment properties: {}", envProperties);
Consumer<String> logConsumer = LOG.atLevel(Level.INFO)::log;
processWrapperFactory.create(module.getWorkDir(), logConsumer, logConsumer, envProperties, args.toArray(new String[0])).execute();
- LOG.info("Generated manifests zip file: {}", zipName);
+ LOG.info("Generated manifests archive file: {}", archiveName);
success = true;
- return zipPath.toFile();
+ return archivePath.toFile();
} finally {
telemetryCache.put("scanner.sca.execution.cli.duration", String.valueOf(system2.now() - startTime));
telemetryCache.put("scanner.sca.execution.cli.success", String.valueOf(success));
@@ -123,7 +127,7 @@ public class CliService {
}
private @Nullable String getExcludeFlag(DefaultInputModule module, DefaultConfiguration configuration) throws IOException {
- List<String> configExcludedPaths = getConfigExcludedPaths(configuration);
+ List<String> configExcludedPaths = getConfigExcludedPaths(configuration, projectExclusionFilters);
List<String> scmIgnoredPaths = getScmIgnoredPaths(module);
ArrayList<String> mergedExclusionPaths = new ArrayList<>();
@@ -143,12 +147,15 @@ public class CliService {
return toCsvString(mergedExclusionPaths);
}
- private static List<String> getConfigExcludedPaths(DefaultConfiguration configuration) {
- String[] excludedPaths = configuration.getStringArray(EXCLUDED_MANIFESTS_PROP_KEY);
- if (excludedPaths == null) {
- return List.of();
- }
- return Arrays.stream(excludedPaths).toList();
+ private static List<String> getConfigExcludedPaths(DefaultConfiguration configuration, ProjectExclusionFilters projectExclusionFilters) {
+ String[] sonarExclusions = projectExclusionFilters.getExclusionsConfig(InputFile.Type.MAIN);
+ String[] scaExclusions = configuration.getStringArray(SCA_EXCLUSIONS_KEY);
+ String[] scaExclusionsLegacy = configuration.getStringArray(LEGACY_SCA_EXCLUSIONS_KEY);
+
+ return Stream.of(sonarExclusions, scaExclusions, scaExclusionsLegacy)
+ .flatMap(Arrays::stream)
+ .distinct()
+ .toList();
}
private List<String> getScmIgnoredPaths(DefaultInputModule module) {
@@ -170,7 +177,13 @@ public class CliService {
}
return scmIgnoredPaths.stream()
.map(ignoredPathRel -> {
- boolean isDirectory = Files.isDirectory(baseDirPath.resolve(ignoredPathRel));
+
+ boolean isDirectory = false;
+ try {
+ isDirectory = Files.isDirectory(baseDirPath.resolve(ignoredPathRel.replace("/", File.separator)));
+ } catch (java.nio.file.InvalidPathException e) {
+ // if it's not a valid path, it's not a directory so we can just pass to the Tidelift CLI
+ }
// Directories need to get turned into a glob for the Tidelift CLI
return isDirectory ? (ignoredPathRel + "/**") : ignoredPathRel;
})
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/ScaExecutor.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/ScaExecutor.java
index 06142fadb8f..143e144c2dc 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/ScaExecutor.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/ScaExecutor.java
@@ -21,6 +21,8 @@ package org.sonar.scanner.sca;
import java.io.File;
import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.commons.lang3.time.StopWatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.batch.fs.internal.DefaultInputModule;
@@ -65,18 +67,25 @@ public class ScaExecutor {
return;
}
+ var stopwatch = new StopWatch();
+ stopwatch.start();
LOG.info("Checking for latest CLI");
File cliFile = cliCacheService.cacheCli();
LOG.info("Collecting manifests for the dependency analysis...");
if (cliFile.exists()) {
try {
- File generatedZip = cliService.generateManifestsZip(root, cliFile, configuration);
+ File generatedZip = cliService.generateManifestsArchive(root, cliFile, configuration);
LOG.debug("Zip ready for report: {}", generatedZip);
reportPublisher.getWriter().writeScaFile(generatedZip);
LOG.debug("Manifest zip written to report");
} catch (IOException | IllegalStateException e) {
LOG.error("Error gathering manifests", e);
+ } finally {
+ stopwatch.stop();
+ if (LOG.isInfoEnabled()) {
+ LOG.info("Load SCA project dependencies (done) | time={}ms", stopwatch.getTime(TimeUnit.MILLISECONDS));
+ }
}
}
}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/ScaProperties.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/ScaProperties.java
index 5c848b4ddbc..a697aef3e20 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/ScaProperties.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/ScaProperties.java
@@ -30,6 +30,13 @@ import org.sonar.scanner.config.DefaultConfiguration;
public class ScaProperties {
private static final Pattern sonarScaPropertyRegex = Pattern.compile("^sonar\\.sca\\.([a-zA-Z]+)$");
private static final String SONAR_SCA_PREFIX = "sonar.sca.";
+ private static final Set<String> IGNORED_PROPERTIES = Set.of(
+ // sonar.sca.exclusions is a special case which we handle when building --exclude
+ "sonar.sca.exclusions",
+ // excludedManifests is a special case which we handle when building --exclude
+ "sonar.sca.excludedManifests",
+ // keep recursive enabled to better match sonar-scanner behavior
+ "sonar.sca.recursiveManifestSearch");
private ScaProperties() {
}
@@ -46,22 +53,16 @@ public class ScaProperties {
* { "sonar.someOtherProperty" : "value" } returns an empty map
*
* @param configuration the scanner configuration possibly containing sonar.sca.* properties
- * @param ignoredPropertyNames property names that should not be processed as a property
* @return a map of Tidelift CLI compatible environment variable names to their configuration values
*/
- public static Map<String, String> buildFromScannerProperties(DefaultConfiguration configuration, Set<String> ignoredPropertyNames) {
+ public static Map<String, String> buildFromScannerProperties(DefaultConfiguration configuration) {
HashMap<String, String> props = new HashMap<>(configuration.getProperties());
- // recursive mode defaults to true
- if (!props.containsKey("sonar.sca.recursiveManifestSearch")) {
- props.put("sonar.sca.recursiveManifestSearch", "true");
- }
-
return props
.entrySet()
.stream()
.filter(entry -> entry.getKey().startsWith(SONAR_SCA_PREFIX))
- .filter(entry -> !ignoredPropertyNames.contains(entry.getKey()))
+ .filter(entry -> !IGNORED_PROPERTIES.contains(entry.getKey()))
.collect(Collectors.toMap(entry -> convertPropToEnvVariable(entry.getKey()), Map.Entry::getValue));
}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/MutableModuleSettings.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/MutableModuleSettings.java
deleted file mode 100644
index 15912f8a510..00000000000
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/MutableModuleSettings.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * 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.scanner.scan;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-import jakarta.annotation.Priority;
-import org.sonar.api.config.internal.Settings;
-
-import static java.util.Objects.requireNonNull;
-
-/**
- * @deprecated since 6.5 {@link ModuleConfiguration} used to be mutable, so keep a mutable copy for backward compatibility.
- */
-@Deprecated
-@Priority(1)
-public class MutableModuleSettings extends Settings {
-
- private final Map<String, String> properties = new HashMap<>();
-
- public MutableModuleSettings(ModuleConfiguration config) {
- super(config.getDefinitions(), config.getEncryption());
- addProperties(config.getProperties());
- }
-
- @Override
- protected Optional<String> get(String key) {
- return Optional.ofNullable(properties.get(key));
- }
-
- @Override
- protected void set(String key, String value) {
- properties.put(
- requireNonNull(key, "key can't be null"),
- requireNonNull(value, "value can't be null").trim());
- }
-
- @Override
- protected void remove(String key) {
- properties.remove(key);
- }
-
- @Override
- public Map<String, String> getProperties() {
- return properties;
- }
-}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/MutableProjectSettings.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/MutableProjectSettings.java
deleted file mode 100644
index df24cbe81e5..00000000000
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/MutableProjectSettings.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * 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.scanner.scan;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-import org.sonar.api.config.internal.Settings;
-import org.sonar.scanner.bootstrap.GlobalConfiguration;
-
-import jakarta.annotation.Priority;
-
-import static java.util.Objects.requireNonNull;
-
-/**
- * @deprecated since 6.5 {@link ProjectConfiguration} used to be mutable, so keep a mutable copy for backward compatibility.
- */
-@Deprecated
-@Priority(2)
-public class MutableProjectSettings extends Settings {
-
- private final Map<String, String> properties = new HashMap<>();
-
- public MutableProjectSettings(GlobalConfiguration globalConfig) {
- super(globalConfig.getDefinitions(), globalConfig.getEncryption());
- addProperties(globalConfig.getProperties());
- }
-
- public void complete(ProjectConfiguration projectConfig) {
- addProperties(projectConfig.getProperties());
- }
-
- @Override
- protected Optional<String> get(String key) {
- return Optional.ofNullable(properties.get(key));
- }
-
- @Override
- protected void set(String key, String value) {
- properties.put(
- requireNonNull(key, "key can't be null"),
- requireNonNull(value, "value can't be null").trim());
- }
-
- @Override
- protected void remove(String key) {
- properties.remove(key);
- }
-
- @Override
- public Map<String, String> getProperties() {
- return properties;
- }
-}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectConfigurationProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectConfigurationProvider.java
index c12ec245924..e5543d4f9c5 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectConfigurationProvider.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectConfigurationProvider.java
@@ -26,7 +26,6 @@ import org.sonar.scanner.bootstrap.GlobalConfiguration;
import org.sonar.scanner.bootstrap.GlobalServerSettings;
import org.springframework.context.annotation.Bean;
-
public class ProjectConfigurationProvider {
private final SonarGlobalPropertiesFilter sonarGlobalPropertiesFilter;
@@ -37,7 +36,7 @@ public class ProjectConfigurationProvider {
@Bean("ProjectConfiguration")
public ProjectConfiguration provide(DefaultInputProject project, GlobalConfiguration globalConfig, GlobalServerSettings globalServerSettings,
- ProjectServerSettings projectServerSettings, MutableProjectSettings projectSettings) {
+ ProjectServerSettings projectServerSettings) {
Map<String, String> settings = new LinkedHashMap<>();
settings.putAll(globalServerSettings.properties());
settings.putAll(projectServerSettings.properties());
@@ -45,10 +44,7 @@ public class ProjectConfigurationProvider {
settings = sonarGlobalPropertiesFilter.enforceOnlyServerSideSonarGlobalPropertiesAreUsed(settings, globalServerSettings.properties());
- ProjectConfiguration projectConfig = new ProjectConfiguration(globalConfig.getDefinitions(), globalConfig.getEncryption(), settings);
- projectSettings.complete(projectConfig);
- return projectConfig;
+ return new ProjectConfiguration(globalConfig.getDefinitions(), globalConfig.getEncryption(), settings);
}
-
}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/SpringModuleScanContainer.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/SpringModuleScanContainer.java
index 4315c762481..8ddb889912d 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/SpringModuleScanContainer.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/SpringModuleScanContainer.java
@@ -54,7 +54,6 @@ public class SpringModuleScanContainer extends SpringComponentContainer {
add(
module.definition(),
module,
- MutableModuleSettings.class,
SonarGlobalPropertiesFilter.class,
ModuleConfigurationProvider.class,
@@ -68,8 +67,7 @@ public class SpringModuleScanContainer extends SpringComponentContainer {
ModuleSensorOptimizer.class,
ModuleSensorContext.class,
- ModuleSensorExtensionDictionary.class
- );
+ ModuleSensorExtensionDictionary.class);
}
private void addExtensions() {
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/DirectoryFileVisitor.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/DirectoryFileVisitor.java
index 314e923ce71..242bc015574 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/DirectoryFileVisitor.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/DirectoryFileVisitor.java
@@ -24,17 +24,15 @@ import java.nio.file.AccessDeniedException;
import java.nio.file.FileSystemLoopException;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
-import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
-import java.nio.file.attribute.DosFileAttributes;
-import org.apache.commons.lang3.SystemUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.fs.internal.DefaultInputModule;
import org.sonar.scanner.fs.InputModuleHierarchy;
+import org.sonar.scanner.scan.ModuleConfiguration;
public class DirectoryFileVisitor implements FileVisitor<Path> {
@@ -43,27 +41,31 @@ public class DirectoryFileVisitor implements FileVisitor<Path> {
private final FileVisitAction fileVisitAction;
private final DefaultInputModule module;
private final ModuleExclusionFilters moduleExclusionFilters;
-
private final InputModuleHierarchy inputModuleHierarchy;
private final InputFile.Type type;
+ private final HiddenFilesVisitorHelper hiddenFilesVisitorHelper;
- DirectoryFileVisitor(FileVisitAction fileVisitAction, DefaultInputModule module, ModuleExclusionFilters moduleExclusionFilters,
- InputModuleHierarchy inputModuleHierarchy, InputFile.Type type) {
+ DirectoryFileVisitor(FileVisitAction fileVisitAction, DefaultInputModule module, ModuleConfiguration moduleConfig, ModuleExclusionFilters moduleExclusionFilters,
+ InputModuleHierarchy inputModuleHierarchy, InputFile.Type type, HiddenFilesProjectData hiddenFilesProjectData) {
this.fileVisitAction = fileVisitAction;
this.module = module;
this.moduleExclusionFilters = moduleExclusionFilters;
this.inputModuleHierarchy = inputModuleHierarchy;
this.type = type;
+ this.hiddenFilesVisitorHelper = new HiddenFilesVisitorHelper(hiddenFilesProjectData, module, moduleConfig);
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
- return isHidden(dir) ? FileVisitResult.SKIP_SUBTREE : FileVisitResult.CONTINUE;
+ if (hiddenFilesVisitorHelper.shouldVisitDir(dir)) {
+ return FileVisitResult.CONTINUE;
+ }
+ return FileVisitResult.SKIP_SUBTREE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
- if (!Files.isHidden(file)) {
+ if (hiddenFilesVisitorHelper.shouldVisitFile(file)) {
fileVisitAction.execute(file);
}
return FileVisitResult.CONTINUE;
@@ -129,25 +131,12 @@ public class DirectoryFileVisitor implements FileVisitor<Path> {
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
+ hiddenFilesVisitorHelper.exitDirectory(dir);
return FileVisitResult.CONTINUE;
}
- private static boolean isHidden(Path path) throws IOException {
- if (SystemUtils.IS_OS_WINDOWS) {
- try {
- DosFileAttributes dosFileAttributes = Files.readAttributes(path, DosFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
- return dosFileAttributes.isHidden();
- } catch (UnsupportedOperationException e) {
- return path.toFile().isHidden();
- }
- } else {
- return Files.isHidden(path);
- }
- }
-
@FunctionalInterface
interface FileVisitAction {
void execute(Path file) throws IOException;
}
}
-
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/FileIndexer.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/FileIndexer.java
index 7f31c949132..0961edbd985 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/FileIndexer.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/FileIndexer.java
@@ -63,12 +63,13 @@ public class FileIndexer {
private final ModuleRelativePathWarner moduleRelativePathWarner;
private final InputFileFilterRepository inputFileFilterRepository;
private final Languages languages;
+ private final HiddenFilesProjectData hiddenFilesProjectData;
public FileIndexer(DefaultInputProject project, ScannerComponentIdGenerator scannerComponentIdGenerator, InputComponentStore componentStore,
ProjectCoverageAndDuplicationExclusions projectCoverageAndDuplicationExclusions, IssueExclusionsLoader issueExclusionsLoader,
MetadataGenerator metadataGenerator, SensorStrategy sensorStrategy, LanguageDetection languageDetection, ScanProperties properties,
ScmChangedFiles scmChangedFiles, StatusDetection statusDetection, ModuleRelativePathWarner moduleRelativePathWarner,
- InputFileFilterRepository inputFileFilterRepository, Languages languages) {
+ InputFileFilterRepository inputFileFilterRepository, Languages languages, HiddenFilesProjectData hiddenFilesProjectData) {
this.project = project;
this.scannerComponentIdGenerator = scannerComponentIdGenerator;
this.componentStore = componentStore;
@@ -83,15 +84,18 @@ public class FileIndexer {
this.moduleRelativePathWarner = moduleRelativePathWarner;
this.inputFileFilterRepository = inputFileFilterRepository;
this.languages = languages;
+ this.hiddenFilesProjectData = hiddenFilesProjectData;
}
- void indexFile(DefaultInputModule module, ModuleCoverageAndDuplicationExclusions moduleCoverageAndDuplicationExclusions, Path sourceFile,
- Type type, ProgressReport progressReport) {
+ void indexFile(DefaultInputModule module, ModuleCoverageAndDuplicationExclusions moduleCoverageAndDuplicationExclusions, Path sourceFile, Type type,
+ ProgressReport progressReport) {
Path projectRelativePath = project.getBaseDir().relativize(sourceFile);
Path moduleRelativePath = module.getBaseDir().relativize(sourceFile);
// This should be fast; language should be cached from preprocessing step
Language language = langDetection.language(sourceFile, projectRelativePath);
+ // cached from directory file visitation, after querying the data is removed to reduce memory consumption
+ boolean isHidden = hiddenFilesProjectData.getIsMarkedAsHiddenFileAndRemoveVisibilityInformation(sourceFile, module);
DefaultIndexedFile indexedFile = new DefaultIndexedFile(
sourceFile,
@@ -102,11 +106,12 @@ public class FileIndexer {
language != null ? language.key() : null,
scannerComponentIdGenerator.getAsInt(),
sensorStrategy,
- scmChangedFiles.getOldRelativeFilePath(sourceFile));
+ scmChangedFiles.getOldRelativeFilePath(sourceFile),
+ isHidden);
DefaultInputFile inputFile = new DefaultInputFile(indexedFile, f -> metadataGenerator.setMetadata(module.key(), f, module.getEncoding()),
f -> f.setStatus(statusDetection.findStatusFromScm(f)));
- if (language != null && isPublishAllFiles(language.key())) {
+ if (!isHidden && language != null && isPublishAllFiles(language.key())) {
inputFile.setPublished(true);
}
if (!accept(inputFile)) {
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/FilePreprocessor.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/FilePreprocessor.java
index 544fe46c43b..a87c5f11fc9 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/FilePreprocessor.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/FilePreprocessor.java
@@ -147,22 +147,35 @@ public class FilePreprocessor {
return true;
}
- Path target = Files.readSymbolicLink(absolutePath);
- if (!Files.exists(target)) {
+ Optional<Path> target = resolvePathToTarget(absolutePath);
+ if (target.isEmpty() || !Files.exists(target.get())) {
LOG.warn("File '{}' is ignored. It is a symbolic link targeting a file that does not exist.", absolutePath);
return false;
}
- if (!target.startsWith(project.getBaseDir())) {
+ if (!target.get().startsWith(project.getBaseDir())) {
LOG.warn("File '{}' is ignored. It is a symbolic link targeting a file not located in project basedir.", absolutePath);
return false;
}
- if (!target.startsWith(moduleBaseDirectory)) {
+ if (!target.get().startsWith(moduleBaseDirectory)) {
LOG.info("File '{}' is ignored. It is a symbolic link targeting a file not located in module basedir.", absolutePath);
return false;
}
return true;
}
+
+ private static Optional<Path> resolvePathToTarget(Path symbolicLinkAbsolutePath) throws IOException {
+ Path target = Files.readSymbolicLink(symbolicLinkAbsolutePath);
+ if (target.isAbsolute()) {
+ return Optional.of(target);
+ }
+
+ try {
+ return Optional.of(symbolicLinkAbsolutePath.getParent().resolve(target).toRealPath(LinkOption.NOFOLLOW_LINKS).toAbsolutePath().normalize());
+ } catch (IOException e) {
+ return Optional.empty();
+ }
+ }
}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/HiddenFilesProjectData.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/HiddenFilesProjectData.java
new file mode 100644
index 00000000000..d779a054455
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/HiddenFilesProjectData.java
@@ -0,0 +1,77 @@
+/*
+ * 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.scanner.scan.filesystem;
+
+import java.io.IOException;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import org.apache.commons.lang3.SystemUtils;
+import org.sonar.api.batch.fs.internal.DefaultInputModule;
+import org.sonar.scanner.bootstrap.SonarUserHome;
+
+public class HiddenFilesProjectData {
+
+ final Map<DefaultInputModule, Set<Path>> hiddenFilesByModule = new HashMap<>();
+ private final SonarUserHome sonarUserHome;
+ private Path cachedSonarUserHomePath;
+
+ public HiddenFilesProjectData(SonarUserHome sonarUserHome) {
+ this.sonarUserHome = sonarUserHome;
+ }
+
+ public void markAsHiddenFile(Path file, DefaultInputModule module) {
+ hiddenFilesByModule.computeIfAbsent(module, k -> new HashSet<>()).add(file);
+ }
+
+ /**
+ * To alleviate additional strain on the memory, we remove the visibility information for <code>hiddenFilesByModule</code> mapdirectly after querying,
+ * as we don't need it afterward.
+ */
+ public boolean getIsMarkedAsHiddenFileAndRemoveVisibilityInformation(Path file, DefaultInputModule module) {
+ Set<Path> hiddenFilesPerModule = hiddenFilesByModule.get(module);
+ if (hiddenFilesPerModule != null) {
+ return hiddenFilesPerModule.remove(file);
+ }
+ return false;
+ }
+
+ public Path getCachedSonarUserHomePath() throws IOException {
+ if (cachedSonarUserHomePath == null) {
+ cachedSonarUserHomePath = resolveRealPath(sonarUserHome.getPath());
+ }
+ return cachedSonarUserHomePath;
+ }
+
+ public void clearHiddenFilesData() {
+ // Allowing the GC to collect the map, should only be done after all indexing is complete
+ hiddenFilesByModule.clear();
+ }
+
+ public Path resolveRealPath(Path path) throws IOException {
+ if (SystemUtils.IS_OS_WINDOWS) {
+ return path.toRealPath(LinkOption.NOFOLLOW_LINKS).toAbsolutePath().normalize();
+ }
+ return path;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/HiddenFilesVisitorHelper.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/HiddenFilesVisitorHelper.java
new file mode 100644
index 00000000000..607a859ef44
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/HiddenFilesVisitorHelper.java
@@ -0,0 +1,112 @@
+/*
+ * 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.scanner.scan.filesystem;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.DosFileAttributes;
+import org.apache.commons.lang3.SystemUtils;
+import org.sonar.api.batch.fs.internal.DefaultInputModule;
+import org.sonar.scanner.scan.ModuleConfiguration;
+
+public class HiddenFilesVisitorHelper {
+
+ private static final String EXCLUDE_HIDDEN_FILES_PROPERTY = "sonar.scanner.excludeHiddenFiles";
+ private final HiddenFilesProjectData hiddenFilesProjectData;
+ private final DefaultInputModule module;
+ final boolean excludeHiddenFiles;
+ private Path moduleWorkDir;
+ Path rootHiddenDir;
+
+ public HiddenFilesVisitorHelper(HiddenFilesProjectData hiddenFilesProjectData, DefaultInputModule module, ModuleConfiguration moduleConfig) {
+ this.hiddenFilesProjectData = hiddenFilesProjectData;
+ this.module = module;
+ this.excludeHiddenFiles = moduleConfig.getBoolean(EXCLUDE_HIDDEN_FILES_PROPERTY).orElse(false);
+ }
+
+ public boolean shouldVisitDir(Path path) throws IOException {
+ boolean isHidden = isHiddenDir(path);
+
+ if (isHidden && (excludeHiddenFiles || isExcludedHiddenDirectory(path))) {
+ return false;
+ }
+ if (isHidden) {
+ enterHiddenDirectory(path);
+ }
+ return true;
+ }
+
+ private boolean isExcludedHiddenDirectory(Path path) throws IOException {
+ return getCachedModuleWorkDir().equals(path) || hiddenFilesProjectData.getCachedSonarUserHomePath().equals(path);
+ }
+
+ void enterHiddenDirectory(Path dir) {
+ if (!insideHiddenDirectory()) {
+ rootHiddenDir = dir;
+ }
+ }
+
+ public void exitDirectory(Path path) {
+ if (insideHiddenDirectory() && rootHiddenDir.equals(path)) {
+ resetRootHiddenDir();
+ }
+ }
+
+ void resetRootHiddenDir() {
+ this.rootHiddenDir = null;
+ }
+
+ public boolean shouldVisitFile(Path path) throws IOException {
+ boolean isHidden = insideHiddenDirectory() || Files.isHidden(path);
+
+ if (!excludeHiddenFiles && isHidden) {
+ hiddenFilesProjectData.markAsHiddenFile(path, module);
+ }
+
+ return !excludeHiddenFiles || !isHidden;
+ }
+
+ private Path getCachedModuleWorkDir() throws IOException {
+ if (moduleWorkDir == null) {
+ moduleWorkDir = hiddenFilesProjectData.resolveRealPath(module.getWorkDir());
+ }
+ return moduleWorkDir;
+ }
+
+ // visible for testing
+ boolean insideHiddenDirectory() {
+ return rootHiddenDir != null;
+ }
+
+ protected static boolean isHiddenDir(Path path) throws IOException {
+ if (SystemUtils.IS_OS_WINDOWS) {
+ try {
+ DosFileAttributes dosFileAttributes = Files.readAttributes(path, DosFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
+ return dosFileAttributes.isHidden();
+ } catch (UnsupportedOperationException e) {
+ return path.toFile().isHidden();
+ }
+ } else {
+ return Files.isHidden(path);
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/ModuleInputComponentStore.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/ModuleInputComponentStore.java
index 6ef26dafd07..68b6d1db580 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/ModuleInputComponentStore.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/ModuleInputComponentStore.java
@@ -19,12 +19,15 @@
*/
package org.sonar.scanner.scan.filesystem;
+import java.util.Set;
import java.util.SortedSet;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
import org.sonar.api.batch.ScannerSide;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.fs.InputModule;
-import org.sonar.api.batch.fs.internal.SensorStrategy;
import org.sonar.api.batch.fs.internal.DefaultFileSystem;
+import org.sonar.api.batch.fs.internal.SensorStrategy;
@ScannerSide
public class ModuleInputComponentStore extends DefaultFileSystem.Cache {
@@ -73,11 +76,29 @@ public class ModuleInputComponentStore extends DefaultFileSystem.Cache {
@Override
public Iterable<InputFile> getFilesByName(String filename) {
- return inputComponentStore.getFilesByName(filename);
+ Iterable<InputFile> allFilesByName = inputComponentStore.getFilesByName(filename);
+ if (strategy.isGlobal()) {
+ return allFilesByName;
+ }
+
+ return filterByModule(allFilesByName);
}
@Override
public Iterable<InputFile> getFilesByExtension(String extension) {
- return inputComponentStore.getFilesByExtension(extension);
+ Iterable<InputFile> allFilesByExtension = inputComponentStore.getFilesByExtension(extension);
+ if (strategy.isGlobal()) {
+ return allFilesByExtension;
+ }
+
+ return filterByModule(allFilesByExtension);
+ }
+
+ private Iterable<InputFile> filterByModule(Iterable<InputFile> projectInputFiles) {
+ Set<InputFile> projectInputFilesSet = StreamSupport.stream(projectInputFiles.spliterator(), false)
+ .collect(Collectors.toSet());
+ return StreamSupport.stream(inputComponentStore.filesByModule(moduleKey).spliterator(), false)
+ .filter(projectInputFilesSet::contains)
+ .toList();
}
}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/MutableFileSystem.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/MutableFileSystem.java
index 5daa384d3ac..9c969f6ae20 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/MutableFileSystem.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/MutableFileSystem.java
@@ -25,35 +25,54 @@ import org.sonar.api.batch.fs.FilePredicates;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.fs.internal.DefaultFileSystem;
import org.sonar.api.batch.fs.internal.predicates.ChangedFilePredicate;
+import org.sonar.api.batch.fs.internal.predicates.NonHiddenFilesPredicate;
public class MutableFileSystem extends DefaultFileSystem {
- private boolean restrictToChangedFiles = false;
+
+ boolean restrictToChangedFiles = false;
+ boolean allowHiddenFileAnalysis = false;
public MutableFileSystem(Path baseDir, Cache cache, FilePredicates filePredicates) {
super(baseDir, cache, filePredicates);
}
- public MutableFileSystem(Path baseDir) {
+ MutableFileSystem(Path baseDir) {
super(baseDir);
}
@Override
public Iterable<InputFile> inputFiles(FilePredicate requestPredicate) {
- if (restrictToChangedFiles) {
- return super.inputFiles(new ChangedFilePredicate(requestPredicate));
- }
- return super.inputFiles(requestPredicate);
+ return super.inputFiles(applyAdditionalPredicate(requestPredicate));
}
@Override
public InputFile inputFile(FilePredicate requestPredicate) {
+ return super.inputFile(applyAdditionalPredicate(requestPredicate));
+ }
+
+ private FilePredicate applyAdditionalPredicate(FilePredicate requestPredicate) {
+ return applyHiddenFilePredicate(applyChangedFilePredicate(requestPredicate));
+ }
+
+ private FilePredicate applyHiddenFilePredicate(FilePredicate predicate) {
+ if (allowHiddenFileAnalysis) {
+ return predicate;
+ }
+ return predicates().and(new NonHiddenFilesPredicate(), predicate);
+ }
+
+ private FilePredicate applyChangedFilePredicate(FilePredicate predicate) {
if (restrictToChangedFiles) {
- return super.inputFile(new ChangedFilePredicate(requestPredicate));
+ return predicates().and(new ChangedFilePredicate(), predicate);
}
- return super.inputFile(requestPredicate);
+ return predicate;
}
public void setRestrictToChangedFiles(boolean restrictToChangedFiles) {
this.restrictToChangedFiles = restrictToChangedFiles;
}
+
+ public void setAllowHiddenFileAnalysis(boolean allowHiddenFileAnalysis) {
+ this.allowHiddenFileAnalysis = allowHiddenFileAnalysis;
+ }
}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/ProjectFileIndexer.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/ProjectFileIndexer.java
index 97e449fcb26..c1349872c24 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/ProjectFileIndexer.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/ProjectFileIndexer.java
@@ -62,6 +62,7 @@ public class ProjectFileIndexer {
private final FileIndexer fileIndexer;
private final ProjectFilePreprocessor projectFilePreprocessor;
private final AnalysisWarnings analysisWarnings;
+ private final HiddenFilesProjectData hiddenFilesProjectData;
private ProgressReport progressReport;
@@ -69,7 +70,7 @@ public class ProjectFileIndexer {
SonarGlobalPropertiesFilter sonarGlobalPropertiesFilter, InputModuleHierarchy inputModuleHierarchy,
GlobalConfiguration globalConfig, GlobalServerSettings globalServerSettings, ProjectServerSettings projectServerSettings,
FileIndexer fileIndexer, ProjectCoverageAndDuplicationExclusions projectCoverageAndDuplicationExclusions,
- ProjectFilePreprocessor projectFilePreprocessor, AnalysisWarnings analysisWarnings) {
+ ProjectFilePreprocessor projectFilePreprocessor, AnalysisWarnings analysisWarnings, HiddenFilesProjectData hiddenFilesProjectData) {
this.componentStore = componentStore;
this.sonarGlobalPropertiesFilter = sonarGlobalPropertiesFilter;
this.inputModuleHierarchy = inputModuleHierarchy;
@@ -81,6 +82,7 @@ public class ProjectFileIndexer {
this.projectCoverageAndDuplicationExclusions = projectCoverageAndDuplicationExclusions;
this.projectFilePreprocessor = projectFilePreprocessor;
this.analysisWarnings = analysisWarnings;
+ this.hiddenFilesProjectData = hiddenFilesProjectData;
}
public void index() {
@@ -91,10 +93,10 @@ public class ProjectFileIndexer {
projectCoverageAndDuplicationExclusions.log(" ");
indexModulesRecursively(inputModuleHierarchy.root());
+ hiddenFilesProjectData.clearHiddenFilesData();
int totalIndexed = componentStore.inputFiles().size();
- progressReport.stop(totalIndexed + " " + pluralizeFiles(totalIndexed) + " indexed");
-
+ progressReport.stopAndLogTotalTime(totalIndexed + " " + pluralizeFiles(totalIndexed) + " indexed");
}
private void indexModulesRecursively(DefaultInputModule module) {
@@ -118,15 +120,15 @@ public class ProjectFileIndexer {
moduleCoverageAndDuplicationExclusions.log(" ");
}
List<Path> mainSourceDirsOrFiles = projectFilePreprocessor.getMainSourcesByModule(module);
- indexFiles(module, moduleExclusionFilters, moduleCoverageAndDuplicationExclusions, mainSourceDirsOrFiles, Type.MAIN);
+ indexFiles(module, moduleConfig, moduleExclusionFilters, moduleCoverageAndDuplicationExclusions, mainSourceDirsOrFiles, Type.MAIN);
projectFilePreprocessor.getTestSourcesByModule(module)
- .ifPresent(tests -> indexFiles(module, moduleExclusionFilters, moduleCoverageAndDuplicationExclusions, tests, Type.TEST));
+ .ifPresent(tests -> indexFiles(module, moduleConfig, moduleExclusionFilters, moduleCoverageAndDuplicationExclusions, tests, Type.TEST));
}
private static void logPaths(String label, Path baseDir, List<Path> paths) {
if (!paths.isEmpty()) {
StringBuilder sb = new StringBuilder(label);
- for (Iterator<Path> it = paths.iterator(); it.hasNext(); ) {
+ for (Iterator<Path> it = paths.iterator(); it.hasNext();) {
Path file = it.next();
Optional<String> relativePathToBaseDir = PathResolver.relativize(baseDir, file);
if (relativePathToBaseDir.isEmpty()) {
@@ -148,12 +150,13 @@ public class ProjectFileIndexer {
}
}
- private void indexFiles(DefaultInputModule module, ModuleExclusionFilters moduleExclusionFilters, ModuleCoverageAndDuplicationExclusions moduleCoverageAndDuplicationExclusions,
+ private void indexFiles(DefaultInputModule module, ModuleConfiguration moduleConfig, ModuleExclusionFilters moduleExclusionFilters,
+ ModuleCoverageAndDuplicationExclusions moduleCoverageAndDuplicationExclusions,
List<Path> sources, Type type) {
try {
for (Path dirOrFile : sources) {
if (dirOrFile.toFile().isDirectory()) {
- indexDirectory(module, moduleExclusionFilters, moduleCoverageAndDuplicationExclusions, dirOrFile, type);
+ indexDirectory(module, moduleConfig, moduleExclusionFilters, moduleCoverageAndDuplicationExclusions, dirOrFile, type);
} else {
fileIndexer.indexFile(module, moduleCoverageAndDuplicationExclusions, dirOrFile, type, progressReport);
}
@@ -163,18 +166,16 @@ public class ProjectFileIndexer {
}
}
- private void indexDirectory(DefaultInputModule module, ModuleExclusionFilters moduleExclusionFilters,
+ private void indexDirectory(DefaultInputModule module, ModuleConfiguration moduleConfig, ModuleExclusionFilters moduleExclusionFilters,
ModuleCoverageAndDuplicationExclusions moduleCoverageAndDuplicationExclusions,
Path dirToIndex, Type type) throws IOException {
Files.walkFileTree(dirToIndex.normalize(), Collections.singleton(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,
new DirectoryFileVisitor(file -> fileIndexer.indexFile(module, moduleCoverageAndDuplicationExclusions, file, type, progressReport),
- module, moduleExclusionFilters, inputModuleHierarchy, type));
+ module, moduleConfig, moduleExclusionFilters, inputModuleHierarchy, type, hiddenFilesProjectData));
}
private static String pluralizeFiles(int count) {
return count == 1 ? "file" : "files";
}
-
-
}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/ProjectFilePreprocessor.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/ProjectFilePreprocessor.java
index 033ab56d3d4..3e7b655589c 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/ProjectFilePreprocessor.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/ProjectFilePreprocessor.java
@@ -66,6 +66,7 @@ public class ProjectFilePreprocessor {
private final LanguageDetection languageDetection;
private final FilePreprocessor filePreprocessor;
private final ProjectExclusionFilters projectExclusionFilters;
+ private final HiddenFilesProjectData hiddenFilesProjectData;
private final SonarGlobalPropertiesFilter sonarGlobalPropertiesFilter;
@@ -79,7 +80,7 @@ public class ProjectFilePreprocessor {
public ProjectFilePreprocessor(AnalysisWarnings analysisWarnings, ScmConfiguration scmConfiguration, InputModuleHierarchy inputModuleHierarchy,
GlobalConfiguration globalConfig, GlobalServerSettings globalServerSettings, ProjectServerSettings projectServerSettings,
LanguageDetection languageDetection, FilePreprocessor filePreprocessor,
- ProjectExclusionFilters projectExclusionFilters, SonarGlobalPropertiesFilter sonarGlobalPropertiesFilter) {
+ ProjectExclusionFilters projectExclusionFilters, SonarGlobalPropertiesFilter sonarGlobalPropertiesFilter, HiddenFilesProjectData hiddenFilesProjectData) {
this.analysisWarnings = analysisWarnings;
this.scmConfiguration = scmConfiguration;
this.inputModuleHierarchy = inputModuleHierarchy;
@@ -92,6 +93,7 @@ public class ProjectFilePreprocessor {
this.sonarGlobalPropertiesFilter = sonarGlobalPropertiesFilter;
this.ignoreCommand = loadIgnoreCommand();
this.useScmExclusion = ignoreCommand != null;
+ this.hiddenFilesProjectData = hiddenFilesProjectData;
}
public void execute() {
@@ -109,7 +111,7 @@ public class ProjectFilePreprocessor {
int totalLanguagesDetected = languageDetection.getDetectedLanguages().size();
- progressReport.stop(String.format("%s detected in %s", pluralizeWithCount("language", totalLanguagesDetected),
+ progressReport.stopAndLogTotalTime(String.format("%s detected in %s", pluralizeWithCount("language", totalLanguagesDetected),
pluralizeWithCount("preprocessed file", totalFilesPreprocessed)));
int excludedFileByPatternCount = exclusionCounter.getByPatternsCount();
@@ -138,27 +140,31 @@ public class ProjectFilePreprocessor {
// Default to index basedir when no sources provided
List<Path> mainSourceDirsOrFiles = module.getSourceDirsOrFiles()
.orElseGet(() -> hasChildModules || hasTests ? emptyList() : singletonList(module.getBaseDir().toAbsolutePath()));
- List<Path> processedSources = processModuleSources(module, moduleExclusionFilters, mainSourceDirsOrFiles, InputFile.Type.MAIN,
+ List<Path> processedSources = processModuleSources(module, moduleConfig, moduleExclusionFilters, mainSourceDirsOrFiles, InputFile.Type.MAIN,
exclusionCounter);
mainSourcesByModule.put(module, processedSources);
totalFilesPreprocessed += processedSources.size();
module.getTestDirsOrFiles().ifPresent(tests -> {
- List<Path> processedTestSources = processModuleSources(module, moduleExclusionFilters, tests, InputFile.Type.TEST, exclusionCounter);
+ List<Path> processedTestSources = processModuleSources(module, moduleConfig, moduleExclusionFilters, tests, InputFile.Type.TEST, exclusionCounter);
testSourcesByModule.put(module, processedTestSources);
totalFilesPreprocessed += processedTestSources.size();
});
}
- private List<Path> processModuleSources(DefaultInputModule module, ModuleExclusionFilters moduleExclusionFilters, List<Path> sources,
+ private List<Path> processModuleSources(DefaultInputModule module, ModuleConfiguration moduleConfiguration, ModuleExclusionFilters moduleExclusionFilters, List<Path> sources,
InputFile.Type type, ExclusionCounter exclusionCounter) {
List<Path> processedFiles = new ArrayList<>();
try {
for (Path dirOrFile : sources) {
if (dirOrFile.toFile().isDirectory()) {
- processedFiles.addAll(processDirectory(module, moduleExclusionFilters, dirOrFile, type, exclusionCounter));
+ processedFiles.addAll(processDirectory(module, moduleConfiguration, moduleExclusionFilters, dirOrFile, type, exclusionCounter));
} else {
filePreprocessor.processFile(module, moduleExclusionFilters, dirOrFile, type, exclusionCounter, ignoreCommand)
- .ifPresent(processedFiles::add);
+ .ifPresentOrElse(
+ processedFiles::add,
+ // If the file is not processed, we don't need to save visibility data and can remove it
+ () -> hiddenFilesProjectData.getIsMarkedAsHiddenFileAndRemoveVisibilityInformation(dirOrFile, module)
+ );
}
}
} catch (IOException e) {
@@ -167,12 +173,17 @@ public class ProjectFilePreprocessor {
return processedFiles;
}
- private List<Path> processDirectory(DefaultInputModule module, ModuleExclusionFilters moduleExclusionFilters, Path path,
+ private List<Path> processDirectory(DefaultInputModule module, ModuleConfiguration moduleConfiguration, ModuleExclusionFilters moduleExclusionFilters, Path path,
InputFile.Type type, ExclusionCounter exclusionCounter) throws IOException {
List<Path> processedFiles = new ArrayList<>();
Files.walkFileTree(path.normalize(), Collections.singleton(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,
- new DirectoryFileVisitor(file -> filePreprocessor.processFile(module, moduleExclusionFilters, file, type, exclusionCounter,
- ignoreCommand).ifPresent(processedFiles::add), module, moduleExclusionFilters, inputModuleHierarchy, type));
+ new DirectoryFileVisitor(file -> filePreprocessor
+ .processFile(module, moduleExclusionFilters, file, type, exclusionCounter, ignoreCommand)
+ .ifPresentOrElse(
+ processedFiles::add,
+ // If the file is not processed, we don't need to save visibility data and can remove it
+ () -> hiddenFilesProjectData.getIsMarkedAsHiddenFileAndRemoveVisibilityInformation(file, module)),
+ module, moduleConfiguration, moduleExclusionFilters, inputModuleHierarchy, type, hiddenFilesProjectData));
return processedFiles;
}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/AbstractSensorWrapper.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/AbstractSensorWrapper.java
index a08380cf9d8..10d75a4b3c5 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/AbstractSensorWrapper.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/AbstractSensorWrapper.java
@@ -19,11 +19,11 @@
*/
package org.sonar.scanner.sensor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.sonar.api.batch.sensor.SensorContext;
import org.sonar.api.batch.sensor.internal.DefaultSensorDescriptor;
import org.sonar.api.scanner.sensor.ProjectSensor;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.sonar.scanner.scan.branch.BranchConfiguration;
import org.sonar.scanner.scan.branch.BranchType;
import org.sonar.scanner.scan.filesystem.MutableFileSystem;
@@ -60,7 +60,12 @@ public abstract class AbstractSensorWrapper<G extends ProjectSensor> {
if (sensorIsRestricted) {
LOGGER.info("Sensor {} is restricted to changed files only", descriptor.name());
}
+ boolean allowHiddenFileAnalysis = descriptor.isProcessesHiddenFiles();
+ if (allowHiddenFileAnalysis) {
+ LOGGER.debug("Sensor {} is allowed to analyze hidden files", descriptor.name());
+ }
fileSystem.setRestrictToChangedFiles(sensorIsRestricted);
+ fileSystem.setAllowHiddenFileAnalysis(allowHiddenFileAnalysis);
wrappedSensor.execute(context);
}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ModuleSensorContext.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ModuleSensorContext.java
index 5f28e7e283e..01b6c0c11cd 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ModuleSensorContext.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ModuleSensorContext.java
@@ -28,7 +28,6 @@ import org.sonar.api.batch.rule.ActiveRules;
import org.sonar.api.batch.sensor.cache.ReadCache;
import org.sonar.api.batch.sensor.cache.WriteCache;
import org.sonar.api.config.Configuration;
-import org.sonar.api.config.Settings;
import org.sonar.scanner.bootstrap.ScannerPluginRepository;
import org.sonar.scanner.cache.AnalysisCacheEnabled;
import org.sonar.scanner.scan.branch.BranchConfiguration;
@@ -38,11 +37,11 @@ public class ModuleSensorContext extends ProjectSensorContext {
private final InputModule module;
- public ModuleSensorContext(DefaultInputProject project, InputModule module, Configuration config, Settings mutableModuleSettings, FileSystem fs, ActiveRules activeRules,
+ public ModuleSensorContext(DefaultInputProject project, InputModule module, Configuration config, FileSystem fs, ActiveRules activeRules,
DefaultSensorStorage sensorStorage, SonarRuntime sonarRuntime, BranchConfiguration branchConfiguration,
WriteCache writeCache, ReadCache readCache, AnalysisCacheEnabled analysisCacheEnabled, UnchangedFilesHandler unchangedFilesHandler,
ExecutingSensorContext executingSensorContext, ScannerPluginRepository pluginRepository) {
- super(project, config, mutableModuleSettings, fs, activeRules, sensorStorage, sonarRuntime, branchConfiguration, writeCache, readCache, analysisCacheEnabled,
+ super(project, config, fs, activeRules, sensorStorage, sonarRuntime, branchConfiguration, writeCache, readCache, analysisCacheEnabled,
unchangedFilesHandler, executingSensorContext, pluginRepository);
this.module = module;
}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ProjectSensorContext.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ProjectSensorContext.java
index 6fb38fa4563..54c86750eaf 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ProjectSensorContext.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ProjectSensorContext.java
@@ -66,7 +66,6 @@ public class ProjectSensorContext implements SensorContext {
static final NoOpNewAnalysisError NO_OP_NEW_ANALYSIS_ERROR = new NoOpNewAnalysisError();
- private final Settings mutableSettings;
private final FileSystem fs;
private final ActiveRules activeRules;
private final DefaultSensorStorage sensorStorage;
@@ -81,15 +80,14 @@ public class ProjectSensorContext implements SensorContext {
private final ExecutingSensorContext executingSensorContext;
private final ScannerPluginRepository pluginRepo;
- public ProjectSensorContext(DefaultInputProject project, Configuration config, Settings mutableSettings, FileSystem fs,
- ActiveRules activeRules,
- DefaultSensorStorage sensorStorage, SonarRuntime sonarRuntime, BranchConfiguration branchConfiguration,
- WriteCache writeCache, ReadCache readCache,
- AnalysisCacheEnabled analysisCacheEnabled, UnchangedFilesHandler unchangedFilesHandler,
- ExecutingSensorContext executingSensorContext, ScannerPluginRepository pluginRepo) {
+ public ProjectSensorContext(DefaultInputProject project, Configuration config, FileSystem fs,
+ ActiveRules activeRules,
+ DefaultSensorStorage sensorStorage, SonarRuntime sonarRuntime, BranchConfiguration branchConfiguration,
+ WriteCache writeCache, ReadCache readCache,
+ AnalysisCacheEnabled analysisCacheEnabled, UnchangedFilesHandler unchangedFilesHandler,
+ ExecutingSensorContext executingSensorContext, ScannerPluginRepository pluginRepo) {
this.project = project;
this.config = config;
- this.mutableSettings = mutableSettings;
this.fs = fs;
this.activeRules = activeRules;
this.sensorStorage = sensorStorage;
@@ -105,7 +103,7 @@ public class ProjectSensorContext implements SensorContext {
@Override
public Settings settings() {
- return mutableSettings;
+ throw new UnsupportedOperationException("This method is not supported anymore");
}
@Override
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/CompositeBlameCommand.java b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/CompositeBlameCommand.java
index 0742740bba6..a481f4a54f4 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/CompositeBlameCommand.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/CompositeBlameCommand.java
@@ -22,7 +22,9 @@ package org.sonar.scm.git;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
+import java.time.Instant;
import java.util.ArrayList;
+import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -32,6 +34,7 @@ import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
+import javax.annotation.Nullable;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.diff.RawTextComparator;
@@ -238,7 +241,7 @@ public class CompositeBlameCommand extends BlameCommand {
break;
}
linesList.add(new BlameLine()
- .date(fileBlame.getCommitDates()[i])
+ .date(toDate(fileBlame.getCommitDates()[i]))
.revision(fileBlame.getCommitHashes()[i])
.author(fileBlame.getAuthorEmails()[i]));
}
@@ -251,4 +254,8 @@ public class CompositeBlameCommand extends BlameCommand {
}
}
+ private static @Nullable Date toDate(@Nullable Instant commitDate) {
+ return commitDate != null ? Date.from(commitDate) : null;
+ }
+
}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/NativeGitBlameCommand.java b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/NativeGitBlameCommand.java
index 569999a55ab..8f066727e21 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/NativeGitBlameCommand.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/NativeGitBlameCommand.java
@@ -25,6 +25,7 @@ import java.time.Instant;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
+import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -62,6 +63,7 @@ public class NativeGitBlameCommand {
private final System2 system;
private final ProcessWrapperFactory processWrapperFactory;
+ private final Consumer<String> stderrConsumer = line -> LOG.debug("[stderr] {}", line);
private String gitCommand;
@Autowired
@@ -85,7 +87,7 @@ public class NativeGitBlameCommand {
try {
this.gitCommand = locateDefaultGit();
MutableString stdOut = new MutableString();
- this.processWrapperFactory.create(null, l -> stdOut.string = l, gitCommand, "--version").execute();
+ this.processWrapperFactory.create(null, l -> stdOut.string = l, stderrConsumer, gitCommand, "--version").execute();
return stdOut.string != null && stdOut.string.startsWith("git version") && isCompatibleGitVersion(stdOut.string);
} catch (Exception e) {
LOG.debug("Failed to find git native client", e);
@@ -109,7 +111,7 @@ public class NativeGitBlameCommand {
// To avoid it we use where.exe to find git binary only in PATH.
LOG.debug("Looking for git command in the PATH using where.exe (Windows)");
List<String> whereCommandResult = new LinkedList<>();
- this.processWrapperFactory.create(null, whereCommandResult::add, "C:\\Windows\\System32\\where.exe", "$PATH:git.exe")
+ this.processWrapperFactory.create(null, whereCommandResult::add, stderrConsumer, "C:\\Windows\\System32\\where.exe", "$PATH:git.exe")
.execute();
if (!whereCommandResult.isEmpty()) {
@@ -125,6 +127,7 @@ public class NativeGitBlameCommand {
var processWrapper = this.processWrapperFactory.create(
baseDir,
outputProcessor::process,
+ stderrConsumer,
gitCommand,
GIT_DIR_FLAG, String.format(GIT_DIR_ARGUMENT, baseDir), GIT_DIR_FORCE_FLAG, baseDir.toString(),
BLAME_COMMAND,
diff --git a/sonar-scanner-engine/src/main/resources/logback.xml b/sonar-scanner-engine/src/main/resources/logback.xml
index ccd0dfe09b9..ddc2805f08a 100644
--- a/sonar-scanner-engine/src/main/resources/logback.xml
+++ b/sonar-scanner-engine/src/main/resources/logback.xml
@@ -1,17 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration>
-<!-- This logback configuration is used when the scanner engine is bootstrapped using the SonarScannerCli class. -->
+<!-- This logback configuration is used when the scanner engine is bootstrapped using the ScannerMain class. -->
<configuration scan="false">
- <import class="ch.qos.logback.core.ConsoleAppender"/>
-
- <appender name="STDOUT" class="ConsoleAppender">
- <encoder class="org.sonar.scanner.bootstrap.ScannerLogbackEncoder"/>
- </appender>
-
- <root level="info">
- <appender-ref ref="STDOUT"/>
- </root>
<!-- BeanUtils generate too many DEBUG logs when sonar.verbose is set -->
<logger name="org.apache.commons.beanutils.converters" level="WARN"/>
@@ -31,4 +22,4 @@
<logger name="nl.altindag.ssl.util.CertificateUtils" level="INFO"/>
-</configuration> \ No newline at end of file
+</configuration>
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/RulesSeverityDetectorTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/RulesSeverityDetectorTest.java
index 5091dcf5a3a..f5d88016944 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/RulesSeverityDetectorTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/RulesSeverityDetectorTest.java
@@ -22,12 +22,16 @@ package org.sonar.scanner.externalissue.sarif;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import javax.annotation.Nullable;
import org.assertj.core.api.Assertions;
import org.assertj.core.groups.Tuple;
-import org.junit.Before;
-import org.junit.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.slf4j.event.Level;
-import org.sonar.api.testfixtures.log.LogTester;
+import org.sonar.api.testfixtures.log.LogTesterJUnit5;
import org.sonar.sarif.pojo.ReportingConfiguration;
import org.sonar.sarif.pojo.ReportingDescriptor;
import org.sonar.sarif.pojo.Result;
@@ -44,12 +48,12 @@ import static org.sonar.sarif.pojo.Result.Level.WARNING;
import static org.sonar.scanner.externalissue.sarif.ResultMapper.DEFAULT_IMPACT_SEVERITY;
import static org.sonar.scanner.externalissue.sarif.ResultMapper.DEFAULT_SEVERITY;
-public class RulesSeverityDetectorTest {
+class RulesSeverityDetectorTest {
private static final String DRIVER_NAME = "Test";
private static final String RULE_ID = "RULE_ID";
- @org.junit.Rule
- public LogTester logTester = new LogTester().setLevel(Level.TRACE);
+ @RegisterExtension
+ private final LogTesterJUnit5 logTester = new LogTesterJUnit5();
private final Run run = mock(Run.class);
private final ReportingDescriptor rule = mock(ReportingDescriptor.class);
@@ -59,8 +63,8 @@ public class RulesSeverityDetectorTest {
private final ToolComponent extension = mock(ToolComponent.class);
private final ReportingConfiguration defaultConfiguration = mock(ReportingConfiguration.class);
- @Before
- public void setUp() {
+ @BeforeEach
+ void setUp() {
when(run.getResults()).thenReturn(List.of(result));
when(run.getTool()).thenReturn(tool);
when(tool.getDriver()).thenReturn(driver);
@@ -68,8 +72,8 @@ public class RulesSeverityDetectorTest {
// We keep this test for backward compatibility until we remove the deprecated severity
@Test
- public void detectRulesSeverities_detectsCorrectlyResultDefinedRuleSeverities() {
- Run run = mockResultDefinedRuleSeverities();
+ void detectRulesSeverities_detectsCorrectlyResultDefinedRuleSeverities() {
+ mockResultDefinedRuleSeverities();
Map<String, Result.Level> rulesSeveritiesByRuleId = RulesSeverityDetector.detectRulesSeverities(run, DRIVER_NAME);
@@ -78,8 +82,8 @@ public class RulesSeverityDetectorTest {
}
@Test
- public void detectRulesSeveritiesForNewTaxonomy_shouldReturnsEmptyMapAndLogsWarning_whenOnlyResultDefinedRuleSeverities() {
- Run run = mockResultDefinedRuleSeverities();
+ void detectRulesSeveritiesForNewTaxonomy_shouldReturnsEmptyMapAndLogsWarning_whenOnlyResultDefinedRuleSeverities() {
+ mockResultDefinedRuleSeverities();
Map<String, Result.Level> rulesSeveritiesByRuleId = RulesSeverityDetector.detectRulesSeveritiesForNewTaxonomy(run, DRIVER_NAME);
@@ -88,8 +92,8 @@ public class RulesSeverityDetectorTest {
}
@Test
- public void detectRulesSeverities_detectsCorrectlyDriverDefinedRuleSeverities() {
- Run run = mockDriverDefinedRuleSeverities();
+ void detectRulesSeverities_detectsCorrectlyDriverDefinedRuleSeverities() {
+ mockDriverDefinedRuleSeverities();
Map<String, Result.Level> rulesSeveritiesByRuleId = RulesSeverityDetector.detectRulesSeveritiesForNewTaxonomy(run, DRIVER_NAME);
@@ -103,9 +107,13 @@ public class RulesSeverityDetectorTest {
assertDetectedRuleSeverities(rulesSeveritiesByRuleId, tuple(RULE_ID, WARNING));
}
- @Test
- public void detectRulesSeverities_detectsCorrectlyExtensionsDefinedRuleSeverities() {
- Run run = mockExtensionsDefinedRuleSeverities();
+
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ void detectRulesSeverities_detectsCorrectlyExtensionsDefinedRuleSeverities(@Nullable Set<ReportingDescriptor> rules) {
+ when(driver.getRules()).thenReturn(rules);
+ mockExtensionsDefinedRuleSeverities();
Map<String, Result.Level> rulesSeveritiesByRuleId = RulesSeverityDetector.detectRulesSeveritiesForNewTaxonomy(run, DRIVER_NAME);
@@ -120,8 +128,8 @@ public class RulesSeverityDetectorTest {
}
@Test
- public void detectRulesSeverities_returnsEmptyMapAndLogsWarning_whenUnableToDetectSeverities() {
- Run run = mockUnsupportedRuleSeveritiesDefinition();
+ void detectRulesSeverities_returnsEmptyMapAndLogsWarning_whenUnableToDetectSeverities() {
+ mockUnsupportedRuleSeveritiesDefinition();
Map<String, Result.Level> rulesSeveritiesByRuleId = RulesSeverityDetector.detectRulesSeveritiesForNewTaxonomy(run, DRIVER_NAME);
@@ -135,38 +143,33 @@ public class RulesSeverityDetectorTest {
assertDetectedRuleSeverities(rulesSeveritiesByRuleId);
}
- private Run mockResultDefinedRuleSeverities() {
+ private void mockResultDefinedRuleSeverities() {
when(run.getResults()).thenReturn(List.of(result));
when(result.getLevel()).thenReturn(WARNING);
when(result.getRuleId()).thenReturn(RULE_ID);
- return run;
}
- private Run mockDriverDefinedRuleSeverities() {
+ private void mockDriverDefinedRuleSeverities() {
when(driver.getRules()).thenReturn(Set.of(rule));
when(rule.getId()).thenReturn(RULE_ID);
when(rule.getDefaultConfiguration()).thenReturn(defaultConfiguration);
when(defaultConfiguration.getLevel()).thenReturn(ReportingConfiguration.Level.WARNING);
- return run;
}
- private Run mockExtensionsDefinedRuleSeverities() {
- when(driver.getRules()).thenReturn(Set.of());
+ private void mockExtensionsDefinedRuleSeverities() {
when(tool.getExtensions()).thenReturn(Set.of(extension));
when(extension.getRules()).thenReturn(Set.of(rule));
when(rule.getId()).thenReturn(RULE_ID);
when(rule.getDefaultConfiguration()).thenReturn(defaultConfiguration);
when(defaultConfiguration.getLevel()).thenReturn(ReportingConfiguration.Level.WARNING);
- return run;
}
- private Run mockUnsupportedRuleSeveritiesDefinition() {
+ private void mockUnsupportedRuleSeveritiesDefinition() {
when(run.getTool()).thenReturn(tool);
when(tool.getDriver()).thenReturn(driver);
when(driver.getRules()).thenReturn(Set.of());
when(tool.getExtensions()).thenReturn(Set.of(extension));
when(extension.getRules()).thenReturn(Set.of());
- return run;
}
private void assertNoLogs() {
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/RunMapperTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/RunMapperTest.java
index 90ddfadd9f3..164787f8cde 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/RunMapperTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/RunMapperTest.java
@@ -22,19 +22,19 @@ package org.sonar.scanner.externalissue.sarif;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
-import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.event.Level;
import org.sonar.api.batch.sensor.issue.NewExternalIssue;
import org.sonar.api.batch.sensor.rule.NewAdHocRule;
-import org.sonar.api.testfixtures.log.LogTester;
+import org.sonar.api.testfixtures.log.LogTesterJUnit5;
import org.sonar.sarif.pojo.ReportingDescriptor;
import org.sonar.sarif.pojo.Result;
import org.sonar.sarif.pojo.Run;
@@ -44,13 +44,14 @@ import org.sonar.scanner.externalissue.sarif.RunMapper.RunMapperResult;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNoException;
+import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import static org.sonar.sarif.pojo.Result.Level.WARNING;
-@RunWith(MockitoJUnitRunner.class)
-public class RunMapperTest {
+@ExtendWith(MockitoExtension.class)
+class RunMapperTest {
private static final String TEST_DRIVER = "Test driver";
public static final String RULE_ID = "ruleId";
@@ -66,21 +67,21 @@ public class RunMapperTest {
@Mock
private ReportingDescriptor rule;
- @Rule
- public LogTester logTester = new LogTester();
+ @RegisterExtension
+ public LogTesterJUnit5 logTester = new LogTesterJUnit5();
@InjectMocks
private RunMapper runMapper;
- @Before
- public void setUp() {
- when(run.getTool().getDriver().getName()).thenReturn(TEST_DRIVER);
- when(run.getTool().getExtensions()).thenReturn(null);
- when(rule.getId()).thenReturn(RULE_ID);
+ @BeforeEach
+ void setUp() {
+ lenient().when(run.getTool().getDriver().getName()).thenReturn(TEST_DRIVER);
+ lenient().when(run.getTool().getExtensions()).thenReturn(null);
+ lenient().when(rule.getId()).thenReturn(RULE_ID);
}
@Test
- public void mapRun_shouldMapExternalIssues() {
+ void mapRun_shouldMapExternalIssues() {
Result result1 = mock(Result.class);
Result result2 = mock(Result.class);
when(run.getResults()).thenReturn(List.of(result1, result2));
@@ -99,7 +100,7 @@ public class RunMapperTest {
}
@Test
- public void mapRun_shouldMapExternalRules_whenDriverHasRulesAndNoExtensions() {
+ void mapRun_shouldMapExternalRules_whenDriverHasRulesAndNoExtensions() {
when(run.getTool().getDriver().getRules()).thenReturn(Set.of(rule));
NewAdHocRule externalRule = mockMappedExternalRule();
@@ -115,7 +116,7 @@ public class RunMapperTest {
}
@Test
- public void mapRun_shouldMapExternalRules_whenRulesInExtensions() {
+ void mapRun_shouldMapExternalRules_whenRulesInExtensions() {
when(run.getTool().getDriver().getRules()).thenReturn(Set.of());
ToolComponent extension = mock(ToolComponent.class);
when(extension.getRules()).thenReturn(Set.of(rule));
@@ -134,7 +135,7 @@ public class RunMapperTest {
}
@Test
- public void mapRun_shouldNotFail_whenExtensionsDontHaveRules() {
+ void mapRun_shouldNotFail_whenExtensionsDontHaveRules() {
when(run.getTool().getDriver().getRules()).thenReturn(Set.of(rule));
ToolComponent extension = mock(ToolComponent.class);
when(extension.getRules()).thenReturn(null);
@@ -149,7 +150,7 @@ public class RunMapperTest {
}
@Test
- public void mapRun_shouldNotFail_whenExtensionsHaveEmptyRules() {
+ void mapRun_shouldNotFail_whenExtensionsHaveEmptyRules() {
when(run.getTool().getDriver().getRules()).thenReturn(Set.of(rule));
ToolComponent extension = mock(ToolComponent.class);
when(extension.getRules()).thenReturn(Set.of());
@@ -164,7 +165,7 @@ public class RunMapperTest {
}
@Test
- public void mapRun_ifRunIsEmpty_returnsEmptyList() {
+ void mapRun_ifRunIsEmpty_returnsEmptyList() {
when(run.getResults()).thenReturn(List.of());
RunMapperResult runMapperResult = runMapper.mapRun(run);
@@ -173,7 +174,7 @@ public class RunMapperTest {
}
@Test
- public void mapRun_ifExceptionThrownByResultMapper_logsThemAndContinueProcessing() {
+ void mapRun_ifExceptionThrownByResultMapper_logsThemAndContinueProcessing() {
Result result1 = mock(Result.class);
Result result2 = mock(Result.class);
when(run.getResults()).thenReturn(List.of(result1, result2));
@@ -194,7 +195,7 @@ public class RunMapperTest {
}
@Test
- public void mapRun_failsIfToolNotSet() {
+ void mapRun_failsIfToolNotSet() {
when(run.getTool()).thenReturn(null);
assertThatIllegalArgumentException()
@@ -203,7 +204,7 @@ public class RunMapperTest {
}
@Test
- public void mapRun_failsIfDriverNotSet() {
+ void mapRun_failsIfDriverNotSet() {
when(run.getTool().getDriver()).thenReturn(null);
assertThatIllegalArgumentException()
@@ -212,7 +213,7 @@ public class RunMapperTest {
}
@Test
- public void mapRun_failsIfDriverNameIsNotSet() {
+ void mapRun_failsIfDriverNameIsNotSet() {
when(run.getTool().getDriver().getName()).thenReturn(null);
assertThatIllegalArgumentException()
@@ -220,6 +221,25 @@ public class RunMapperTest {
.withMessage("The run does not have a tool driver name defined.");
}
+ @Test
+ void mapRun_shouldNotFail_whenDriverRulesNullAndExtensionsRulesNotNull() {
+ when(run.getTool().getDriver().getRules()).thenReturn(null);
+ ToolComponent extension = mock(ToolComponent.class);
+ when(extension.getRules()).thenReturn(Set.of(rule));
+ when(run.getTool().getExtensions()).thenReturn(Set.of(extension));
+ NewAdHocRule expectedRule = mock(NewAdHocRule.class);
+ when(ruleMapper.mapRule(rule, TEST_DRIVER, WARNING, WARNING)).thenReturn(expectedRule);
+
+ try (MockedStatic<RulesSeverityDetector> detector = mockStatic(RulesSeverityDetector.class)) {
+ detector.when(() -> RulesSeverityDetector.detectRulesSeverities(run, TEST_DRIVER)).thenReturn(Map.of(RULE_ID, WARNING));
+ detector.when(() -> RulesSeverityDetector.detectRulesSeveritiesForNewTaxonomy(run, TEST_DRIVER)).thenReturn(Map.of(RULE_ID, WARNING));
+
+ RunMapperResult runMapperResult = runMapper.mapRun(run);
+ assertThat(runMapperResult.getNewAdHocRules()).hasSize(1);
+ assertThat(runMapperResult.getNewAdHocRules().get(0)).isEqualTo(expectedRule);
+ }
+ }
+
private NewExternalIssue mockMappedExternalIssue(Result result) {
NewExternalIssue externalIssue = mock(NewExternalIssue.class);
when(result.getRuleId()).thenReturn(RULE_ID);
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/CliCacheServiceTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/CliCacheServiceTest.java
index 9193157e213..6615ba4e4e4 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/CliCacheServiceTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/CliCacheServiceTest.java
@@ -29,6 +29,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.SystemUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -161,7 +162,7 @@ class CliCacheServiceTest {
WsTestUtil.mockException(scannerWsClient, e);
assertThatThrownBy(underTest::cacheCli).isInstanceOf(IllegalStateException.class)
- .hasMessageContaining("Unable to load CLI metadata");
+ .hasMessageContaining("http error");
verify(telemetryCache).put("scanner.sca.get.cli.success", "false");
}
@@ -187,14 +188,18 @@ class CliCacheServiceTest {
FileUtils.writeStringToFile(existingFile, fileContent, Charset.defaultCharset());
assertThat(existingFile).exists();
- assertThat(existingFile.canExecute()).isFalse();
+ if (!SystemUtils.IS_OS_WINDOWS) {
+ assertThat(existingFile.canExecute()).isFalse();
+ }
assertThat(FileUtils.readFileToString(existingFile, Charset.defaultCharset())).isEqualTo(fileContent);
underTest.cacheCli();
WsTestUtil.verifyCall(scannerWsClient, CLI_WS_URL);
assertThat(existingFile).exists();
- assertThat(existingFile.canExecute()).isFalse();
+ if (!SystemUtils.IS_OS_WINDOWS) {
+ assertThat(existingFile.canExecute()).isFalse();
+ }
assertThat(FileUtils.readFileToString(existingFile, Charset.defaultCharset())).isEqualTo(fileContent);
verify(telemetryCache).put("scanner.sca.get.cli.cache.hit", "true");
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/CliServiceTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/CliServiceTest.java
index 597fafa833c..33cf6c146e6 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/CliServiceTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/CliServiceTest.java
@@ -27,7 +27,6 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
-import java.util.Optional;
import org.apache.commons.lang3.SystemUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -36,6 +35,7 @@ import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.MockedStatic;
import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.fs.internal.DefaultInputModule;
import org.sonar.api.batch.scm.ScmProvider;
import org.sonar.api.platform.Server;
@@ -44,6 +44,7 @@ import org.sonar.api.utils.System2;
import org.sonar.core.util.ProcessWrapperFactory;
import org.sonar.scanner.config.DefaultConfiguration;
import org.sonar.scanner.repository.TelemetryCache;
+import org.sonar.scanner.scan.filesystem.ProjectExclusionFilters;
import org.sonar.scanner.scm.ScmConfiguration;
import org.sonar.scm.git.GitScmProvider;
import org.sonar.scm.git.JGitUtils;
@@ -69,6 +70,7 @@ class CliServiceTest {
ProcessWrapperFactory processWrapperFactory = mock(ProcessWrapperFactory.class, CALLS_REAL_METHODS);
private MockedStatic<JGitUtils> jGitUtilsMock;
DefaultConfiguration configuration = mock(DefaultConfiguration.class);
+ ProjectExclusionFilters projectExclusionFilters = mock(ProjectExclusionFilters.class);
private CliService underTest;
@@ -86,9 +88,11 @@ class CliServiceTest {
jGitUtilsMock.when(() -> JGitUtils.getAllIgnoredPaths(any(Path.class))).thenReturn(List.of("ignored.txt"));
when(server.getVersion()).thenReturn("1.0.0");
logTester.setLevel(INFO);
- when(configuration.getBoolean("sonar.sca.debug")).thenReturn(Optional.of(true));
+ when(projectExclusionFilters.getExclusionsConfig(InputFile.Type.MAIN)).thenReturn(new String[0]);
+ when(configuration.getStringArray(CliService.SCA_EXCLUSIONS_KEY)).thenReturn(new String[0]);
+ when(configuration.getStringArray(CliService.LEGACY_SCA_EXCLUSIONS_KEY)).thenReturn(new String[0]);
- underTest = new CliService(processWrapperFactory, telemetryCache, System2.INSTANCE, server, scmConfiguration);
+ underTest = new CliService(processWrapperFactory, telemetryCache, System2.INSTANCE, server, scmConfiguration, projectExclusionFilters);
}
@AfterEach
@@ -99,57 +103,55 @@ class CliServiceTest {
}
@Test
- void generateZip_shouldCallProcessCorrectly_andRegisterTelemetry() throws IOException, URISyntaxException {
+ void generateManifestsArchive_shouldCallProcessCorrectly_andRegisterTelemetry() throws IOException, URISyntaxException {
assertThat(rootModuleDir.resolve("test_file").toFile().createNewFile()).isTrue();
- when(configuration.getProperties()).thenReturn(Map.of("sonar.sca.recursiveManifestSearch", "true", CliService.EXCLUDED_MANIFESTS_PROP_KEY, "foo,bar,baz/**"));
- when(configuration.get("sonar.sca.recursiveManifestSearch")).thenReturn(Optional.of("true"));
- when(configuration.getStringArray(CliService.EXCLUDED_MANIFESTS_PROP_KEY)).thenReturn(new String[] {"foo", "bar", "baz/**"});
+ when(configuration.getProperties()).thenReturn(Map.of(CliService.SCA_EXCLUSIONS_KEY, "foo,bar,baz/**"));
+ when(configuration.getStringArray(CliService.SCA_EXCLUSIONS_KEY)).thenReturn(new String[] {"foo", "bar", "baz/**"});
- File producedZip = underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
+ File producedArchive = underTest.generateManifestsArchive(rootInputModule, scriptDir(), configuration);
- assertThat(producedZip).exists();
+ assertThat(producedArchive).exists();
var expectedArguments = List.of(
"projects",
"save-lockfiles",
- "--zip",
- "--zip-filename",
- rootInputModule.getWorkDir().resolve("dependency-files.zip").toString(),
+ "--xz",
+ "--xz-filename",
+ rootInputModule.getWorkDir().resolve("dependency-files.tar.xz").toString(),
"--directory",
rootInputModule.getBaseDir().toString(),
+ "--recursive",
"--exclude",
- "foo,bar,baz/**,ignored.txt,.scannerwork/**",
- "--debug");
+ "foo,bar,baz/**,ignored.txt,.scannerwork/**");
assertThat(logTester.logs(INFO))
.contains("Arguments Passed In: " + String.join(" ", expectedArguments))
.contains("TIDELIFT_SKIP_UPDATE_CHECK=1")
.contains("TIDELIFT_ALLOW_MANIFEST_FAILURES=1")
- .contains("TIDELIFT_RECURSIVE_MANIFEST_SEARCH=true")
- .contains("Generated manifests zip file: " + producedZip.getName());
+ .contains("Generated manifests archive file: " + producedArchive.getName());
assertThat(telemetryCache.getAll()).containsKey("scanner.sca.execution.cli.duration").isNotNull();
assertThat(telemetryCache.getAll()).containsEntry("scanner.sca.execution.cli.success", "true");
}
@Test
- void generateZip_whenDebugLogLevelAndScaDebugNotEnabled_shouldWriteDebugLogsToDebugStream() throws IOException, URISyntaxException {
+ void generateManifestsArchive_whenDebugLogLevelAndScaDebugNotEnabled_shouldWriteDebugLogsToDebugStream() throws IOException, URISyntaxException {
logTester.setLevel(DEBUG);
- when(configuration.getBoolean("sonar.sca.debug")).thenReturn(Optional.of(false));
assertThat(rootModuleDir.resolve("test_file").toFile().createNewFile()).isTrue();
- underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
+ underTest.generateManifestsArchive(rootInputModule, scriptDir(), configuration);
var expectedArguments = List.of(
"projects",
"save-lockfiles",
- "--zip",
- "--zip-filename",
- rootInputModule.getWorkDir().resolve("dependency-files.zip").toString(),
+ "--xz",
+ "--xz-filename",
+ rootInputModule.getWorkDir().resolve("dependency-files.tar.xz").toString(),
"--directory",
rootInputModule.getBaseDir().toString(),
+ "--recursive",
"--exclude",
"ignored.txt,.scannerwork/**",
"--debug");
@@ -159,32 +161,30 @@ class CliServiceTest {
}
@Test
- void generateZip_whenScaDebugEnabled_shouldWriteDebugLogsToInfoStream() throws IOException, URISyntaxException {
- when(configuration.getBoolean("sonar.sca.debug")).thenReturn(Optional.of(true));
-
+ void generateManifestsArchive_whenScaDebugEnabled_shouldWriteDebugLogsToInfoStream() throws IOException, URISyntaxException {
assertThat(rootModuleDir.resolve("test_file").toFile().createNewFile()).isTrue();
- underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
+ underTest.generateManifestsArchive(rootInputModule, scriptDir(), configuration);
var expectedArguments = List.of(
"projects",
"save-lockfiles",
- "--zip",
- "--zip-filename",
- rootInputModule.getWorkDir().resolve("dependency-files.zip").toString(),
+ "--xz",
+ "--xz-filename",
+ rootInputModule.getWorkDir().resolve("dependency-files.tar.xz").toString(),
"--directory",
rootInputModule.getBaseDir().toString(),
+ "--recursive",
"--exclude",
- "ignored.txt,.scannerwork/**",
- "--debug");
+ "ignored.txt,.scannerwork/**");
assertThat(logTester.logs(INFO))
.contains("Arguments Passed In: " + String.join(" ", expectedArguments));
}
@Test
- void generateZip_shouldSendSQEnvVars() throws IOException, URISyntaxException {
- underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
+ void generateManifestsArchive_shouldSendSQEnvVars() throws IOException, URISyntaxException {
+ underTest.generateManifestsArchive(rootInputModule, scriptDir(), configuration);
assertThat(logTester.logs(INFO))
.contains("TIDELIFT_CLI_INSIDE_SCANNER_ENGINE=1")
@@ -192,20 +192,20 @@ class CliServiceTest {
}
@Test
- void generateZip_includesIgnoredPathsFromGitProvider() throws Exception {
- underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
+ void generateManifestsArchive_includesIgnoredPathsFromGitProvider() throws Exception {
+ underTest.generateManifestsArchive(rootInputModule, scriptDir(), configuration);
var expectedArguments = List.of(
"projects",
"save-lockfiles",
- "--zip",
- "--zip-filename",
- rootInputModule.getWorkDir().resolve("dependency-files.zip").toString(),
+ "--xz",
+ "--xz-filename",
+ rootInputModule.getWorkDir().resolve("dependency-files.tar.xz").toString(),
"--directory",
rootInputModule.getBaseDir().toString(),
+ "--recursive",
"--exclude",
- "ignored.txt,.scannerwork/**",
- "--debug");
+ "ignored.txt,.scannerwork/**");
assertThat(logTester.logs(INFO))
.contains("Arguments Passed In: " + String.join(" ", expectedArguments))
@@ -217,87 +217,120 @@ class CliServiceTest {
}
@Test
- void generateZip_withNoScm_doesNotIncludeScmIgnoredPaths() throws Exception {
+ void generateManifestsArchive_withNoScm_doesNotIncludeScmIgnoredPaths() throws Exception {
when(scmConfiguration.provider()).thenReturn(null);
- underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
+ underTest.generateManifestsArchive(rootInputModule, scriptDir(), configuration);
String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get();
- assertThat(capturedArgs).contains("--exclude .scannerwork/** --debug");
+ assertThat(capturedArgs).contains("--exclude .scannerwork/**");
}
@Test
- void generateZip_withNonGit_doesNotIncludeScmIgnoredPaths() throws Exception {
+ void generateManifestsArchive_withNonGit_doesNotIncludeScmIgnoredPaths() throws Exception {
when(scmProvider.key()).thenReturn("notgit");
- underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
+ underTest.generateManifestsArchive(rootInputModule, scriptDir(), configuration);
String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get();
- assertThat(capturedArgs).contains("--exclude .scannerwork/** --debug");
+ assertThat(capturedArgs).contains("--exclude .scannerwork/**");
}
@Test
- void generateZip_withExclusionDisabled_doesNotIncludeScmIgnoredPaths() throws Exception {
+ void generateManifestsArchive_withScmExclusionDisabled_doesNotIncludeScmIgnoredPaths() throws Exception {
when(scmConfiguration.isExclusionDisabled()).thenReturn(true);
- underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
+ underTest.generateManifestsArchive(rootInputModule, scriptDir(), configuration);
String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get();
- assertThat(capturedArgs).contains("--exclude .scannerwork/** --debug");
+ assertThat(capturedArgs).contains("--exclude .scannerwork/**");
}
@Test
- void generateZip_withNoScmIgnores_doesNotIncludeScmIgnoredPaths() throws Exception {
+ void generateManifestsArchive_withNoScmIgnores_doesNotIncludeScmIgnoredPaths() throws Exception {
jGitUtilsMock.when(() -> JGitUtils.getAllIgnoredPaths(any(Path.class))).thenReturn(List.of());
- underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
+ underTest.generateManifestsArchive(rootInputModule, scriptDir(), configuration);
String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get();
- assertThat(capturedArgs).contains("--exclude .scannerwork/** --debug");
+ assertThat(capturedArgs).contains("--exclude .scannerwork/**");
}
@Test
- void generateZip_withExistingExcludedManifests_appendsScmIgnoredPaths() throws Exception {
- when(configuration.getStringArray(CliService.EXCLUDED_MANIFESTS_PROP_KEY)).thenReturn(new String[] {"**/test/**"});
+ void generateManifestsArchive_withExcludedManifests_appendsScmIgnoredPaths() throws Exception {
+ when(configuration.getStringArray(CliService.SCA_EXCLUSIONS_KEY)).thenReturn(new String[] {"**/test/**"});
- underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
+ underTest.generateManifestsArchive(rootInputModule, scriptDir(), configuration);
String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get();
assertThat(capturedArgs).contains("--exclude **/test/**,ignored.txt,.scannerwork/**");
}
@Test
- void generateZip_withExcludedManifestsSettingContainingBadCharacters_handlesTheBadCharacters() throws Exception {
- when(configuration.getStringArray(CliService.EXCLUDED_MANIFESTS_PROP_KEY)).thenReturn(new String[] {
- "**/test/**", "**/path with spaces/**", "**/path,with,commas/**", "**/path'with'quotes/**", "**/path\"with\"double\"quotes/**"});
+ void generateManifestsArchive_withExcludedManifestsContainingBadCharacters_handlesTheBadCharacters() throws Exception {
+ when(configuration.getStringArray(CliService.SCA_EXCLUSIONS_KEY)).thenReturn(new String[] {
+ "**/test/**", "**/path with spaces/**", "**/path'with'quotes/**", "**/path\"with\"double\"quotes/**"});
- underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
+ underTest.generateManifestsArchive(rootInputModule, scriptDir(), configuration);
String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get();
String expectedExcludeFlag = """
- --exclude **/test/**,**/path with spaces/**,"**/path,with,commas/**",**/path'with'quotes/**,"**/path""with""double""quotes/**",ignored.txt
+ --exclude **/test/**,**/path with spaces/**,**/path'with'quotes/**,"**/path""with""double""quotes/**",ignored.txt
""".strip();
+ if (SystemUtils.IS_OS_WINDOWS) {
+ expectedExcludeFlag = """
+ --exclude "**/test/**,**/path with spaces/**,**/path'with'quotes/**,"**/path""with""double""quotes/**",ignored.txt
+ """.strip();
+ }
assertThat(capturedArgs).contains(expectedExcludeFlag);
}
@Test
- void generateZip_withScmIgnoresContainingBadCharacters_handlesTheBadCharacters() throws Exception {
+ void generateManifestsArchive_withExcludedManifestsContainingDupes_dedupes() throws Exception {
+ when(configuration.getStringArray(CliService.SCA_EXCLUSIONS_KEY)).thenReturn(new String[] {"**/test1/**", "**/test2/**", "**/test1/**"});
+ when(configuration.getStringArray(CliService.LEGACY_SCA_EXCLUSIONS_KEY)).thenReturn(new String[] {"**/test1/**", "**/test3/**"});
+
+ underTest.generateManifestsArchive(rootInputModule, scriptDir(), configuration);
+
+ String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get();
+ assertThat(capturedArgs).contains("--exclude **/test1/**,**/test2/**,**/test3/**,ignored.txt,.scannerwork/**");
+ }
+
+ @Test
+ void generateManifestsArchive_withExcludedManifestsAndSonarExcludesContainingDupes_mergesAndDedupes() throws Exception {
+ when(projectExclusionFilters.getExclusionsConfig(InputFile.Type.MAIN)).thenReturn(new String[] {"**/test1/**", "**/test4/**"});
+ when(configuration.getStringArray(CliService.SCA_EXCLUSIONS_KEY)).thenReturn(new String[] {"**/test1/**", "**/test2/**", "**/test1/**"});
+ when(configuration.getStringArray(CliService.LEGACY_SCA_EXCLUSIONS_KEY)).thenReturn(new String[] {"**/test1/**", "**/test3/**"});
+
+ underTest.generateManifestsArchive(rootInputModule, scriptDir(), configuration);
+
+ String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get();
+ assertThat(capturedArgs).contains("--exclude **/test1/**,**/test4/**,**/test2/**,**/test3/**,ignored.txt,.scannerwork/**");
+ }
+
+ @Test
+ void generateManifestsArchive_withScmIgnoresContainingBadCharacters_handlesTheBadCharacters() throws Exception {
jGitUtilsMock.when(() -> JGitUtils.getAllIgnoredPaths(any(Path.class)))
- .thenReturn(List.of("**/test/**", "**/path with spaces/**", "**/path,with,commas/**", "**/path'with'quotes/**", "**/path\"with\"double\"quotes/**"));
+ .thenReturn(List.of("**/test/**", "**/path with spaces/**", "**/path'with'quotes/**", "**/path\"with\"double\"quotes/**"));
- underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
+ underTest.generateManifestsArchive(rootInputModule, scriptDir(), configuration);
String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get();
String expectedExcludeFlag = """
- --exclude **/test/**,**/path with spaces/**,"**/path,with,commas/**",**/path'with'quotes/**,"**/path""with""double""quotes/**"
+ --exclude **/test/**,**/path with spaces/**,**/path'with'quotes/**,"**/path""with""double""quotes/**"
""".strip();
+ if (SystemUtils.IS_OS_WINDOWS) {
+ expectedExcludeFlag = """
+ --exclude "**/test/**,**/path with spaces/**,**/path'with'quotes/**,"**/path""with""double""quotes/**"
+ """.strip();
+ }
assertThat(capturedArgs).contains(expectedExcludeFlag);
}
@Test
- void generateZip_withIgnoredDirectories_GlobifiesDirectories() throws Exception {
+ void generateManifestsArchive_withIgnoredDirectories_GlobifiesDirectories() throws Exception {
String ignoredDirectory = "directory1";
Files.createDirectories(rootModuleDir.resolve(ignoredDirectory));
String ignoredFile = "directory2/file.txt";
@@ -306,22 +339,22 @@ class CliServiceTest {
Files.createFile(ignoredFilePath);
jGitUtilsMock.when(() -> JGitUtils.getAllIgnoredPaths(any(Path.class))).thenReturn(List.of(ignoredDirectory, ignoredFile));
- underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
+ underTest.generateManifestsArchive(rootInputModule, scriptDir(), configuration);
String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get();
assertThat(capturedArgs).contains("--exclude directory1/**,directory2/file.txt");
}
@Test
- void generateZip_withExternalWorkDir_DoesNotExcludeWorkingDir() throws URISyntaxException, IOException {
+ void generateManifestsArchive_withExternalWorkDir_DoesNotExcludeWorkingDir() throws URISyntaxException, IOException {
Path externalWorkDir = Files.createTempDirectory("externalWorkDir");
try {
rootInputModule = new DefaultInputModule(ProjectDefinition.create().setBaseDir(rootModuleDir.toFile()).setWorkDir(externalWorkDir.toFile()));
- underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
+ underTest.generateManifestsArchive(rootInputModule, scriptDir(), configuration);
String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get();
// externalWorkDir is not present in the exclude flag
- assertThat(capturedArgs).contains("--exclude ignored.txt --debug");
+ assertThat(capturedArgs).contains("--exclude ignored.txt");
} finally {
externalWorkDir.toFile().delete();
}
@@ -330,7 +363,7 @@ class CliServiceTest {
private URL scriptUrl() {
// There is a custom test Bash script available in src/test/resources/org/sonar/scanner/sca that
// will serve as our "CLI". This script will output some messages about what arguments were passed
- // to it and will try to generate a zip file in the location the process specifies. This allows us
+ // to it and will try to generate an archive file in the location the process specifies. This allows us
// to simulate a real CLI call without needing an OS specific CLI executable to run on a real project.
URL scriptUrl = CliServiceTest.class.getResource(SystemUtils.IS_OS_WINDOWS ? "echo_args.bat" : "echo_args.sh");
assertThat(scriptUrl).isNotNull();
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/ScaExecutorTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/ScaExecutorTest.java
index aecbc7011a9..ebe6007a1c1 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/ScaExecutorTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/ScaExecutorTest.java
@@ -70,14 +70,14 @@ class ScaExecutorTest {
File mockManifestZip = Files.newTemporaryFile();
ScannerReportWriter mockReportWriter = mock(ScannerReportWriter.class);
when(cliCacheService.cacheCli()).thenReturn(mockCliFile);
- when(cliService.generateManifestsZip(root, mockCliFile, configuration)).thenReturn(mockManifestZip);
+ when(cliService.generateManifestsArchive(root, mockCliFile, configuration)).thenReturn(mockManifestZip);
when(reportPublisher.getWriter()).thenReturn(mockReportWriter);
logTester.setLevel(Level.DEBUG);
underTest.execute(root);
- verify(cliService).generateManifestsZip(root, mockCliFile, configuration);
+ verify(cliService).generateManifestsArchive(root, mockCliFile, configuration);
verify(mockReportWriter).writeScaFile(mockManifestZip);
assertThat(logTester.logs(Level.DEBUG)).contains("Zip ready for report: " + mockManifestZip);
assertThat(logTester.logs(Level.DEBUG)).contains("Manifest zip written to report");
@@ -87,13 +87,13 @@ class ScaExecutorTest {
void execute_whenIOException_shouldHandleException() throws IOException {
File mockCliFile = Files.newTemporaryFile();
when(cliCacheService.cacheCli()).thenReturn(mockCliFile);
- doThrow(IOException.class).when(cliService).generateManifestsZip(root, mockCliFile, configuration);
+ doThrow(IOException.class).when(cliService).generateManifestsArchive(root, mockCliFile, configuration);
logTester.setLevel(Level.INFO);
underTest.execute(root);
- verify(cliService).generateManifestsZip(root, mockCliFile, configuration);
+ verify(cliService).generateManifestsArchive(root, mockCliFile, configuration);
assertThat(logTester.logs(Level.ERROR)).contains("Error gathering manifests");
}
@@ -101,13 +101,13 @@ class ScaExecutorTest {
void execute_whenIllegalStateException_shouldHandleException() throws IOException {
File mockCliFile = Files.newTemporaryFile();
when(cliCacheService.cacheCli()).thenReturn(mockCliFile);
- doThrow(IllegalStateException.class).when(cliService).generateManifestsZip(root, mockCliFile, configuration);
+ doThrow(IllegalStateException.class).when(cliService).generateManifestsArchive(root, mockCliFile, configuration);
logTester.setLevel(Level.INFO);
underTest.execute(root);
- verify(cliService).generateManifestsZip(root, mockCliFile, configuration);
+ verify(cliService).generateManifestsArchive(root, mockCliFile, configuration);
assertThat(logTester.logs(Level.ERROR)).contains("Error gathering manifests");
}
@@ -118,7 +118,7 @@ class ScaExecutorTest {
underTest.execute(root);
- verify(cliService, never()).generateManifestsZip(root, mockCliFile, configuration);
+ verify(cliService, never()).generateManifestsArchive(root, mockCliFile, configuration);
}
@Test
@@ -150,15 +150,31 @@ class ScaExecutorTest {
File mockManifestZip = Files.newTemporaryFile();
ScannerReportWriter mockReportWriter = mock(ScannerReportWriter.class);
when(cliCacheService.cacheCli()).thenReturn(mockCliFile);
- when(cliService.generateManifestsZip(root, mockCliFile, configuration)).thenReturn(mockManifestZip);
+ when(cliService.generateManifestsArchive(root, mockCliFile, configuration)).thenReturn(mockManifestZip);
when(reportPublisher.getWriter()).thenReturn(mockReportWriter);
logTester.setLevel(Level.DEBUG);
underTest.execute(root);
- verify(cliService).generateManifestsZip(root, mockCliFile, configuration);
+ verify(cliService).generateManifestsArchive(root, mockCliFile, configuration);
verify(mockReportWriter).writeScaFile(mockManifestZip);
assertThat(logTester.logs(Level.DEBUG)).contains("Zip ready for report: " + mockManifestZip);
assertThat(logTester.logs(Level.DEBUG)).contains("Manifest zip written to report");
}
+
+ @Test
+ void execute_printsRuntime() throws IOException {
+ File mockCliFile = Files.newTemporaryFile();
+ File mockManifestZip = Files.newTemporaryFile();
+ ScannerReportWriter mockReportWriter = mock(ScannerReportWriter.class);
+ when(cliCacheService.cacheCli()).thenReturn(mockCliFile);
+ when(cliService.generateManifestsArchive(root, mockCliFile, configuration)).thenReturn(mockManifestZip);
+ when(reportPublisher.getWriter()).thenReturn(mockReportWriter);
+
+ logTester.setLevel(Level.INFO);
+
+ underTest.execute(root);
+
+ assertThat(logTester.logs(Level.INFO)).anyMatch(l -> l.matches("Load SCA project dependencies \\(done\\) \\| time=\\d+ms"));
+ }
}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/ScaPropertiesTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/ScaPropertiesTest.java
index e598a225b9c..70e7a6b6e53 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/ScaPropertiesTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/ScaPropertiesTest.java
@@ -19,11 +19,9 @@
*/
package org.sonar.scanner.sca;
-import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
-import java.util.Set;
import org.junit.jupiter.api.Test;
import org.sonar.scanner.config.DefaultConfiguration;
@@ -36,13 +34,12 @@ class ScaPropertiesTest {
private final DefaultConfiguration configuration = mock(DefaultConfiguration.class);
@Test
- void buildFromScannerProperties_withNoProperties_returnsDefaultMap() {
+ void buildFromScannerProperties_withNoProperties_returnsEmptyMap() {
when(configuration.get(anyString())).thenReturn(Optional.empty());
- var result = ScaProperties.buildFromScannerProperties(configuration, Collections.emptySet());
+ var result = ScaProperties.buildFromScannerProperties(configuration);
- assertThat(result).containsExactly(
- Map.entry("TIDELIFT_RECURSIVE_MANIFEST_SEARCH", "true"));
+ assertThat(result).isEqualTo(Map.of());
}
@Test
@@ -51,14 +48,13 @@ class ScaPropertiesTest {
inputProperties.put("sonar.sca.pythonBinary", "/usr/bin/python3");
inputProperties.put("sonar.sca.unknownProperty", "value");
inputProperties.put("sonar.somethingElse", "dont-include-non-sca");
- inputProperties.put("sonar.sca.ignoredProperty", "ignore-me");
+ inputProperties.put("sonar.sca.recursiveManifestSearch", "ignore-me");
when(configuration.getProperties()).thenReturn(inputProperties);
when(configuration.get(anyString())).thenAnswer(i -> Optional.ofNullable(inputProperties.get(i.getArgument(0, String.class))));
- var result = ScaProperties.buildFromScannerProperties(configuration, Set.of("sonar.sca.ignoredProperty"));
+ var result = ScaProperties.buildFromScannerProperties(configuration);
assertThat(result).containsExactly(
- Map.entry("TIDELIFT_RECURSIVE_MANIFEST_SEARCH", "true"),
Map.entry("TIDELIFT_PYTHON_BINARY", "/usr/bin/python3"),
Map.entry("TIDELIFT_UNKNOWN_PROPERTY", "value"));
}
@@ -79,7 +75,6 @@ class ScaPropertiesTest {
inputProperties.put("sonar.sca.pythonBinary", "/usr/bin/python3");
inputProperties.put("sonar.sca.pythonNoResolve", "true");
inputProperties.put("sonar.sca.pythonResolveLocal", "false");
- inputProperties.put("sonar.sca.recursiveManifestSearch", "true");
when(configuration.getProperties()).thenReturn(inputProperties);
when(configuration.get(anyString())).thenAnswer(i -> Optional.ofNullable(inputProperties.get(i.getArgument(0, String.class))));
@@ -97,37 +92,9 @@ class ScaPropertiesTest {
expectedProperties.put("TIDELIFT_PYTHON_BINARY", "/usr/bin/python3");
expectedProperties.put("TIDELIFT_PYTHON_NO_RESOLVE", "true");
expectedProperties.put("TIDELIFT_PYTHON_RESOLVE_LOCAL", "false");
- expectedProperties.put("TIDELIFT_RECURSIVE_MANIFEST_SEARCH", "true");
- var result = ScaProperties.buildFromScannerProperties(configuration, Collections.emptySet());
+ var result = ScaProperties.buildFromScannerProperties(configuration);
assertThat(result).containsExactlyInAnyOrderEntriesOf(expectedProperties);
}
-
-
- @Test
- void buildFromScannerProperties_withoutRecursiveModeProp_defaultsRecursiveModeTrue() {
- var inputProperties = new HashMap<String, String>();
- when(configuration.getProperties()).thenReturn(inputProperties);
- when(configuration.get(anyString())).thenAnswer(i -> Optional.ofNullable(inputProperties.get(i.getArgument(0, String.class))));
-
- var result = ScaProperties.buildFromScannerProperties(configuration, Collections.emptySet());
-
- assertThat(result).containsExactly(
- Map.entry("TIDELIFT_RECURSIVE_MANIFEST_SEARCH", "true"));
- }
-
- @Test
- void buildFromScannerProperties_withRecursiveModeProp_usesPropAsOverride() {
- var inputProperties = new HashMap<String, String>();
- inputProperties.put("sonar.sca.recursiveManifestSearch", "false");
- when(configuration.getProperties()).thenReturn(inputProperties);
- when(configuration.get(anyString())).thenAnswer(i -> Optional.ofNullable(inputProperties.get(i.getArgument(0, String.class))));
-
- var result = ScaProperties.buildFromScannerProperties(configuration, Collections.emptySet());
-
- assertThat(result).containsExactly(
- Map.entry("TIDELIFT_RECURSIVE_MANIFEST_SEARCH", "false"));
- }
-
}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/ProjectConfigurationProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/ProjectConfigurationProviderTest.java
index 21dcf58b114..3e3066e76e2 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/ProjectConfigurationProviderTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/ProjectConfigurationProviderTest.java
@@ -52,10 +52,9 @@ public class ProjectConfigurationProviderTest {
private static final Map<String, String> PROJECT_SERVER_PROPERTIES = Map.of(NON_GLOBAL_KEY_PROPERTIES_1, NON_GLOBAL_VALUE_PROPERTIES_1);
private static final Map<String, String> DEFAULT_PROJECT_PROPERTIES = Map.of(DEFAULT_KEY_PROPERTIES_1, DEFAULT_VALUE_1);
- private static final Map<String, String> ALL_PROPERTIES_MAP =
- Stream.of(GLOBAL_SERVER_PROPERTIES, PROJECT_SERVER_PROPERTIES, DEFAULT_PROJECT_PROPERTIES)
- .flatMap(map -> map.entrySet().stream())
- .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+ private static final Map<String, String> ALL_PROPERTIES_MAP = Stream.of(GLOBAL_SERVER_PROPERTIES, PROJECT_SERVER_PROPERTIES, DEFAULT_PROJECT_PROPERTIES)
+ .flatMap(map -> map.entrySet().stream())
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
private static final Map<String, String> PROPERTIES_AFTER_FILTERING = Map.of("aKey", "aValue");
@@ -66,8 +65,6 @@ public class ProjectConfigurationProviderTest {
@Mock
private GlobalConfiguration globalConfiguration;
@Mock
- private MutableProjectSettings mutableProjectSettings;
- @Mock
private DefaultInputProject defaultInputProject;
@Mock
private SonarGlobalPropertiesFilter sonarGlobalPropertiesFilter;
@@ -75,7 +72,6 @@ public class ProjectConfigurationProviderTest {
@InjectMocks
private ProjectConfigurationProvider provider;
-
@Before
public void init() {
when(globalConfiguration.getDefinitions()).thenReturn(new PropertyDefinitions(System2.INSTANCE));
@@ -89,11 +85,11 @@ public class ProjectConfigurationProviderTest {
when(sonarGlobalPropertiesFilter.enforceOnlyServerSideSonarGlobalPropertiesAreUsed(ALL_PROPERTIES_MAP, GLOBAL_SERVER_PROPERTIES))
.thenReturn(PROPERTIES_AFTER_FILTERING);
- ProjectConfiguration provide = provider.provide(defaultInputProject, globalConfiguration, globalServerSettings, projectServerSettings, mutableProjectSettings);
+ ProjectConfiguration provide = provider.provide(defaultInputProject, globalConfiguration, globalServerSettings, projectServerSettings);
verify(sonarGlobalPropertiesFilter).enforceOnlyServerSideSonarGlobalPropertiesAreUsed(ALL_PROPERTIES_MAP, GLOBAL_SERVER_PROPERTIES);
assertThat(provide.getOriginalProperties()).containsExactlyEntriesOf(PROPERTIES_AFTER_FILTERING);
}
-} \ No newline at end of file
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/DirectoryFileVisitorTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/DirectoryFileVisitorTest.java
index 01cb7d1bccf..277fc0dc68a 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/DirectoryFileVisitorTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/DirectoryFileVisitorTest.java
@@ -27,20 +27,26 @@ import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
+import java.util.Optional;
import org.apache.commons.lang3.SystemUtils;
+import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.fs.internal.DefaultInputModule;
+import org.sonar.scanner.bootstrap.SonarUserHome;
import org.sonar.scanner.fs.InputModuleHierarchy;
+import org.sonar.scanner.scan.ModuleConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
public class DirectoryFileVisitorTest {
@@ -48,33 +54,57 @@ public class DirectoryFileVisitorTest {
public static TemporaryFolder temp = new TemporaryFolder();
private final DefaultInputModule module = mock();
+ private final ModuleConfiguration moduleConfiguration = mock();
private final ModuleExclusionFilters moduleExclusionFilters = mock();
private final InputModuleHierarchy inputModuleHierarchy = mock();
private final InputFile.Type type = mock();
+ private final SonarUserHome sonarUserHome = mock();
+ private HiddenFilesProjectData hiddenFilesProjectData;
+
+ @Before
+ public void before() throws IOException {
+ Path sonarUserHomePath = temp.newFolder().toPath();
+ when(sonarUserHome.getPath()).thenReturn(sonarUserHomePath);
+ File workDir = temp.newFolder();
+ when(module.getWorkDir()).thenReturn(workDir.toPath());
+ hiddenFilesProjectData = spy(new HiddenFilesProjectData(sonarUserHome));
+ }
@Test
- public void visit_hidden_file() throws IOException {
+ public void should_not_visit_hidden_file() throws IOException {
+ when(moduleConfiguration.getBoolean("sonar.scanner.excludeHiddenFiles")).thenReturn(Optional.of(true));
DirectoryFileVisitor.FileVisitAction action = mock(DirectoryFileVisitor.FileVisitAction.class);
- File hidden = temp.newFile(".hidden");
- if (SystemUtils.IS_OS_WINDOWS) {
- Files.setAttribute(hidden.toPath(), "dos:hidden", true, LinkOption.NOFOLLOW_LINKS);
- }
-
+ File hidden = temp.newFile(".hiddenNotVisited");
+ setAsHiddenOnWindows(hidden);
- DirectoryFileVisitor underTest = new DirectoryFileVisitor(action, module, moduleExclusionFilters, inputModuleHierarchy, type);
+ DirectoryFileVisitor underTest = new DirectoryFileVisitor(action, module, moduleConfiguration, moduleExclusionFilters, inputModuleHierarchy, type, hiddenFilesProjectData);
underTest.visitFile(hidden.toPath(), Files.readAttributes(hidden.toPath(), BasicFileAttributes.class));
verify(action, never()).execute(any(Path.class));
}
@Test
+ public void should_visit_hidden_file() throws IOException {
+ when(moduleConfiguration.getBoolean("sonar.scanner.excludeHiddenFiles")).thenReturn(Optional.of(false));
+ DirectoryFileVisitor.FileVisitAction action = mock(DirectoryFileVisitor.FileVisitAction.class);
+
+ File hidden = temp.newFile(".hiddenVisited");
+ setAsHiddenOnWindows(hidden);
+
+ DirectoryFileVisitor underTest = new DirectoryFileVisitor(action, module, moduleConfiguration, moduleExclusionFilters, inputModuleHierarchy, type, hiddenFilesProjectData);
+ underTest.visitFile(hidden.toPath(), Files.readAttributes(hidden.toPath(), BasicFileAttributes.class));
+
+ verify(action).execute(any(Path.class));
+ }
+
+ @Test
public void test_visit_file_failed_generic_io_exception() throws IOException {
DirectoryFileVisitor.FileVisitAction action = mock(DirectoryFileVisitor.FileVisitAction.class);
File file = temp.newFile("failed");
- DirectoryFileVisitor underTest = new DirectoryFileVisitor(action, module, moduleExclusionFilters, inputModuleHierarchy, type);
+ DirectoryFileVisitor underTest = new DirectoryFileVisitor(action, module, moduleConfiguration, moduleExclusionFilters, inputModuleHierarchy, type, hiddenFilesProjectData);
assertThrows(IOException.class, () -> underTest.visitFileFailed(file.toPath(), new IOException()));
}
@@ -84,10 +114,15 @@ public class DirectoryFileVisitorTest {
File file = temp.newFile("symlink");
- DirectoryFileVisitor underTest = new DirectoryFileVisitor(action, module, moduleExclusionFilters, inputModuleHierarchy, type);
+ DirectoryFileVisitor underTest = new DirectoryFileVisitor(action, module, moduleConfiguration, moduleExclusionFilters, inputModuleHierarchy, type, hiddenFilesProjectData);
FileVisitResult result = underTest.visitFileFailed(file.toPath(), new FileSystemLoopException(file.getPath()));
assertThat(result).isEqualTo(FileVisitResult.CONTINUE);
}
+ private static void setAsHiddenOnWindows(File file) throws IOException {
+ if (SystemUtils.IS_OS_WINDOWS) {
+ Files.setAttribute(file.toPath(), "dos:hidden", true, LinkOption.NOFOLLOW_LINKS);
+ }
+ }
}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/HiddenFilesProjectDataTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/HiddenFilesProjectDataTest.java
new file mode 100644
index 00000000000..d5a6e4ff843
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/HiddenFilesProjectDataTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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.scanner.scan.filesystem;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import org.apache.commons.lang3.SystemUtils;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.fs.internal.DefaultInputModule;
+import org.sonar.scanner.bootstrap.SonarUserHome;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+public class HiddenFilesProjectDataTest {
+
+ @ClassRule
+ public static TemporaryFolder temp = new TemporaryFolder();
+
+ private static final SonarUserHome sonarUserHome = mock(SonarUserHome.class);
+ private final DefaultInputModule inputModule = mock(DefaultInputModule.class);
+ private final DefaultInputModule secondInputModule = mock(DefaultInputModule.class);
+ private HiddenFilesProjectData underTest;
+
+ @BeforeClass
+ public static void setUp() throws IOException {
+ File userHomeFolder = temp.newFolder(".userhome");
+ setAsHiddenOnWindows(userHomeFolder);
+ when(sonarUserHome.getPath()).thenReturn(userHomeFolder.toPath());
+ }
+
+ @Before
+ public void before() {
+ underTest = spy(new HiddenFilesProjectData(sonarUserHome));
+ }
+
+ @Test
+ public void shouldContainNoMarkedHiddenFileOnConstruction() {
+ assertThat(underTest.hiddenFilesByModule).isEmpty();
+ }
+
+ @Test
+ public void shouldMarkWithCorrectAssociatedInputModule() {
+ Path myFile = Path.of("myFile");
+ Path myFile2 = Path.of("myFile2");
+ underTest.markAsHiddenFile(myFile, inputModule);
+ underTest.markAsHiddenFile(myFile2, inputModule);
+
+ assertThat(underTest.hiddenFilesByModule).hasSize(1);
+ assertThat(underTest.getIsMarkedAsHiddenFileAndRemoveVisibilityInformation(myFile, inputModule)).isTrue();
+ assertThat(underTest.getIsMarkedAsHiddenFileAndRemoveVisibilityInformation(myFile2, inputModule)).isTrue();
+ assertThat(underTest.getIsMarkedAsHiddenFileAndRemoveVisibilityInformation(myFile, secondInputModule)).isFalse();
+ assertThat(underTest.getIsMarkedAsHiddenFileAndRemoveVisibilityInformation(myFile2, secondInputModule)).isFalse();
+ }
+
+ @Test
+ public void shouldMarkWithCorrectAssociatedInputModuleForTwoDifferentModules() {
+ Path myFile = Path.of("myFile");
+ Path myFile2 = Path.of("myFile2");
+ underTest.markAsHiddenFile(myFile, inputModule);
+ underTest.markAsHiddenFile(myFile2, secondInputModule);
+
+ assertThat(underTest.hiddenFilesByModule).hasSize(2);
+ assertThat(underTest.getIsMarkedAsHiddenFileAndRemoveVisibilityInformation(myFile, inputModule)).isTrue();
+ assertThat(underTest.getIsMarkedAsHiddenFileAndRemoveVisibilityInformation(myFile2, inputModule)).isFalse();
+ assertThat(underTest.getIsMarkedAsHiddenFileAndRemoveVisibilityInformation(myFile, secondInputModule)).isFalse();
+ assertThat(underTest.getIsMarkedAsHiddenFileAndRemoveVisibilityInformation(myFile2, secondInputModule)).isTrue();
+ }
+
+ @Test
+ public void shouldNotShowAsHiddenFileWhenInputModuleIsNotExistingInData() {
+ Path myFile = Path.of("myFile");
+ Path notMarkedFile = Path.of("notMarkedFile");
+ underTest.markAsHiddenFile(myFile, inputModule);
+
+ assertThat(underTest.hiddenFilesByModule).isNotEmpty();
+ assertThat(underTest.getIsMarkedAsHiddenFileAndRemoveVisibilityInformation(notMarkedFile, secondInputModule)).isFalse();
+ }
+
+ @Test
+ public void shouldClearMap() {
+ Path myFile = Path.of("myFile");
+ Path myFile2 = Path.of("myFile2");
+ underTest.markAsHiddenFile(myFile, inputModule);
+ underTest.markAsHiddenFile(myFile2, secondInputModule);
+
+ assertThat(underTest.hiddenFilesByModule).hasSize(2);
+
+ underTest.clearHiddenFilesData();
+ assertThat(underTest.hiddenFilesByModule).isEmpty();
+ }
+
+ @Test
+ public void shouldRemoveVisibilityAfterQuerying() {
+ Path myFile = Path.of("myFile");
+ Path myFile2 = Path.of("myFile2");
+ underTest.markAsHiddenFile(myFile, inputModule);
+ underTest.markAsHiddenFile(myFile2, inputModule);
+
+ assertThat(underTest.hiddenFilesByModule).hasSize(1);
+ assertThat(underTest.getIsMarkedAsHiddenFileAndRemoveVisibilityInformation(myFile, inputModule)).isTrue();
+ assertThat(underTest.getIsMarkedAsHiddenFileAndRemoveVisibilityInformation(myFile2, inputModule)).isTrue();
+
+ assertThat(underTest.hiddenFilesByModule).hasSize(1);
+ assertThat(underTest.hiddenFilesByModule.get(inputModule)).isEmpty();
+ assertThat(underTest.getIsMarkedAsHiddenFileAndRemoveVisibilityInformation(myFile, inputModule)).isFalse();
+ assertThat(underTest.getIsMarkedAsHiddenFileAndRemoveVisibilityInformation(myFile2, inputModule)).isFalse();
+ }
+
+ @Test
+ public void shouldOnlyRemoveModuleIfAllFilesAreRemoved() {
+ Path myFile = Path.of("myFile");
+ Path myFile2 = Path.of("myFile2");
+ underTest.markAsHiddenFile(myFile, inputModule);
+ underTest.markAsHiddenFile(myFile2, inputModule);
+
+ assertThat(underTest.hiddenFilesByModule).hasSize(1);
+ assertThat(underTest.getIsMarkedAsHiddenFileAndRemoveVisibilityInformation(myFile, inputModule)).isTrue();
+
+ assertThat(underTest.hiddenFilesByModule).isNotEmpty();
+ }
+
+ @Test
+ public void shouldNotFailOnUserPathResolving() throws IOException {
+ Path expectedPath = sonarUserHome.getPath().toRealPath(LinkOption.NOFOLLOW_LINKS).toAbsolutePath().normalize();
+ assertThat(underTest.getCachedSonarUserHomePath()).isEqualTo(expectedPath);
+ }
+
+ private static void setAsHiddenOnWindows(File file) throws IOException {
+ if (SystemUtils.IS_OS_WINDOWS) {
+ Files.setAttribute(file.toPath(), "dos:hidden", true, LinkOption.NOFOLLOW_LINKS);
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/HiddenFilesVisitorHelperTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/HiddenFilesVisitorHelperTest.java
new file mode 100644
index 00000000000..8c111c7ea15
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/HiddenFilesVisitorHelperTest.java
@@ -0,0 +1,315 @@
+/*
+ * 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.scanner.scan.filesystem;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.util.Optional;
+import org.apache.commons.lang3.SystemUtils;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.fs.internal.DefaultInputModule;
+import org.sonar.scanner.bootstrap.SonarUserHome;
+import org.sonar.scanner.scan.ModuleConfiguration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class HiddenFilesVisitorHelperTest {
+
+ @ClassRule
+ public static TemporaryFolder temp = new TemporaryFolder();
+
+ private static final SonarUserHome sonarUserHome = mock(SonarUserHome.class);
+ private static final DefaultInputModule inputModule = mock(DefaultInputModule.class);
+
+ private final ModuleConfiguration moduleConfig = mock(ModuleConfiguration.class);
+ private final HiddenFilesProjectData hiddenFilesProjectData = spy(new HiddenFilesProjectData(sonarUserHome));
+ private HiddenFilesVisitorHelper underTest;
+
+ @BeforeClass
+ public static void setUp() throws IOException {
+ File userHomeFolder = temp.newFolder(".userhome");
+ setAsHiddenOnWindows(userHomeFolder);
+ when(sonarUserHome.getPath()).thenReturn(userHomeFolder.toPath());
+
+ File workDir = temp.newFolder(".sonar");
+ setAsHiddenOnWindows(workDir);
+ when(inputModule.getWorkDir()).thenReturn(workDir.toPath());
+ }
+
+ @Before
+ public void before() {
+ hiddenFilesProjectData.clearHiddenFilesData();
+ underTest = spy(new HiddenFilesVisitorHelper(hiddenFilesProjectData, inputModule, moduleConfig));
+ }
+
+ @Test
+ public void verifyDefaultOnConstruction() {
+ assertThat(underTest.excludeHiddenFiles).isFalse();
+ assertThat(underTest.rootHiddenDir).isNull();
+ }
+
+ @Test
+ public void excludeHiddenFilesShouldBeSetToFalseFromConfigurationWhenNotConfigured() {
+ when(moduleConfig.getBoolean("sonar.scanner.excludeHiddenFiles")).thenReturn(Optional.empty());
+ HiddenFilesVisitorHelper configuredVisitorHelper = spy(new HiddenFilesVisitorHelper(hiddenFilesProjectData, inputModule, moduleConfig));
+
+ assertThat(configuredVisitorHelper.excludeHiddenFiles).isFalse();
+ }
+
+ @Test
+ public void excludeHiddenFilesShouldBeSetToFalseFromConfigurationWhenDisabled() {
+ when(moduleConfig.getBoolean("sonar.scanner.excludeHiddenFiles")).thenReturn(Optional.of(false));
+ HiddenFilesVisitorHelper configuredVisitorHelper = spy(new HiddenFilesVisitorHelper(hiddenFilesProjectData, inputModule, moduleConfig));
+
+ assertThat(configuredVisitorHelper.excludeHiddenFiles).isFalse();
+ }
+
+ @Test
+ public void excludeHiddenFilesShouldBeSetToTrueFromConfigurationWhenEnabled() {
+ when(moduleConfig.getBoolean("sonar.scanner.excludeHiddenFiles")).thenReturn(Optional.of(true));
+ HiddenFilesVisitorHelper configuredVisitorHelper = spy(new HiddenFilesVisitorHelper(hiddenFilesProjectData, inputModule, moduleConfig));
+
+ assertThat(configuredVisitorHelper.excludeHiddenFiles).isTrue();
+ }
+
+ @Test
+ public void shouldVisitHiddenDirectory() throws IOException {
+ File hiddenDir = temp.newFolder(".hiddenVisited");
+ setAsHiddenOnWindows(hiddenDir);
+
+ boolean visitDir = underTest.shouldVisitDir(hiddenDir.toPath());
+
+ assertThat(visitDir).isTrue();
+ assertThat(underTest.insideHiddenDirectory()).isTrue();
+ assertThat(underTest.rootHiddenDir).isEqualTo(hiddenDir.toPath());
+ verify(underTest).enterHiddenDirectory(hiddenDir.toPath());
+ }
+
+ @Test
+ public void shouldNotVisitHiddenDirectoryWhenHiddenFilesVisitIsExcluded() throws IOException {
+ when(moduleConfig.getBoolean("sonar.scanner.excludeHiddenFiles")).thenReturn(Optional.of(true));
+ HiddenFilesVisitorHelper configuredVisitorHelper = spy(new HiddenFilesVisitorHelper(hiddenFilesProjectData, inputModule, moduleConfig));
+
+ File hidden = temp.newFolder(".hiddenNotVisited");
+ setAsHiddenOnWindows(hidden);
+
+ boolean visitDir = configuredVisitorHelper.shouldVisitDir(hidden.toPath());
+
+ assertThat(visitDir).isFalse();
+ assertThat(configuredVisitorHelper.insideHiddenDirectory()).isFalse();
+ verify(configuredVisitorHelper, never()).enterHiddenDirectory(any());
+ }
+
+ @Test
+ public void shouldVisitNonHiddenDirectoryWhenHiddenFilesVisitIsExcluded() throws IOException {
+ when(moduleConfig.getBoolean("sonar.scanner.excludeHiddenFiles")).thenReturn(Optional.of(true));
+ HiddenFilesVisitorHelper configuredVisitorHelper = spy(new HiddenFilesVisitorHelper(hiddenFilesProjectData, inputModule, moduleConfig));
+
+ File nonHiddenFolder = temp.newFolder();
+
+ boolean visitDir = configuredVisitorHelper.shouldVisitDir(nonHiddenFolder.toPath());
+
+ assertThat(visitDir).isTrue();
+ assertThat(configuredVisitorHelper.insideHiddenDirectory()).isFalse();
+ verify(configuredVisitorHelper, never()).enterHiddenDirectory(any());
+ }
+
+ @Test
+ public void shouldVisitNonHiddenDirectory() throws IOException {
+ File nonHiddenFolder = temp.newFolder();
+
+ boolean visitDir = underTest.shouldVisitDir(nonHiddenFolder.toPath());
+
+ assertThat(visitDir).isTrue();
+ assertThat(underTest.insideHiddenDirectory()).isFalse();
+ verify(underTest, never()).enterHiddenDirectory(any());
+ assertThat(underTest.excludeHiddenFiles).isFalse();
+ }
+
+ @Test
+ public void shouldNotVisitModuleWorkDir() throws IOException {
+ Path workingDirectory = inputModule.getWorkDir().toRealPath(LinkOption.NOFOLLOW_LINKS).toAbsolutePath().normalize();
+ boolean visitDir = underTest.shouldVisitDir(workingDirectory);
+
+ assertThat(visitDir).isFalse();
+ assertThat(underTest.insideHiddenDirectory()).isFalse();
+ verify(underTest, never()).enterHiddenDirectory(any());
+ }
+
+ @Test
+ public void shouldNotVisitSonarUserHome() throws IOException {
+ Path userHome = sonarUserHome.getPath().toRealPath(LinkOption.NOFOLLOW_LINKS).toAbsolutePath().normalize();
+ boolean visitDir = underTest.shouldVisitDir(userHome);
+
+ assertThat(visitDir).isFalse();
+ assertThat(underTest.insideHiddenDirectory()).isFalse();
+ verify(underTest, never()).enterHiddenDirectory(any());
+ }
+
+ @Test
+ public void hiddenFileShouldBeVisited() throws IOException {
+ File hiddenFile = temp.newFile(".hiddenFileShouldBeVisited");
+ setAsHiddenOnWindows(hiddenFile);
+
+ assertThat(underTest.insideHiddenDirectory()).isFalse();
+ boolean visitFile = underTest.shouldVisitFile(hiddenFile.toPath());
+
+ assertThat(visitFile).isTrue();
+ verify(hiddenFilesProjectData).markAsHiddenFile(hiddenFile.toPath(), inputModule);
+ }
+
+ @Test
+ public void nonHiddenFileShouldBeVisitedInHiddenFolder() throws IOException {
+ File hidden = temp.newFolder(".hiddenFolder");
+ setAsHiddenOnWindows(hidden);
+
+ File nonHiddenFile = temp.newFile();
+
+ underTest.shouldVisitDir(hidden.toPath());
+ assertThat(underTest.insideHiddenDirectory()).isTrue();
+
+ boolean shouldVisitFile = underTest.shouldVisitFile(nonHiddenFile.toPath());
+
+ assertThat(shouldVisitFile).isTrue();
+ verify(hiddenFilesProjectData).markAsHiddenFile(nonHiddenFile.toPath(), inputModule);
+ }
+
+ @Test
+ public void shouldNotSetAsRootHiddenDirectoryWhenAlreadyEnteredHiddenDirectory() throws IOException {
+ File hidden = temp.newFolder(".outerHiddenFolder");
+ File nestedHiddenFolder = temp.newFolder(".outerHiddenFolder", ".nestedHiddenFolder");
+ setAsHiddenOnWindows(hidden);
+ setAsHiddenOnWindows(nestedHiddenFolder);
+
+ underTest.shouldVisitDir(hidden.toPath());
+ assertThat(underTest.insideHiddenDirectory()).isTrue();
+
+ boolean shouldVisitNestedDir = underTest.shouldVisitDir(nestedHiddenFolder.toPath());
+
+ assertThat(shouldVisitNestedDir).isTrue();
+ assertThat(underTest.rootHiddenDir).isEqualTo(hidden.toPath());
+ verify(underTest).enterHiddenDirectory(nestedHiddenFolder.toPath());
+ }
+
+ @Test
+ public void hiddenFileShouldNotBeVisitedWhenHiddenFileVisitExcluded() throws IOException {
+ when(moduleConfig.getBoolean("sonar.scanner.excludeHiddenFiles")).thenReturn(Optional.of(true));
+ HiddenFilesVisitorHelper configuredVisitorHelper = spy(new HiddenFilesVisitorHelper(hiddenFilesProjectData, inputModule, moduleConfig));
+
+ File hiddenFile = temp.newFile(".hiddenFileNotVisited");
+ setAsHiddenOnWindows(hiddenFile);
+
+ assertThat(configuredVisitorHelper.insideHiddenDirectory()).isFalse();
+
+ configuredVisitorHelper.shouldVisitFile(hiddenFile.toPath());
+ boolean shouldVisitFile = configuredVisitorHelper.shouldVisitFile(hiddenFile.toPath());
+
+ assertThat(shouldVisitFile).isFalse();
+ verify(hiddenFilesProjectData, never()).markAsHiddenFile(hiddenFile.toPath(), inputModule);
+ }
+
+ @Test
+ public void shouldCorrectlyExitHiddenFolderOnlyOnHiddenFolderThatEntered() throws IOException {
+ File hiddenFolder = temp.newFolder(".hiddenRootFolder");
+ setAsHiddenOnWindows(hiddenFolder);
+
+ boolean shouldVisitDir = underTest.shouldVisitDir(hiddenFolder.toPath());
+
+ assertThat(shouldVisitDir).isTrue();
+ assertThat(underTest.insideHiddenDirectory()).isTrue();
+ assertThat(underTest.rootHiddenDir).isEqualTo(hiddenFolder.toPath());
+ verify(underTest).enterHiddenDirectory(hiddenFolder.toPath());
+
+ File folder1 = temp.newFolder(".hiddenRootFolder", "myFolderExit");
+ File folder2 = temp.newFolder("myFolderExit");
+ File folder3 = temp.newFolder(".myFolderExit");
+ setAsHiddenOnWindows(folder3);
+
+ underTest.exitDirectory(folder1.toPath());
+ underTest.exitDirectory(folder2.toPath());
+ underTest.exitDirectory(folder3.toPath());
+
+ assertThat(underTest.insideHiddenDirectory()).isTrue();
+ assertThat(underTest.rootHiddenDir).isEqualTo(hiddenFolder.toPath());
+ verify(underTest, never()).resetRootHiddenDir();
+
+ underTest.exitDirectory(hiddenFolder.toPath());
+ assertThat(underTest.insideHiddenDirectory()).isFalse();
+ assertThat(underTest.rootHiddenDir).isNull();
+ verify(underTest).resetRootHiddenDir();
+ }
+
+ @Test
+ public void shouldNotInitiateResetRootDirWhenNotInHiddenDirectory() throws IOException {
+ File hiddenFolder = temp.newFolder(".hiddenFolderNonRoot");
+ setAsHiddenOnWindows(hiddenFolder);
+
+ underTest.exitDirectory(hiddenFolder.toPath());
+
+ verify(underTest, never()).resetRootHiddenDir();
+ }
+
+ @Test
+ public void filesShouldBeCorrectlyMarkedAsHidden() throws IOException {
+ File hiddenFolder = temp.newFolder(".hiddenFolderRoot");
+ setAsHiddenOnWindows(hiddenFolder);
+
+ File file1 = temp.newFile();
+ File file2 = temp.newFile();
+ File file3 = temp.newFile(".markedHiddenFile");
+ setAsHiddenOnWindows(file3);
+ File file4 = temp.newFile();
+ File file5 = temp.newFile(".markedHiddenFile2");
+ setAsHiddenOnWindows(file5);
+
+ underTest.shouldVisitFile(file1.toPath());
+ underTest.shouldVisitDir(hiddenFolder.toPath());
+ underTest.shouldVisitFile(file2.toPath());
+ underTest.shouldVisitFile(file3.toPath());
+ underTest.exitDirectory(hiddenFolder.toPath());
+ underTest.shouldVisitFile(file4.toPath());
+ underTest.shouldVisitFile(file5.toPath());
+
+ verify(hiddenFilesProjectData, never()).markAsHiddenFile(file1.toPath(), inputModule);
+ verify(hiddenFilesProjectData).markAsHiddenFile(file2.toPath(), inputModule);
+ verify(hiddenFilesProjectData).markAsHiddenFile(file3.toPath(), inputModule);
+ verify(hiddenFilesProjectData, never()).markAsHiddenFile(file4.toPath(), inputModule);
+ verify(hiddenFilesProjectData).markAsHiddenFile(file5.toPath(), inputModule);
+ }
+
+ private static void setAsHiddenOnWindows(File file) throws IOException {
+ if (SystemUtils.IS_OS_WINDOWS) {
+ Files.setAttribute(file.toPath(), "dos:hidden", true, LinkOption.NOFOLLOW_LINKS);
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/ModuleInputComponentStoreTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/ModuleInputComponentStoreTest.java
index a0031f77633..07f7afec036 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/ModuleInputComponentStoreTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/ModuleInputComponentStoreTest.java
@@ -19,80 +19,151 @@
*/
package org.sonar.scanner.scan.filesystem;
-import java.io.IOException;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
+import java.io.File;
+import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
import org.sonar.api.SonarRuntime;
import org.sonar.api.batch.fs.InputFile;
-import org.sonar.api.batch.fs.InputModule;
import org.sonar.api.batch.fs.internal.SensorStrategy;
-import org.sonar.api.batch.fs.internal.DefaultInputProject;
import org.sonar.api.batch.fs.internal.TestInputFileBuilder;
+import org.sonar.api.batch.sensor.internal.SensorContextTester;
import org.sonar.scanner.scan.branch.BranchConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-public class ModuleInputComponentStoreTest {
- @Rule
- public TemporaryFolder temp = new TemporaryFolder();
+@ExtendWith(MockitoExtension.class)
+class ModuleInputComponentStoreTest {
+
+ @TempDir
+ private File projectBaseDir;
+
+ @Mock
+ BranchConfiguration branchConfiguration;
+
+ @Mock
+ SonarRuntime sonarRuntime;
+
+ @Mock
+ InputComponentStore mockedInputComponentStore;
private InputComponentStore componentStore;
+ private SensorContextTester sensorContextTester;
private final String projectKey = "dummy key";
- @Before
- public void setUp() throws IOException {
- DefaultInputProject root = TestInputFileBuilder.newDefaultInputProject(projectKey, temp.newFolder());
- componentStore = new InputComponentStore(mock(BranchConfiguration.class), mock(SonarRuntime.class));
+ @BeforeEach
+ void setUp() {
+ TestInputFileBuilder.newDefaultInputProject(projectKey, projectBaseDir);
+ File moduleBaseDir = new File(projectBaseDir, "module");
+ moduleBaseDir.mkdir();
+ sensorContextTester = SensorContextTester.create(moduleBaseDir);
+ componentStore = spy(new InputComponentStore(branchConfiguration, sonarRuntime));
}
@Test
- public void should_cache_files_by_filename() {
+ void should_cache_module_files_by_filename() {
ModuleInputComponentStore store = newModuleInputComponentStore();
String filename = "some name";
- InputFile inputFile1 = new TestInputFileBuilder(projectKey, "some/path/" + filename).build();
+ InputFile inputFile1 = new TestInputFileBuilder(projectKey, "module/some/path/" + filename).build();
store.doAdd(inputFile1);
- InputFile inputFile2 = new TestInputFileBuilder(projectKey, "other/path/" + filename).build();
+ InputFile inputFile2 = new TestInputFileBuilder(projectKey, "module/other/path/" + filename).build();
store.doAdd(inputFile2);
- InputFile dummyInputFile = new TestInputFileBuilder(projectKey, "some/path/Dummy.java").build();
+ InputFile dummyInputFile = new TestInputFileBuilder(projectKey, "module/some/path/Dummy.java").build();
store.doAdd(dummyInputFile);
assertThat(store.getFilesByName(filename)).containsExactlyInAnyOrder(inputFile1, inputFile2);
}
@Test
- public void should_cache_files_by_extension() {
+ void should_cache_filtered_module_files_by_filename() {
+ ModuleInputComponentStore store = newModuleInputComponentStore();
+
+ String filename = "some name";
+ InputFile inputFile1 = new TestInputFileBuilder(projectKey, "some/path/" + filename).build();
+ InputFile inputFile2 = new TestInputFileBuilder(projectKey, "module/other/path/" + filename).build();
+ store.doAdd(inputFile2);
+
+ when(componentStore.getFilesByName(filename)).thenReturn(List.of(inputFile1, inputFile2));
+
+ assertThat(store.getFilesByName(filename)).containsOnly(inputFile2);
+ }
+
+ @Test
+ void should_cache_module_files_by_filename_global_strategy() {
+ ModuleInputComponentStore store = new ModuleInputComponentStore(sensorContextTester.module(), componentStore, new SensorStrategy());
+
+ String filename = "some name";
+ // None in the module
+ InputFile inputFile1 = new TestInputFileBuilder(projectKey, "some/path/" + filename).build();
+ InputFile inputFile2 = new TestInputFileBuilder(projectKey, "other/path/" + filename).build();
+
+ when(componentStore.getFilesByName(filename)).thenReturn(List.of(inputFile1, inputFile2));
+
+ assertThat(store.getFilesByName(filename)).containsExactlyInAnyOrder(inputFile1, inputFile2);
+ }
+
+ @Test
+ void should_cache_module_files_by_extension() {
ModuleInputComponentStore store = newModuleInputComponentStore();
- InputFile inputFile1 = new TestInputFileBuilder(projectKey, "some/path/Program.java").build();
+ InputFile inputFile1 = new TestInputFileBuilder(projectKey, "module/some/path/Program.java").build();
store.doAdd(inputFile1);
- InputFile inputFile2 = new TestInputFileBuilder(projectKey, "other/path/Utils.java").build();
+ InputFile inputFile2 = new TestInputFileBuilder(projectKey, "module/other/path/Utils.java").build();
store.doAdd(inputFile2);
- InputFile dummyInputFile = new TestInputFileBuilder(projectKey, "some/path/NotJava.cpp").build();
+ InputFile dummyInputFile = new TestInputFileBuilder(projectKey, "module/some/path/NotJava.cpp").build();
store.doAdd(dummyInputFile);
assertThat(store.getFilesByExtension("java")).containsExactlyInAnyOrder(inputFile1, inputFile2);
}
@Test
- public void should_not_cache_duplicates() {
+ void should_cache_filtered_module_files_by_extension() {
+ ModuleInputComponentStore store = newModuleInputComponentStore();
+
+ InputFile inputFile1 = new TestInputFileBuilder(projectKey, "some/path/NotInModule.java").build();
+ InputFile inputFile2 = new TestInputFileBuilder(projectKey, "module/some/path/Other.java").build();
+ store.doAdd(inputFile2);
+
+ when(componentStore.getFilesByExtension("java")).thenReturn(List.of(inputFile1, inputFile2));
+
+ assertThat(store.getFilesByExtension("java")).containsOnly(inputFile2);
+ }
+
+ @Test
+ void should_cache_module_files_by_extension_global_strategy() {
+ ModuleInputComponentStore store = new ModuleInputComponentStore(sensorContextTester.module(), componentStore, new SensorStrategy());
+
+ // None in the module
+ InputFile inputFile1 = new TestInputFileBuilder(projectKey, "some/path/NotInModule.java").build();
+ InputFile inputFile2 = new TestInputFileBuilder(projectKey, "some/path/Other.java").build();
+
+ when(componentStore.getFilesByExtension("java")).thenReturn(List.of(inputFile1, inputFile2));
+
+ assertThat(store.getFilesByExtension("java")).containsExactlyInAnyOrder(inputFile1, inputFile2);
+ }
+
+ @Test
+ void should_not_cache_duplicates() {
ModuleInputComponentStore store = newModuleInputComponentStore();
String ext = "java";
String filename = "Program." + ext;
- InputFile inputFile = new TestInputFileBuilder(projectKey, "some/path/" + filename).build();
+ InputFile inputFile = new TestInputFileBuilder(projectKey, "module/some/path/" + filename).build();
store.doAdd(inputFile);
store.doAdd(inputFile);
store.doAdd(inputFile);
@@ -102,12 +173,12 @@ public class ModuleInputComponentStoreTest {
}
@Test
- public void should_get_empty_iterable_on_cache_miss() {
+ void should_get_empty_iterable_on_cache_miss() {
ModuleInputComponentStore store = newModuleInputComponentStore();
String ext = "java";
String filename = "Program." + ext;
- InputFile inputFile = new TestInputFileBuilder(projectKey, "some/path/" + filename).build();
+ InputFile inputFile = new TestInputFileBuilder(projectKey, "module/some/path/" + filename).build();
store.doAdd(inputFile);
assertThat(store.getFilesByName("nonexistent")).isEmpty();
@@ -115,48 +186,42 @@ public class ModuleInputComponentStoreTest {
}
private ModuleInputComponentStore newModuleInputComponentStore() {
- InputModule module = mock(InputModule.class);
- when(module.key()).thenReturn("moduleKey");
- return new ModuleInputComponentStore(module, componentStore, mock(SensorStrategy.class));
+ SensorStrategy strategy = new SensorStrategy();
+ strategy.setGlobal(false);
+ return new ModuleInputComponentStore(sensorContextTester.module(), componentStore, strategy);
}
@Test
- public void should_find_module_components_with_non_global_strategy() {
- InputComponentStore inputComponentStore = mock(InputComponentStore.class);
+ void should_find_module_components_with_non_global_strategy() {
SensorStrategy strategy = new SensorStrategy();
- InputModule module = mock(InputModule.class);
- when(module.key()).thenReturn("foo");
- ModuleInputComponentStore store = new ModuleInputComponentStore(module, inputComponentStore, strategy);
+ ModuleInputComponentStore store = new ModuleInputComponentStore(sensorContextTester.module(), mockedInputComponentStore, strategy);
strategy.setGlobal(false);
store.inputFiles();
- verify(inputComponentStore).filesByModule("foo");
+ verify(mockedInputComponentStore).filesByModule(sensorContextTester.module().key());
String relativePath = "somepath";
store.inputFile(relativePath);
- verify(inputComponentStore).getFile(any(String.class), eq(relativePath));
+ verify(mockedInputComponentStore).getFile(any(String.class), eq(relativePath));
store.languages();
- verify(inputComponentStore).languages(any(String.class));
+ verify(mockedInputComponentStore).languages(any(String.class));
}
@Test
- public void should_find_all_components_with_global_strategy() {
- InputComponentStore inputComponentStore = mock(InputComponentStore.class);
+ void should_find_all_components_with_global_strategy() {
SensorStrategy strategy = new SensorStrategy();
- ModuleInputComponentStore store = new ModuleInputComponentStore(mock(InputModule.class), inputComponentStore, strategy);
-
- strategy.setGlobal(true);
+ ModuleInputComponentStore store = new ModuleInputComponentStore(sensorContextTester.module(), mockedInputComponentStore, strategy);
store.inputFiles();
- verify(inputComponentStore).inputFiles();
+ verify(mockedInputComponentStore).inputFiles();
String relativePath = "somepath";
store.inputFile(relativePath);
- verify(inputComponentStore).inputFile(relativePath);
+ verify(mockedInputComponentStore).inputFile(relativePath);
store.languages();
- verify(inputComponentStore).languages();
+ verify(mockedInputComponentStore).languages();
}
}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/MutableFileSystemTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/MutableFileSystemTest.java
index 31d3312853b..485708c9936 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/MutableFileSystemTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/MutableFileSystemTest.java
@@ -28,6 +28,9 @@ import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.fs.internal.TestInputFileBuilder;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
public class MutableFileSystemTest {
@@ -44,9 +47,15 @@ public class MutableFileSystemTest {
}
@Test
- public void return_all_files_when_not_restricted() {
+ public void restriction_and_hidden_file_should_be_disabled_on_default() {
+ assertThat(underTest.restrictToChangedFiles).isFalse();
+ assertThat(underTest.allowHiddenFileAnalysis).isFalse();
+ }
+
+ @Test
+ public void return_all_non_hidden_files_when_not_restricted_and_disabled() {
assertThat(underTest.inputFiles(underTest.predicates().all())).isEmpty();
- addFileWithAllStatus();
+ addFilesWithAllStatus();
underTest.setRestrictToChangedFiles(false);
assertThat(underTest.inputFiles(underTest.predicates().all())).hasSize(3);
@@ -58,7 +67,7 @@ public class MutableFileSystemTest {
@Test
public void return_only_changed_files_when_restricted() {
assertThat(underTest.inputFiles(underTest.predicates().all())).isEmpty();
- addFileWithAllStatus();
+ addFilesWithAllStatus();
underTest.setRestrictToChangedFiles(true);
assertThat(underTest.inputFiles(underTest.predicates().all())).hasSize(2);
@@ -67,19 +76,95 @@ public class MutableFileSystemTest {
assertThat(underTest.inputFile(underTest.predicates().hasFilename(generateFilename(InputFile.Status.CHANGED)))).isNotNull();
}
- private void addFileWithAllStatus() {
+ @Test
+ public void return_all_files_when_allowing_hidden_files_analysis() {
+ assertThat(underTest.inputFiles(underTest.predicates().all())).isEmpty();
+ addFilesWithVisibility();
+ underTest.setAllowHiddenFileAnalysis(true);
+
+ assertThat(underTest.inputFiles(underTest.predicates().all())).hasSize(2);
+ assertThat(underTest.inputFile(underTest.predicates().hasFilename(generateFilename(true)))).isNotNull();
+ assertThat(underTest.inputFile(underTest.predicates().hasFilename(generateFilename(false)))).isNotNull();
+ }
+
+ @Test
+ public void return_only_non_hidden_files_when_not_allowing_hidden_files_analysis() {
+ assertThat(underTest.inputFiles(underTest.predicates().all())).isEmpty();
+ addFilesWithVisibility();
+ underTest.setAllowHiddenFileAnalysis(false);
+
+ assertThat(underTest.inputFiles(underTest.predicates().all())).hasSize(1);
+ assertThat(underTest.inputFile(underTest.predicates().hasFilename(generateFilename(true)))).isNull();
+ assertThat(underTest.inputFile(underTest.predicates().hasFilename(generateFilename(false)))).isNotNull();
+ }
+
+ @Test
+ public void hidden_file_predicate_should_preserve_predicate_optimization() {
+ addFilesWithVisibility();
+ var anotherHiddenFile = spy(new TestInputFileBuilder("foo", String.format("src/%s", ".myHiddenFile.txt"))
+ .setLanguage(LANGUAGE).setStatus(InputFile.Status.ADDED).setHidden(true).build());
+ underTest.add(anotherHiddenFile);
+ underTest.setAllowHiddenFileAnalysis(false);
+
+ assertThat(underTest.inputFile(underTest.predicates().hasFilename(generateFilename(true)))).isNull();
+ assertThat(underTest.inputFile(underTest.predicates().hasFilename(generateFilename(false)))).isNotNull();
+ // Verify that predicate optimization is still effective
+ verify(anotherHiddenFile, never()).isHidden();
+
+ // This predicate can't be optimized
+ assertThat(underTest.inputFiles(underTest.predicates().all())).hasSize(1);
+ verify(anotherHiddenFile).isHidden();
+ }
+
+ @Test
+ public void hidden_file_predicate_should_be_applied_first_for_non_optimized_predicates() {
+ // Checking the file type is not very costly, but it is not optimized. In real life, something more costly would be reading the file
+ // content, for example.
+ addFilesWithVisibility();
+ var anotherHiddenFile = spy(new TestInputFileBuilder("foo", String.format("src/%s", ".myHiddenFile." + LANGUAGE))
+ .setLanguage(LANGUAGE).setType(InputFile.Type.MAIN).setStatus(InputFile.Status.ADDED).setHidden(true).build());
+ underTest.add(anotherHiddenFile);
+ underTest.setAllowHiddenFileAnalysis(false);
+
+ assertThat(underTest.inputFiles(underTest.predicates().hasType(InputFile.Type.MAIN))).hasSize(1);
+ // Verify that the file type has not been evaluated
+ verify(anotherHiddenFile, never()).type();
+ }
+
+ private void addFilesWithVisibility() {
+ addFile(true);
+ addFile(false);
+ }
+
+ private void addFilesWithAllStatus() {
addFile(InputFile.Status.ADDED);
addFile(InputFile.Status.CHANGED);
addFile(InputFile.Status.SAME);
}
private void addFile(InputFile.Status status) {
- underTest.add(new TestInputFileBuilder("foo", String.format("src/%s", generateFilename(status)))
- .setLanguage(LANGUAGE).setStatus(status).build());
+ addFile(status, false);
+ }
+
+ private void addFile(boolean hidden) {
+ addFile(InputFile.Status.SAME, hidden);
+ }
+
+ private void addFile(InputFile.Status status, boolean hidden) {
+ underTest.add(new TestInputFileBuilder("foo", String.format("src/%s", generateFilename(status, hidden)))
+ .setLanguage(LANGUAGE).setType(InputFile.Type.MAIN).setStatus(status).setHidden(hidden).build());
+ }
+
+ private String generateFilename(boolean hidden) {
+ return generateFilename(InputFile.Status.SAME, hidden);
}
private String generateFilename(InputFile.Status status) {
- return String.format("%s.%s", status.name().toLowerCase(Locale.ROOT), LANGUAGE);
+ return generateFilename(status, false);
+ }
+
+ private String generateFilename(InputFile.Status status, boolean hidden) {
+ return String.format("%s.%s.%s", status.name().toLowerCase(Locale.ROOT), hidden, LANGUAGE);
}
}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java
index e769e0d6451..355fb2cde0e 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java
@@ -19,16 +19,6 @@
*/
package org.sonar.scanner.sensor;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.assertj.core.data.MapEntry.entry;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoInteractions;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.when;
-
import java.io.File;
import java.util.ArrayList;
import java.util.List;
@@ -77,6 +67,16 @@ import org.sonar.scanner.repository.ContextPropertiesCache;
import org.sonar.scanner.repository.TelemetryCache;
import org.sonar.scanner.scan.branch.BranchConfiguration;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.data.MapEntry.entry;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
class DefaultSensorStorageTest {
@TempDir
@@ -97,7 +97,6 @@ class DefaultSensorStorageTest {
public void prepare() {
MetricFinder metricFinder = mock(MetricFinder.class);
when(metricFinder.<Integer>findByKey(CoreMetrics.NCLOC_KEY)).thenReturn(CoreMetrics.NCLOC);
- when(metricFinder.<String>findByKey(CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION_KEY)).thenReturn(CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION);
when(metricFinder.<Integer>findByKey(CoreMetrics.LINES_TO_COVER_KEY)).thenReturn(CoreMetrics.LINES_TO_COVER);
settings = new MapSettings();
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/ModuleSensorContextTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/ModuleSensorContextTest.java
index 4ab9f46fb4a..3a0ff7fb4c2 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/ModuleSensorContextTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/ModuleSensorContextTest.java
@@ -68,7 +68,7 @@ class ModuleSensorContextTest {
@BeforeEach
void prepare() {
fs = new DefaultFileSystem(temp);
- underTest = new ModuleSensorContext(mock(DefaultInputProject.class), mock(InputModule.class), settings.asConfig(), settings, fs, activeRules, sensorStorage, runtime,
+ underTest = new ModuleSensorContext(mock(DefaultInputProject.class), mock(InputModule.class), settings.asConfig(), fs, activeRules, sensorStorage, runtime,
branchConfiguration, writeCache, readCache, analysisCacheEnabled, unchangedFilesHandler, executingSensorContext, pluginRepository);
}
@@ -104,7 +104,7 @@ class ModuleSensorContextTest {
@Test
void pull_request_can_skip_unchanged_files() {
when(branchConfiguration.isPullRequest()).thenReturn(true);
- underTest = new ModuleSensorContext(mock(DefaultInputProject.class), mock(InputModule.class), settings.asConfig(), settings, fs, activeRules, sensorStorage, runtime,
+ underTest = new ModuleSensorContext(mock(DefaultInputProject.class), mock(InputModule.class), settings.asConfig(), fs, activeRules, sensorStorage, runtime,
branchConfiguration, writeCache, readCache, analysisCacheEnabled, unchangedFilesHandler, executingSensorContext, pluginRepository);
assertThat(underTest.canSkipUnchangedFiles()).isTrue();
}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/ProjectSensorContextTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/ProjectSensorContextTest.java
index 3c7f3d36793..01c337a5ed0 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/ProjectSensorContextTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/ProjectSensorContextTest.java
@@ -59,8 +59,8 @@ class ProjectSensorContextTest {
private ExecutingSensorContext executingSensorContext = mock(ExecutingSensorContext.class);
private ScannerPluginRepository pluginRepository = mock(ScannerPluginRepository.class);
- private ProjectSensorContext underTest = new ProjectSensorContext(mock(DefaultInputProject.class), settings.asConfig(), settings, fs, activeRules, sensorStorage, runtime,
- branchConfiguration, writeCache, readCache, analysisCacheEnabled, unchangedFilesHandler, executingSensorContext, pluginRepository);
+ private ProjectSensorContext underTest = new ProjectSensorContext(mock(DefaultInputProject.class), settings.asConfig(), fs, activeRules, sensorStorage, runtime,
+ branchConfiguration, writeCache, readCache, analysisCacheEnabled, unchangedFilesHandler, executingSensorContext, pluginRepository);
private static final String PLUGIN_KEY = "org.sonarsource.pluginKey";
@@ -69,7 +69,6 @@ class ProjectSensorContextTest {
when(executingSensorContext.getSensorExecuting()).thenReturn(new SensorId(PLUGIN_KEY, "sensorName"));
}
-
@Test
void addTelemetryProperty_whenTheOrganizationIsSonarSource_mustStoreTheTelemetry() {
@@ -77,16 +76,21 @@ class ProjectSensorContextTest {
underTest.addTelemetryProperty("key", "value");
- //then verify that the defaultStorage is called with the telemetry property once
+ // then verify that the defaultStorage is called with the telemetry property once
verify(sensorStorage).storeTelemetry("key", "value");
}
@Test
- void addTelemetryProperty_whenTheOrganizationIsNotSonarSource_mustThrowExcaption() {
+ void addTelemetryProperty_whenTheOrganizationIsNotSonarSource_mustThrowException() {
when(pluginRepository.getPluginInfo(PLUGIN_KEY)).thenReturn(new PluginInfo(PLUGIN_KEY).setOrganizationName("notSonarsource"));
assertThrows(IllegalStateException.class, () -> underTest.addTelemetryProperty("key", "value"));
verifyNoInteractions(sensorStorage);
}
+
+ @Test
+ void settings_throwsUnsupportedOperationException() {
+ assertThrows(UnsupportedOperationException.class, () -> underTest.settings());
+ }
}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scm/git/ChangedFileTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/ChangedFileTest.java
index fe637015ed7..91f59b964b9 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scm/git/ChangedFileTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/ChangedFileTest.java
@@ -88,7 +88,7 @@ public class ChangedFileTest {
secure().next(5),
Integer.parseInt(secure().nextNumeric(5)),
new SensorStrategy(),
- oldRelativePath);
+ oldRelativePath, false);
}
}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scm/git/JGitUtilsTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/JGitUtilsTest.java
index 383e2a1d643..d8264a5745b 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scm/git/JGitUtilsTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/JGitUtilsTest.java
@@ -23,6 +23,7 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
+import org.apache.commons.lang.SystemUtils;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.junit.jupiter.api.Test;
@@ -46,7 +47,11 @@ class JGitUtilsTest {
// in directory1, the entire directory is ignored without listing each file
// in directory2, specific files are ignored, so those files are listed
// in directory3, specific files are ignored via a separate .gitignore file
- assertThat(result).isEqualTo(List.of("directory1", "directory2/file_a.txt", "directory3/file_b.txt"));
+ if (SystemUtils.IS_OS_WINDOWS) {
+ assertThat(result).isEqualTo(List.of("directory1", "directory2\\file_a.txt", "directory3\\file_b.txt"));
+ } else {
+ assertThat(result).isEqualTo(List.of("directory1", "directory2/file_a.txt", "directory3/file_b.txt"));
+ }
}
@Test
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scm/git/NativeGitBlameCommandTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/NativeGitBlameCommandTest.java
index 68b7e1b7bd2..23d5e6ca5f0 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scm/git/NativeGitBlameCommandTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/NativeGitBlameCommandTest.java
@@ -159,14 +159,14 @@ class NativeGitBlameCommandTest {
ProcessWrapperFactory mockFactory = mock(ProcessWrapperFactory.class);
ProcessWrapper mockProcess = mock(ProcessWrapper.class);
String gitCommand = "git";
- when(mockFactory.create(any(), any(), anyString(), anyString(), anyString(), anyString(),
+ when(mockFactory.create(any(), any(), any(), anyString(), anyString(), anyString(), anyString(),
anyString(), anyString(), anyString(), anyString(), anyString(), anyString()))
- .then(invocation -> mockProcess);
+ .then(invocation -> mockProcess);
NativeGitBlameCommand blameCommand = new NativeGitBlameCommand(gitCommand, System2.INSTANCE, mockFactory);
blameCommand.blame(baseDir.toPath(), DUMMY_JAVA);
- verify(mockFactory).create(any(), any(), eq(gitCommand),
+ verify(mockFactory).create(any(), any(), any(), eq(gitCommand),
eq(GIT_DIR_FLAG),
eq(String.format(GIT_DIR_ARGUMENT, baseDir.toPath())),
eq(GIT_DIR_FORCE_FLAG),
@@ -238,7 +238,7 @@ class NativeGitBlameCommandTest {
"git version 2.25.1.msysgit.2").forEach(output -> {
ProcessWrapperFactory mockedCmd = mockGitVersionCommand(output);
mockGitWhereOnWindows(mockedCmd);
- when(mockedCmd.create(isNull(), any(), eq("C:\\mockGit.exe"), eq("--version"))).then(invocation -> {
+ when(mockedCmd.create(isNull(), any(), any(), eq("C:\\mockGit.exe"), eq("--version"))).then(invocation -> {
var argument = (Consumer<String>) invocation.getArgument(1);
argument.accept(output);
return mock(ProcessWrapper.class);
@@ -303,7 +303,7 @@ class NativeGitBlameCommandTest {
ProcessWrapper mockProcess = mock(ProcessWrapper.class);
mockGitWhereOnWindows(mockFactory);
- when(mockFactory.create(isNull(), any(), eq("C:\\mockGit.exe"), eq("--version"))).then(invocation -> {
+ when(mockFactory.create(isNull(), any(), any(), eq("C:\\mockGit.exe"), eq("--version"))).then(invocation -> {
var argument = (Consumer<String>) invocation.getArgument(1);
argument.accept("git version 2.30.1");
return mockProcess;
@@ -346,7 +346,7 @@ class NativeGitBlameCommandTest {
}
private void mockGitWhereOnWindows(ProcessWrapperFactory processWrapperFactory) {
- when(processWrapperFactory.create(isNull(), any(), eq("C:\\Windows\\System32\\where.exe"), eq("$PATH:git.exe"))).then(invocation -> {
+ when(processWrapperFactory.create(isNull(), any(), any(), eq("C:\\Windows\\System32\\where.exe"), eq("$PATH:git.exe"))).then(invocation -> {
var argument = (Consumer<String>) invocation.getArgument(1);
argument.accept("C:\\mockGit.exe");
return mock(ProcessWrapper.class);
@@ -357,7 +357,7 @@ class NativeGitBlameCommandTest {
ProcessWrapperFactory mockFactory = mock(ProcessWrapperFactory.class);
ProcessWrapper mockProcess = mock(ProcessWrapper.class);
- when(mockFactory.create(isNull(), any(), eq("git"), eq("--version"))).then(invocation -> {
+ when(mockFactory.create(isNull(), any(), any(), eq("git"), eq("--version"))).then(invocation -> {
var argument = (Consumer<String>) invocation.getArgument(1);
argument.accept(commandOutput);
return mockProcess;
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/sca/echo_args.bat b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/sca/echo_args.bat
index 5677cf5c437..577375b330d 100644
--- a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/sca/echo_args.bat
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/sca/echo_args.bat
@@ -6,7 +6,7 @@ set "POSITIONAL_ARGS="
:loop
if "%~1"=="" goto endloop
-if "%~1"=="--zip-filename" (
+if "%~1"=="--xz-filename" (
set "FILENAME=%~2"
shift
shift
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/sca/echo_args.sh b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/sca/echo_args.sh
index 881be2eaac5..f7feed1f501 100755
--- a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/sca/echo_args.sh
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/sca/echo_args.sh
@@ -6,7 +6,7 @@ POSITIONAL_ARGS=()
while [[ $# -gt 0 ]]; do
case $1 in
- --zip-filename)
+ --xz-filename)
FILENAME="$2"
shift
shift
diff --git a/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/sonar-project.properties b/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/sonar-project.properties
new file mode 100644
index 00000000000..d704931fcdc
--- /dev/null
+++ b/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/sonar-project.properties
@@ -0,0 +1,4 @@
+sonar.projectKey=sample-with-hidden-files
+sonar.projectName=Sample with hidden files
+sonar.projectVersion=0.1-SNAPSHOT
+sonar.sources=xources
diff --git a/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/.hidden/.nestedHidden/.xoo b/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/.hidden/.nestedHidden/.xoo
new file mode 100644
index 00000000000..9d5005aafea
--- /dev/null
+++ b/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/.hidden/.nestedHidden/.xoo
@@ -0,0 +1 @@
+Some random content \ No newline at end of file
diff --git a/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/.hidden/.nestedHidden/Class.xoo b/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/.hidden/.nestedHidden/Class.xoo
new file mode 100644
index 00000000000..fe9d2e54718
--- /dev/null
+++ b/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/.hidden/.nestedHidden/Class.xoo
@@ -0,0 +1,8 @@
+package hello;
+
+public class ClassOne {
+
+ public static void main(String[] args) {
+ System.out.println("ClassOne");
+ }
+}
diff --git a/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/.hidden/.nestedHidden/visibleInHiddenFolder/.xoo b/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/.hidden/.nestedHidden/visibleInHiddenFolder/.xoo
new file mode 100644
index 00000000000..9d5005aafea
--- /dev/null
+++ b/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/.hidden/.nestedHidden/visibleInHiddenFolder/.xoo
@@ -0,0 +1 @@
+Some random content \ No newline at end of file
diff --git a/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/.hidden/.nestedHidden/visibleInHiddenFolder/Class.xoo b/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/.hidden/.nestedHidden/visibleInHiddenFolder/Class.xoo
new file mode 100644
index 00000000000..fe9d2e54718
--- /dev/null
+++ b/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/.hidden/.nestedHidden/visibleInHiddenFolder/Class.xoo
@@ -0,0 +1,8 @@
+package hello;
+
+public class ClassOne {
+
+ public static void main(String[] args) {
+ System.out.println("ClassOne");
+ }
+}
diff --git a/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/.hidden/.xoo b/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/.hidden/.xoo
new file mode 100644
index 00000000000..9d5005aafea
--- /dev/null
+++ b/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/.hidden/.xoo
@@ -0,0 +1 @@
+Some random content \ No newline at end of file
diff --git a/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/.hidden/Class.xoo b/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/.hidden/Class.xoo
new file mode 100644
index 00000000000..fe9d2e54718
--- /dev/null
+++ b/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/.hidden/Class.xoo
@@ -0,0 +1,8 @@
+package hello;
+
+public class ClassOne {
+
+ public static void main(String[] args) {
+ System.out.println("ClassOne");
+ }
+}
diff --git a/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/nonHidden/.hiddenInVisibleFolder/.xoo b/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/nonHidden/.hiddenInVisibleFolder/.xoo
new file mode 100644
index 00000000000..9d5005aafea
--- /dev/null
+++ b/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/nonHidden/.hiddenInVisibleFolder/.xoo
@@ -0,0 +1 @@
+Some random content \ No newline at end of file
diff --git a/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/nonHidden/.hiddenInVisibleFolder/.xoo.ignore b/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/nonHidden/.hiddenInVisibleFolder/.xoo.ignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/nonHidden/.hiddenInVisibleFolder/.xoo.ignore
diff --git a/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/nonHidden/.hiddenInVisibleFolder/Class.xoo b/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/nonHidden/.hiddenInVisibleFolder/Class.xoo
new file mode 100644
index 00000000000..fe9d2e54718
--- /dev/null
+++ b/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/nonHidden/.hiddenInVisibleFolder/Class.xoo
@@ -0,0 +1,8 @@
+package hello;
+
+public class ClassOne {
+
+ public static void main(String[] args) {
+ System.out.println("ClassOne");
+ }
+}
diff --git a/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/nonHidden/.xoo b/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/nonHidden/.xoo
new file mode 100644
index 00000000000..9d5005aafea
--- /dev/null
+++ b/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/nonHidden/.xoo
@@ -0,0 +1 @@
+Some random content \ No newline at end of file
diff --git a/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/nonHidden/Class.xoo b/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/nonHidden/Class.xoo
new file mode 100644
index 00000000000..fe9d2e54718
--- /dev/null
+++ b/sonar-scanner-engine/test-resources/mediumtest/xoo/sample-with-hidden-files/xources/nonHidden/Class.xoo
@@ -0,0 +1,8 @@
+package hello;
+
+public class ClassOne {
+
+ public static void main(String[] args) {
+ System.out.println("ClassOne");
+ }
+}
diff --git a/sonar-scanner-protocol/src/it/java/org/sonar/scanner/protocol/output/ScannerReportReaderIT.java b/sonar-scanner-protocol/src/it/java/org/sonar/scanner/protocol/output/ScannerReportReaderIT.java
index 384e42bb191..674c92e15fb 100644
--- a/sonar-scanner-protocol/src/it/java/org/sonar/scanner/protocol/output/ScannerReportReaderIT.java
+++ b/sonar-scanner-protocol/src/it/java/org/sonar/scanner/protocol/output/ScannerReportReaderIT.java
@@ -391,16 +391,16 @@ public class ScannerReportReaderIT {
}
@Test
- public void readDependencyFilesZip_withNoFile_returnsNull() {
- assertThat(underTest.readDependencyFilesZip()).isNull();
+ public void readDependencyFilesArchive_withNoFile_returnsNull() {
+ assertThat(underTest.readDependencyFilesArchive()).isNull();
}
@Test
- public void readDependencyFilesZip_withFile_returnsFile() throws IOException {
+ public void readDependencyFilesArchive_withFile_returnsFile() throws IOException {
ScannerReportWriter writer = new ScannerReportWriter(fileStructure);
temp.create();
- File tempFile = temp.newFile("dependency-files.zip");
+ File tempFile = temp.newFile("dependency-files.tar.xz");
byte[] expectedBytes = "hello world!".getBytes();
try (FileOutputStream fos = new FileOutputStream(tempFile)) {
fos.write(expectedBytes);
@@ -408,8 +408,8 @@ public class ScannerReportReaderIT {
writer.writeScaFile(tempFile);
- assertThat(underTest.readDependencyFilesZip()).isNotNull();
- var returnBytes = FileUtils.readFileToByteArray(underTest.readDependencyFilesZip());
+ assertThat(underTest.readDependencyFilesArchive()).isNotNull();
+ var returnBytes = FileUtils.readFileToByteArray(underTest.readDependencyFilesArchive());
assertThat(returnBytes).isEqualTo(expectedBytes);
}
}
diff --git a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java
index fb10916aeac..ec2d8fcf660 100644
--- a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java
+++ b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java
@@ -114,7 +114,7 @@ public class FileStructure {
return sca;
}
- public File dependencyFilesZip() {
- return new File(scaDir(), "dependency-files.zip");
+ public File dependencyFilesArchive() {
+ return new File(scaDir(), "dependency-files.tar.xz");
}
}
diff --git a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportReader.java b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportReader.java
index 797c6ecae74..d7543f4a6d4 100644
--- a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportReader.java
+++ b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportReader.java
@@ -19,8 +19,6 @@
*/
package org.sonar.scanner.protocol.output;
-import static org.sonar.core.util.CloseableIterator.emptyCloseableIterator;
-
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
@@ -30,6 +28,8 @@ import javax.annotation.CheckForNull;
import org.sonar.core.util.CloseableIterator;
import org.sonar.core.util.Protobuf;
+import static org.sonar.core.util.CloseableIterator.emptyCloseableIterator;
+
public class ScannerReportReader {
private final FileStructure fileStructure;
@@ -218,8 +218,8 @@ public class ScannerReportReader {
return Protobuf.readStream(file, ScannerReport.AnalysisWarning.parser());
}
- public File readDependencyFilesZip() {
- File file = fileStructure.dependencyFilesZip();
+ public File readDependencyFilesArchive() {
+ File file = fileStructure.dependencyFilesArchive();
if (fileExists(file)) {
return file;
}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/BaseRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/BaseRequest.java
index 6a92121fd8c..73e99ed14b6 100644
--- a/sonar-ws/src/main/java/org/sonarqube/ws/client/BaseRequest.java
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/BaseRequest.java
@@ -29,8 +29,6 @@ import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
-import java.util.function.Function;
-import java.util.stream.Collectors;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonarqube.ws.MediaTypes;
@@ -90,7 +88,7 @@ abstract class BaseRequest<SELF extends BaseRequest<SELF>> implements WsRequest
* Expected media type of response. Default is {@link MediaTypes#JSON}.
*/
@SuppressWarnings("unchecked")
- public <T extends SELF> T setMediaType(String s) {
+ public <T extends SELF> T setMediaType(String s) {
requireNonNull(s, "media type of response cannot be null");
this.mediaType = s;
return (T) this;
@@ -143,18 +141,6 @@ abstract class BaseRequest<SELF extends BaseRequest<SELF>> implements WsRequest
}
@Override
- public Map<String, String> getParams() {
- return parameters.keyValues.keySet().stream()
- .collect(Collectors.toMap(
- Function.identity(),
- key -> parameters.keyValues.get(key).get(0),
- (v1, v2) -> {
- throw new IllegalStateException(String.format("Duplicate key '%s' in request", v1));
- },
- LinkedHashMap::new));
- }
-
- @Override
public Parameters getParameters() {
return parameters;
}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/DefaultWsClient.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/DefaultWsClient.java
index 70aee7bc31b..fe46a2ed771 100644
--- a/sonar-ws/src/main/java/org/sonarqube/ws/client/DefaultWsClient.java
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/DefaultWsClient.java
@@ -66,7 +66,6 @@ import org.sonarqube.ws.client.push.SonarLintServerPushService;
import org.sonarqube.ws.client.qualitygates.QualitygatesService;
import org.sonarqube.ws.client.qualityprofiles.QualityprofilesService;
import org.sonarqube.ws.client.regulatoryreports.RegulatoryReportsService;
-import org.sonarqube.ws.client.roots.RootsService;
import org.sonarqube.ws.client.rules.RulesService;
import org.sonarqube.ws.client.securityreports.SecurityReportsService;
import org.sonarqube.ws.client.server.ServerService;
@@ -130,7 +129,6 @@ class DefaultWsClient implements WsClient {
private final ProjectsService projectsService;
private final QualitygatesService qualitygatesService;
private final QualityprofilesService qualityprofilesService;
- private final RootsService rootsService;
private final RulesService rulesService;
private final ServerService serverService;
private final SettingsService settingsService;
@@ -197,7 +195,6 @@ class DefaultWsClient implements WsClient {
this.projectsService = new ProjectsService(wsConnector);
this.qualitygatesService = new QualitygatesService(wsConnector);
this.qualityprofilesService = new QualityprofilesService(wsConnector);
- this.rootsService = new RootsService(wsConnector);
this.rulesService = new RulesService(wsConnector);
this.serverService = new ServerService(wsConnector);
this.settingsService = new SettingsService(wsConnector);
@@ -453,11 +450,6 @@ class DefaultWsClient implements WsClient {
}
@Override
- public RootsService roots() {
- return rootsService;
- }
-
- @Override
public RulesService rules() {
return rulesService;
}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/WsClient.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/WsClient.java
index 3a3edda8b30..60350474b84 100644
--- a/sonar-ws/src/main/java/org/sonarqube/ws/client/WsClient.java
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/WsClient.java
@@ -66,7 +66,6 @@ import org.sonarqube.ws.client.push.SonarLintServerPushService;
import org.sonarqube.ws.client.qualitygates.QualitygatesService;
import org.sonarqube.ws.client.qualityprofiles.QualityprofilesService;
import org.sonarqube.ws.client.regulatoryreports.RegulatoryReportsService;
-import org.sonarqube.ws.client.roots.RootsService;
import org.sonarqube.ws.client.rules.RulesService;
import org.sonarqube.ws.client.securityreports.SecurityReportsService;
import org.sonarqube.ws.client.server.ServerService;
@@ -189,8 +188,6 @@ public interface WsClient {
QualityprofilesService qualityprofiles();
- RootsService roots();
-
RulesService rules();
ServerService server();
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/WsRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/WsRequest.java
index bf338d5a9cc..65dee5e36b2 100644
--- a/sonar-ws/src/main/java/org/sonarqube/ws/client/WsRequest.java
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/WsRequest.java
@@ -19,7 +19,6 @@
*/
package org.sonarqube.ws.client;
-import java.util.Map;
import java.util.OptionalInt;
/**
@@ -37,15 +36,6 @@ public interface WsRequest {
OptionalInt getWriteTimeOutInMs();
- /**
- *
- * In case of multi value parameters, returns the first value
- *
- * @deprecated since 6.1. Use {@link #getParameters()} instead
- */
- @Deprecated
- Map<String, String> getParams();
-
Parameters getParameters();
Headers getHeaders();
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java
index 46c73bfdab5..7844a135df7 100644
--- a/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java
@@ -93,6 +93,8 @@ public class IssuesWsParameters {
public static final String PARAM_PCI_DSS_40 = "pciDss-4.0";
public static final String PARAM_OWASP_ASVS = "owaspAsvs";
public static final String PARAM_OWASP_ASVS_40 = "owaspAsvs-4.0";
+ public static final String PARAM_OWASP_MOBILE_TOP_10 = "owaspMobileTop10";
+ public static final String PARAM_OWASP_MOBILE_TOP_10_2024 = "owaspMobileTop10-2024";
public static final String PARAM_OWASP_TOP_10 = "owaspTop10";
public static final String PARAM_OWASP_TOP_10_2021 = "owaspTop10-2021";
public static final String PARAM_STIG_ASD_V5R3 = "stig-ASD_V5R3";
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/roots/RootsService.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/roots/RootsService.java
deleted file mode 100644
index fba80339f52..00000000000
--- a/sonar-ws/src/main/java/org/sonarqube/ws/client/roots/RootsService.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * 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.sonarqube.ws.client.roots;
-
-import jakarta.annotation.Generated;
-import org.sonarqube.ws.MediaTypes;
-import org.sonarqube.ws.client.BaseService;
-import org.sonarqube.ws.client.GetRequest;
-import org.sonarqube.ws.client.PostRequest;
-import org.sonarqube.ws.client.WsConnector;
-import org.sonarqube.ws.Roots.SearchResponse;
-
-/**
- * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/roots">Further information about this web service online</a>
- */
-@Generated("sonar-ws-generator")
-public class RootsService extends BaseService {
-
- public RootsService(WsConnector wsConnector) {
- super(wsConnector, "api/roots");
- }
-
- /**
- *
- * This is part of the internal API.
- * This is a GET request.
- * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/roots/search">Further information about this action online (including a response example)</a>
- * @since 6.2
- */
- public SearchResponse search() {
- return call(
- new GetRequest(path("search")),
- SearchResponse.parser());
- }
-
- /**
- *
- * This is part of the internal API.
- * This is a POST request.
- * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/roots/set_root">Further information about this action online (including a response example)</a>
- * @since 6.2
- */
- public void setRoot(SetRootRequest request) {
- call(
- new PostRequest(path("set_root"))
- .setParam("login", request.getLogin())
- .setMediaType(MediaTypes.JSON)
- ).content();
- }
-
- /**
- *
- * This is part of the internal API.
- * This is a POST request.
- * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/roots/unset_root">Further information about this action online (including a response example)</a>
- * @since 6.2
- */
- public void unsetRoot(UnsetRootRequest request) {
- call(
- new PostRequest(path("unset_root"))
- .setParam("login", request.getLogin())
- .setMediaType(MediaTypes.JSON)
- ).content();
- }
-}
diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/BaseRequestTest.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/BaseRequestTest.java
index b8d51afcc99..74e91caa23d 100644
--- a/sonar-ws/src/test/java/org/sonarqube/ws/client/BaseRequestTest.java
+++ b/sonar-ws/src/test/java/org/sonarqube/ws/client/BaseRequestTest.java
@@ -33,12 +33,12 @@ import static org.assertj.core.data.MapEntry.entry;
public class BaseRequestTest {
- private FakeRequest underTest = new FakeRequest("api/foo");
+ private final FakeRequest underTest = new FakeRequest("api/foo");
@Test
public void test_defaults() {
assertThat(underTest.getMethod()).isEqualTo(WsRequest.Method.GET);
- assertThat(underTest.getParams()).isEmpty();
+ assertThat(underTest.getParameters().getKeys()).isEmpty();
assertThat(underTest.getMediaType()).isEqualTo(MediaTypes.JSON);
assertThat(underTest.getPath()).isEqualTo("api/foo");
assertThat(underTest.getWriteTimeOutInMs()).isEmpty();
@@ -58,16 +58,13 @@ public class BaseRequestTest {
@Test
public void keep_order_of_params() {
- assertThat(underTest.getParams()).isEmpty();
assertThat(underTest.getParameters().getKeys()).isEmpty();
underTest.setParam("keyB", "b");
- assertThat(underTest.getParams()).containsExactly(entry("keyB", "b"));
assertParameters(entry("keyB", "b"));
assertMultiValueParameters(entry("keyB", singletonList("b")));
underTest.setParam("keyA", "a");
- assertThat(underTest.getParams()).containsExactly(entry("keyB", "b"), entry("keyA", "a"));
assertParameters(entry("keyB", "b"), entry("keyA", "a"));
assertMultiValueParameters(entry("keyB", singletonList("b")), entry("keyA", singletonList("a")));
@@ -90,7 +87,7 @@ public class BaseRequestTest {
public void null_param_value() {
Boolean nullBool = null;
underTest.setParam("key", nullBool);
- assertThat(underTest.getParams()).isEmpty();
+ assertThat(underTest.getParameters().getKeys()).isEmpty();
}
@Test
diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/PostRequestTest.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/PostRequestTest.java
index ec50a1c7267..cf7882aca48 100644
--- a/sonar-ws/src/test/java/org/sonarqube/ws/client/PostRequestTest.java
+++ b/sonar-ws/src/test/java/org/sonarqube/ws/client/PostRequestTest.java
@@ -43,7 +43,7 @@ public class PostRequestTest {
public void empty_parts_and_params_by_default() {
PostRequest request = new PostRequest("api/issues/search");
assertThat(request.getParts()).isEmpty();
- assertThat(request.getParams()).isEmpty();
+ assertThat(request.getParameters().getKeys()).isEmpty();
}
@Test